diff --git a/handlers/coupons/__init__.py b/handlers/coupons/__init__.py new file mode 100644 index 00000000..9095f9df --- /dev/null +++ b/handlers/coupons/__init__.py @@ -0,0 +1,6 @@ +from aiogram import Router + +from .router import router + + +__all__ = ["router"] diff --git a/handlers/coupons.py b/handlers/coupons/router.py similarity index 61% rename from handlers/coupons.py rename to handlers/coupons/router.py index fc230c49..cb1d20ed 100644 --- a/handlers/coupons.py +++ b/handlers/coupons/router.py @@ -3,8 +3,7 @@ from typing import Any from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, InlineKeyboardButton, Message -from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.types import CallbackQuery, Message from database import ( check_coupon_usage, @@ -13,8 +12,8 @@ from database import ( update_balance, update_coupon_usage_count, ) - -from .utils import edit_or_send_message +from handlers.utils import edit_or_send_message +from keyboards.coupons import get_coupon_keyboard class CouponActivationState(StatesGroup): @@ -26,14 +25,16 @@ router = Router() @router.callback_query(F.data == "activate_coupon") @router.message(F.text == "/activate_coupon") -async def handle_activate_coupon(callback_query_or_message: Message | CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) +async def handle_activate_coupon(state: FSMContext, target_message: Message): + """ + Обрабатывает запрос на активацию купона. - if isinstance(callback_query_or_message, CallbackQuery): - target_message = callback_query_or_message.message - else: - target_message = callback_query_or_message + Args: + state: Контекст состояния FSM. + chat_id: ID чата (добавлено middleware). + target_message: Целевое сообщение для ответа (добавлено middleware). + """ + builder = get_coupon_keyboard() await edit_or_send_message( target_message=target_message, @@ -46,18 +47,37 @@ async def handle_activate_coupon(callback_query_or_message: Message | CallbackQu @router.message(CouponActivationState.waiting_for_coupon_code) -async def process_coupon_code(message: Message, state: FSMContext, session: Any): - coupon_code = message.text.strip() - activation_result = await activate_coupon(message.chat.id, coupon_code, session) +async def process_coupon_code(message: Message, state: FSMContext, session: Any, chat_id: int): + """ + Обрабатывает введенный код купона. - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + Args: + message: Сообщение с кодом купона. + state: Контекст состояния FSM. + session: Сессия базы данных. + chat_id: ID чата (добавлено middleware). + """ + coupon_code = message.text.strip() + activation_result = await activate_coupon(chat_id, coupon_code, session) + + builder = get_coupon_keyboard() await message.answer(activation_result, reply_markup=builder.as_markup()) await state.clear() -async def activate_coupon(user_id: int, coupon_code: str, session: Any): +async def activate_coupon(user_id: int, coupon_code: str, session: Any) -> str: + """ + Активирует купон для пользователя. + + Args: + user_id: ID пользователя. + coupon_code: Код купона. + session: Сессия базы данных. + + Returns: + str: Сообщение о результате активации. + """ coupon_record = await get_coupon_by_code(coupon_code, session) if not coupon_record: diff --git a/handlers/donate.py b/handlers/donate.py index 49fe0f3d..ce3cbd70 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -20,7 +20,7 @@ router = Router() @router.callback_query(F.data == "donate") -async def process_donate(callback_query: CallbackQuery, state: FSMContext): +async def process_donate(callback_query: CallbackQuery, state: FSMContext, target_message: Message): await state.clear() builder = InlineKeyboardBuilder() @@ -40,20 +40,20 @@ async def process_donate(callback_query: CallbackQuery, state: FSMContext): ) await edit_or_send_message( - target_message=callback_query.message, + target_message=target_message, text=text, reply_markup=builder.as_markup(), ) @router.callback_query(F.data == "enter_custom_donate_amount") -async def process_enter_donate_amount(callback_query: CallbackQuery, state: FSMContext): +async def process_enter_donate_amount(callback_query: CallbackQuery, state: FSMContext, target_message: Message): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate")) text = "💸 Введите сумму доната в рублях:" await edit_or_send_message( - target_message=callback_query.message, + target_message=target_message, text=text, reply_markup=builder.as_markup(), ) diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 56343f64..87a1034e 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -27,7 +27,7 @@ router = Router() @router.callback_query(F.data == "instructions") @router.message(F.text == "/instructions") -async def send_instructions(callback_query_or_message: CallbackQuery | Message): +async def send_instructions(target_message: Message): instructions_message = INSTRUCTIONS image_path = os.path.join("img", "instructions.jpg") @@ -35,11 +35,6 @@ async def send_instructions(callback_query_or_message: CallbackQuery | Message): builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL)) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - if isinstance(callback_query_or_message, CallbackQuery): - target_message = callback_query_or_message.message - else: - target_message = callback_query_or_message - await edit_or_send_message( target_message=target_message, text=instructions_message, @@ -49,14 +44,14 @@ async def send_instructions(callback_query_or_message: CallbackQuery | Message): @router.callback_query(F.data.startswith("connect_pc|")) -async def process_connect_pc(callback_query: CallbackQuery, session: Any): +async def process_connect_pc(callback_query: CallbackQuery, session: Any, target_message: Message): key_name = callback_query.data.split("|")[1] record = await get_key_details(key_name, session) if not record: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await edit_or_send_message( - target_message=callback_query.message, + target_message=target_message, text="❌ Ключ не найден. Проверьте имя ключа. 🔍", reply_markup=builder.as_markup(), media_path=None, @@ -75,7 +70,7 @@ async def process_connect_pc(callback_query: CallbackQuery, session: Any): builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await edit_or_send_message( - target_message=callback_query.message, + target_message=target_message, text=instruction_message, reply_markup=builder.as_markup(), media_path=None, @@ -83,7 +78,7 @@ async def process_connect_pc(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data.startswith("connect_tv|")) -async def process_connect_tv(callback_query: CallbackQuery): +async def process_connect_tv(callback_query: CallbackQuery, target_message: Message): key_name = callback_query.data.split("|")[1] builder = InlineKeyboardBuilder() @@ -92,7 +87,7 @@ async def process_connect_tv(callback_query: CallbackQuery): builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await edit_or_send_message( - target_message=callback_query.message, + target_message=target_message, text=CONNECT_TV_TEXT, reply_markup=builder.as_markup(), media_path=None, @@ -101,7 +96,7 @@ async def process_connect_tv(callback_query: CallbackQuery): @router.callback_query(F.data.startswith("continue_tv|")) -async def process_continue_tv(callback_query: CallbackQuery, session: Any): +async def process_continue_tv(callback_query: CallbackQuery, session: Any, target_message: Message): key_name = callback_query.data.split("|")[1] record = await get_key_details(key_name, session) @@ -114,5 +109,5 @@ async def process_continue_tv(callback_query: CallbackQuery, session: Any): builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await edit_or_send_message( - target_message=callback_query.message, text=message_text, reply_markup=builder.as_markup(), media_path=None + target_message=target_message, text=message_text, reply_markup=builder.as_markup(), media_path=None ) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index ed8344e7..a5d5401a 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -71,16 +71,18 @@ class Form(FSMContext): @router.callback_query(F.data == "create_key") -async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def confirm_create_new_key( + callback_query: CallbackQuery, state: FSMContext, session: Any, target_message: Message +): tg_id = callback_query.message.chat.id - await handle_key_creation(tg_id, state, session, callback_query) + await handle_key_creation(tg_id, state, session, target_message) async def handle_key_creation( tg_id: int, state: FSMContext, session: Any, - message_or_query: Message | CallbackQuery, + target_message: Message, ): """Создание ключа с учётом выбора тарифного плана.""" current_time = datetime.now(moscow_tz) @@ -94,14 +96,12 @@ async def handle_key_creation( updated = await update_trial(tg_id, 1, session) if updated: await edit_or_send_message( - target_message=message_or_query - if isinstance(message_or_query, Message) - else message_or_query.message, + target_message=target_message, text="⏳ Пожалуйста, подождите, создаем вам подключение...", reply_markup=None, ) - await create_key(tg_id, expiry_time, state, session, message_or_query) + await create_key(tg_id, expiry_time, state, session, target_message) return else: logger.error(f"Не удалось обновить статус триального периода для пользователя {tg_id}.") @@ -122,11 +122,6 @@ async def handle_key_creation( ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - if isinstance(message_or_query, CallbackQuery): - target_message = message_or_query.message - else: - target_message = message_or_query - await edit_or_send_message( target_message=target_message, text="💳 Выберите тарифный план для создания нового ключа:", @@ -195,13 +190,12 @@ async def create_key( expiry_time: datetime, state: FSMContext | None, session: Any, - message_or_query: Message | CallbackQuery | None = None, + target_message: Message, old_key_name: str = None, plan: int = None, ): """Создаёт ключ с заданным сроком действия.""" - target_message = message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query if not await check_connection_exists(tg_id): await add_connection(tg_id, balance=0.0, trial=0, session=session) logger.info(f"[Connection] Подключение создано для пользователя {tg_id}") diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index f5cb6148..5e9eaaed 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -74,14 +74,9 @@ router = Router() @router.callback_query(F.data == "view_keys") @router.message(F.text == "/subs") -async def process_callback_or_message_view_keys(callback_query_or_message: Message | CallbackQuery, session: Any): - if isinstance(callback_query_or_message, CallbackQuery): - target_message = callback_query_or_message.message - else: - target_message = callback_query_or_message - +async def process_callback_or_message_view_keys(session: Any, target_message: Message, chat_id: int): try: - records = await get_keys(target_message.chat.id, session) + records = await get_keys(chat_id, session) inline_keyboard, response_message = build_keys_response(records) image_path = os.path.join("img", "pic_keys.jpg") diff --git a/handlers/pay.py b/handlers/pay.py index cc041a3f..954c090b 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,5 +1,5 @@ from aiogram import F, Router -from aiogram.types import CallbackQuery, InlineKeyboardButton +from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from config import ( CRYPTO_BOT_ENABLE, @@ -17,7 +17,7 @@ router = Router() @router.callback_query(F.data == "pay") -async def handle_pay(callback_query: CallbackQuery): +async def handle_pay(callback_query: CallbackQuery, target_message: Message): builder = InlineKeyboardBuilder() if YOOKASSA_ENABLE: @@ -71,7 +71,7 @@ async def handle_pay(callback_query: CallbackQuery): ) await edit_or_send_message( - target_message=callback_query.message, + target_message=target_message, text=payment_text, reply_markup=builder.as_markup(), media_path=None, diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 9437e05d..1c67ec16 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -75,18 +75,18 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], - callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', + callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}", ), InlineKeyboardButton( text=PAYMENT_OPTIONS[i + 1]["text"], - callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}', + callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}", ), ) else: builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], - callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', + callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}", ) ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")) @@ -100,12 +100,12 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st logger.info(f"Created new connection for user {tg_id} with balance 0.0.") await callback_query.message.delete() - + new_message = await callback_query.message.answer( text="Выберите сумму пополнения:", reply_markup=builder.as_markup(), ) - await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id) + await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id) await state.set_state(ReplenishBalanceState.choosing_amount_robokassa) logger.info(f"Displayed amount selection for user {tg_id}.") diff --git a/handlers/profile.py b/handlers/profile.py deleted file mode 100644 index b592c192..00000000 --- a/handlers/profile.py +++ /dev/null @@ -1,255 +0,0 @@ -import html -import os - -from typing import Any - -import asyncpg - -from aiogram import F, Router -from aiogram.enums import ParseMode -from aiogram.fsm.context import FSMContext -from aiogram.types import ( - CallbackQuery, - InlineKeyboardButton, - InlineQuery, - InlineQueryResultArticle, - InputTextMessageContent, - Message, -) -from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import ( - DATABASE_URL, - INLINE_MODE, - INSTRUCTIONS_BUTTON, - NEWS_MESSAGE, - REFERRAL_OFFERS, - RENEWAL_PLANS, - TRIAL_TIME, - USERNAME_BOT, -) - -from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial -from handlers.buttons.profile import ( - ADD_SUB, - BALANCE, - BALANCE_HISTORY, - GIFTS, - INSTRUCTIONS, - INVITE, - MAIN_MENU, - MY_SUBS, - PAYMENT, -) -from handlers.texts import get_referral_link, invite_message_send, profile_message_send -from keyboards.admin.panel_kb import AdminPanelCallback -from logger import logger - -from .utils import edit_or_send_message - - -router = Router() - - -@router.callback_query(F.data == "profile") -@router.message(F.text == "/profile") -async def process_callback_view_profile( - callback_query_or_message: Message | CallbackQuery, - state: FSMContext, - admin: bool, -): - if isinstance(callback_query_or_message, CallbackQuery): - chat_id = callback_query_or_message.message.chat.id - username = html.escape(callback_query_or_message.from_user.full_name) - target_message = callback_query_or_message.message - else: - chat_id = callback_query_or_message.chat.id - username = html.escape(callback_query_or_message.from_user.full_name) - target_message = callback_query_or_message - - image_path = os.path.join("img", "profile.jpg") - logger.info(f"Переход в профиль. Используется изображение: {image_path}") - - key_count = await get_key_count(chat_id) - balance = await get_balance(chat_id) or 0 - - conn = await asyncpg.connect(DATABASE_URL) - try: - trial_status = await get_trial(chat_id, conn) - - profile_message = profile_message_send(username, chat_id, int(balance), key_count) - if key_count == 0: - profile_message += ( - "\n
🔧 Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение
" - ) - else: - profile_message += f"\n
{NEWS_MESSAGE}
" - - builder = InlineKeyboardBuilder() - if trial_status == 0 or key_count == 0: - builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key")) - else: - builder.row(InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys")) - builder.row(InlineKeyboardButton(text=BALANCE, callback_data="balance")) - builder.row( - InlineKeyboardButton(text=INVITE, callback_data="invite"), - InlineKeyboardButton(text=GIFTS, callback_data="gifts"), - ) - if INSTRUCTIONS_BUTTON: - builder.row(InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions")) - if admin: - builder.row( - InlineKeyboardButton(text="🔧 Администратор", callback_data=AdminPanelCallback(action="admin").pack()) - ) - - builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start")) - - await edit_or_send_message( - target_message=target_message, - text=profile_message, - reply_markup=builder.as_markup(), - media_path=image_path, - disable_web_page_preview=False, - force_text=True, - ) - finally: - await conn.close() - - -@router.callback_query(F.data == "balance") -async def balance_handler(callback_query: CallbackQuery, session: Any): - result = await session.fetchrow( - "SELECT balance FROM connections WHERE tg_id = $1", - callback_query.from_user.id, - ) - balance = result["balance"] if result else 0.0 - balance = int(balance) - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) - builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history")) - builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - - text = f"Управление вашим балансом 💰\n\nВаш баланс: {balance}" - await edit_or_send_message( - target_message=callback_query.message, - text=text, - reply_markup=builder.as_markup(), - media_path=None, - disable_web_page_preview=False, - ) - - -@router.callback_query(F.data == "balance_history") -async def balance_history_handler(callback_query: CallbackQuery, session: Any): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) - builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - - records = await get_last_payments(callback_query.from_user.id, session) - - if records: - history_text = "📊 Последние 3 операции с балансом:\n\n" - for record in records: - amount = record["amount"] - payment_system = record["payment_system"] - status = record["status"] - date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S") - history_text += ( - f"Сумма: {amount}₽\n" - f"Способ оплаты: {payment_system}\n" - f"Статус: {status}\n" - f"Дата: {date}\n\n" - ) - else: - history_text = "❌ У вас пока нет операций с балансом." - - await edit_or_send_message( - target_message=callback_query.message, - text=history_text, - reply_markup=builder.as_markup(), - media_path=None, - disable_web_page_preview=False, - ) - - -@router.message(F.text == "/tariffs") -@router.callback_query(F.data == "view_tariffs") -async def view_tariffs_handler(callback_query: CallbackQuery): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - - image_path = os.path.join("img", "tariffs.jpg") - tariffs_message = "🚀 Доступные тарифы VPN:\n\n" + "\n".join([ - f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: " - f"{RENEWAL_PLANS[months]['price']} " - f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей" - for months in sorted(RENEWAL_PLANS.keys(), key=int) - ]) - - await edit_or_send_message( - target_message=callback_query.message, - text=tariffs_message, - reply_markup=builder.as_markup(), - media_path=image_path, - disable_web_page_preview=False, - ) - - -@router.callback_query(F.data == "invite") -@router.message(F.text == "/invite") -async def invite_handler(callback_query_or_message: Message | CallbackQuery): - chat_id = None - if isinstance(callback_query_or_message, CallbackQuery): - chat_id = callback_query_or_message.message.chat.id - target_message = callback_query_or_message.message - else: - chat_id = callback_query_or_message.chat.id - target_message = callback_query_or_message - - referral_link = get_referral_link(chat_id) - referral_stats = await get_referral_stats(chat_id) - invite_message = invite_message_send(referral_link, referral_stats) - image_path = os.path.join("img", "pic_invite.jpg") - - builder = InlineKeyboardBuilder() - if INLINE_MODE: - builder.button(text="👥 Пригласить друга", switch_inline_query="invite") - else: - invite_text = f"\nПриглашаю тебя пользоваться действительно быстрым VPN вместе:\n\n{referral_link}" - builder.button(text="👥 Пригласить друга", switch_inline_query=invite_text) - builder.button(text="👤 Личный кабинет", callback_data="profile") - builder.adjust(1) - - await edit_or_send_message( - target_message=target_message, - text=invite_message, - reply_markup=builder.as_markup(), - media_path=image_path, - disable_web_page_preview=False, - ) - - -@router.inline_query(F.query.in_(["referral", "ref", "invite"])) -async def inline_referral_handler(inline_query: InlineQuery): - referral_link = f"https://t.me/{USERNAME_BOT}?start=referral_{inline_query.from_user.id}" - - results: list[InlineQueryResultArticle] = [] - - for index, offer in enumerate(REFERRAL_OFFERS): - description = offer["description"][:64] - message_text = offer["message"].format(trial_time=TRIAL_TIME)[:4096] - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text=offer["title"], url=referral_link)) - - results.append( - InlineQueryResultArticle( - id=str(index), - title=offer["title"], - description=description, - input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML), - reply_markup=builder.as_markup(), - ) - ) - - await inline_query.answer(results=results, cache_time=86400, is_personal=True) diff --git a/handlers/profile/__init__.py b/handlers/profile/__init__.py new file mode 100644 index 00000000..9095f9df --- /dev/null +++ b/handlers/profile/__init__.py @@ -0,0 +1,6 @@ +from aiogram import Router + +from .router import router + + +__all__ = ["router"] diff --git a/handlers/profile/router.py b/handlers/profile/router.py new file mode 100644 index 00000000..acbc4125 --- /dev/null +++ b/handlers/profile/router.py @@ -0,0 +1,175 @@ +import html +import os + +from typing import Any + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, InlineQuery, InlineQueryResultArticle, InputTextMessageContent, Message +from config import INLINE_MODE, REFERRAL_OFFERS, TRIAL_TIME, USERNAME_BOT + +from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial +from handlers.texts import get_referral_link, invite_message_send, profile_message_send +from handlers.utils import edit_or_send_message +from keyboards.profile import get_balance_keyboard, get_invite_keyboard, get_profile_keyboard + + +router = Router() + + +@router.callback_query(F.data == "profile") +@router.message(F.text == "/profile") +async def process_callback_view_profile( + state: FSMContext, + admin: bool, + chat_id: int, + target_message: Message, +): + """ + Обрабатывает запрос на просмотр профиля пользователя. + + Args: + state: Контекст состояния FSM. + admin: Флаг, указывающий, является ли пользователь администратором. + chat_id: ID чата (добавлено middleware). + target_message: Целевое сообщение для ответа (добавлено middleware). + """ + # Получаем информацию о профиле + profile_message = await profile_message_send(chat_id) + + # Получаем клавиатуру профиля + builder = get_profile_keyboard(admin) + + # Отправляем сообщение с профилем + await edit_or_send_message( + target_message=target_message, + text=profile_message, + reply_markup=builder.as_markup(), + media_path=None, + ) + + +@router.callback_query(F.data == "balance") +async def balance_handler(callback_query: CallbackQuery, session: Any, chat_id: int, target_message: Message): + """ + Обрабатывает запрос на просмотр баланса. + + Args: + callback_query: Колбэк запрос. + session: Сессия базы данных. + chat_id: ID чата (добавлено middleware). + target_message: Целевое сообщение для ответа (добавлено middleware). + """ + balance = await get_balance(chat_id, session) + + # Получаем клавиатуру баланса + builder = get_balance_keyboard() + + # Отправляем сообщение с балансом + await edit_or_send_message( + target_message=target_message, + text=f"💰 Ваш текущий баланс: {balance} руб.\n\n" + "Вы можете пополнить баланс через раздел 💸 Пополнить баланс в личном кабинете.", + reply_markup=builder.as_markup(), + media_path=None, + ) + + +@router.callback_query(F.data == "balance_history") +async def balance_history_handler(callback_query: CallbackQuery, session: Any, chat_id: int, target_message: Message): + """ + Обрабатывает запрос на просмотр истории баланса. + + Args: + callback_query: Колбэк запрос. + session: Сессия базы данных. + chat_id: ID чата (добавлено middleware). + target_message: Целевое сообщение для ответа (добавлено middleware). + """ + payments = await get_last_payments(chat_id, session) + + if not payments: + history_text = "📊 История операций:\n\nУ вас пока нет операций по балансу." + else: + history_text = "📊 История операций:\n\n" + for payment in payments: + amount = payment["amount"] + date = payment["created_at"].strftime("%d.%m.%Y %H:%M") + description = html.escape(payment["description"] or "") + + if amount > 0: + history_text += f"➕ {amount} руб. - {description} ({date})\n" + else: + history_text += f"➖ {abs(amount)} руб. - {description} ({date})\n" + + # Получаем клавиатуру баланса + builder = get_balance_keyboard() + + # Отправляем сообщение с историей баланса + await edit_or_send_message( + target_message=target_message, + text=history_text, + reply_markup=builder.as_markup(), + media_path=None, + ) + + +@router.callback_query(F.data == "invite") +@router.message(F.text == "/invite") +async def invite_handler(chat_id: int, target_message: Message): + """ + Обрабатывает запрос на приглашение друзей. + + Args: + chat_id: ID чата (добавлено middleware). + target_message: Целевое сообщение для ответа (добавлено middleware). + """ + referral_link = get_referral_link(chat_id) + referral_stats = await get_referral_stats(chat_id) + invite_message = invite_message_send(referral_link, referral_stats) + image_path = os.path.join("img", "pic_invite.jpg") + + # Получаем клавиатуру для приглашений + builder = get_invite_keyboard(chat_id, referral_link) + + # Отправляем сообщение с приглашением + await edit_or_send_message( + target_message=target_message, + text=invite_message, + reply_markup=builder.as_markup(), + media_path=image_path, + disable_web_page_preview=False, + ) + + +@router.inline_query(F.query.in_(["referral", "ref", "invite"])) +async def inline_referral_handler(inline_query: InlineQuery): + """ + Обрабатывает инлайн-запрос для реферальной программы. + + Args: + inline_query: Инлайн-запрос. + """ + + results = [] + + for index, offer in enumerate(REFERRAL_OFFERS): + description = offer["description"][:64] + message_text = offer["message"].format(trial_time=TRIAL_TIME)[:4096] + + results.append( + InlineQueryResultArticle( + id=f"ref_{index}", + title=offer["title"], + description=description, + input_message_content=InputTextMessageContent( + message_text=message_text, + parse_mode="HTML", + ), + thumbnail_url=offer.get("thumbnail_url"), + thumbnail_width=100, + thumbnail_height=100, + ) + ) + + await inline_query.answer(results=results, cache_time=300) diff --git a/keyboards/coupons/__init__.py b/keyboards/coupons/__init__.py new file mode 100644 index 00000000..020edffb --- /dev/null +++ b/keyboards/coupons/__init__.py @@ -0,0 +1,4 @@ +from .keyboards import get_coupon_keyboard + + +__all__ = ["get_coupon_keyboard"] diff --git a/keyboards/coupons/keyboards.py b/keyboards/coupons/keyboards.py new file mode 100644 index 00000000..6c77ba4f --- /dev/null +++ b/keyboards/coupons/keyboards.py @@ -0,0 +1,14 @@ +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + + +def get_coupon_keyboard() -> InlineKeyboardBuilder: + """ + Создает клавиатуру для работы с купонами. + + Returns: + InlineKeyboardBuilder: Построитель клавиатуры с кнопками для купонов. + """ + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + return builder diff --git a/keyboards/profile/__init__.py b/keyboards/profile/__init__.py new file mode 100644 index 00000000..96b8726b --- /dev/null +++ b/keyboards/profile/__init__.py @@ -0,0 +1,4 @@ +from .keyboards import get_balance_keyboard, get_invite_keyboard, get_profile_keyboard + + +__all__ = ["get_profile_keyboard", "get_balance_keyboard", "get_invite_keyboard"] diff --git a/keyboards/profile/keyboards.py b/keyboards/profile/keyboards.py new file mode 100644 index 00000000..ec0bc391 --- /dev/null +++ b/keyboards/profile/keyboards.py @@ -0,0 +1,78 @@ +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import INLINE_MODE + + +def get_profile_keyboard(admin: bool = False) -> InlineKeyboardBuilder: + """ + Создает клавиатуру для профиля пользователя. + + Args: + admin: Флаг, указывающий, является ли пользователь администратором. + + Returns: + InlineKeyboardBuilder: Построитель клавиатуры с кнопками профиля. + """ + builder = InlineKeyboardBuilder() + + # Основные кнопки профиля + builder.button(text="💰 Баланс", callback_data="balance") + builder.button(text="🔑 Мои подписки", callback_data="my_subs") + builder.button(text="💸 Пополнить баланс", callback_data="payment") + builder.button(text="🎁 Подарки", callback_data="gifts") + builder.button(text="👥 Пригласить друга", callback_data="invite") + builder.button(text="📚 Инструкции", callback_data="instructions") + builder.button(text="🎫 Активировать купон", callback_data="activate_coupon") + + # Кнопка админ-панели для администраторов + if admin: + builder.button(text="⚙️ Админ-панель", callback_data="admin_panel") + + # Кнопка главного меню + builder.button(text="🏠 Главное меню", callback_data="main_menu") + + # Настройка расположения кнопок (2 кнопки в ряд) + builder.adjust(2) + + return builder + + +def get_balance_keyboard() -> InlineKeyboardBuilder: + """ + Создает клавиатуру для раздела баланса. + + Returns: + InlineKeyboardBuilder: Построитель клавиатуры с кнопками для баланса. + """ + builder = InlineKeyboardBuilder() + + builder.button(text="📊 История баланса", callback_data="balance_history") + builder.button(text="👤 Личный кабинет", callback_data="profile") + builder.adjust(1) + + return builder + + +def get_invite_keyboard(chat_id: int, referral_link: str) -> InlineKeyboardBuilder: + """ + Создает клавиатуру для приглашения друзей. + + Args: + chat_id: ID чата пользователя. + referral_link: Реферальная ссылка. + + Returns: + InlineKeyboardBuilder: Построитель клавиатуры с кнопками для приглашений. + """ + builder = InlineKeyboardBuilder() + + if INLINE_MODE: + builder.button(text="👥 Пригласить друга", switch_inline_query="invite") + else: + invite_text = f"\nПриглашаю тебя пользоваться действительно быстрым VPN вместе:\n\n{referral_link}" + builder.button(text="👥 Пригласить друга", switch_inline_query=invite_text) + + builder.button(text="👤 Личный кабинет", callback_data="profile") + builder.adjust(1) + + return builder diff --git a/middlewares/__init__.py b/middlewares/__init__.py index f2b1d9d9..8235a424 100644 --- a/middlewares/__init__.py +++ b/middlewares/__init__.py @@ -6,6 +6,7 @@ from aiogram.dispatcher.middlewares.base import BaseMiddleware from .admin import AdminMiddleware from .loggings import LoggingMiddleware +from .message_handler import MessageHandlerMiddleware from .session import SessionMiddleware from .throttling import ThrottlingMiddleware from .user import UserMiddleware @@ -34,6 +35,7 @@ def register_middleware( "logging": LoggingMiddleware(), "throttling": ThrottlingMiddleware(), "user": UserMiddleware(), + "message_handler": MessageHandlerMiddleware(), } # Фильтруем middleware по списку исключений diff --git a/middlewares/message_handler.py b/middlewares/message_handler.py new file mode 100644 index 00000000..2b1b4e77 --- /dev/null +++ b/middlewares/message_handler.py @@ -0,0 +1,46 @@ +from collections.abc import Awaitable, Callable +from typing import Any, Dict, Union + +from aiogram import BaseMiddleware +from aiogram.types import CallbackQuery, Message, TelegramObject + + +class MessageHandlerMiddleware(BaseMiddleware): + """ + Middleware для обработки сообщений и callback-запросов. + + Добавляет в контекст обработчика следующие данные: + - chat_id: ID чата или пользователя + - target_message: Объект сообщения для ответа или редактирования + - is_callback: Флаг, указывающий, является ли запрос callback-запросом + """ + + async def __call__( + self, + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], + event: Message | CallbackQuery, + data: dict[str, Any], + ) -> Any: + """ + Обрабатывает входящее событие и добавляет в контекст необходимые данные. + + Args: + handler: Обработчик события. + event: Событие (сообщение или callback-запрос). + data: Словарь с данными контекста. + + Returns: + Any: Результат выполнения обработчика. + """ + # Определяем тип события и извлекаем нужные данные + if isinstance(event, CallbackQuery): + data["chat_id"] = event.from_user.id + data["target_message"] = event.message + data["is_callback"] = True + else: + data["chat_id"] = event.chat.id + data["target_message"] = event + data["is_callback"] = False + + # Вызываем следующий обработчик + return await handler(event, data)