From d3e47a662f8d814a0b9ac6f23a0fed6bce586052 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 5 Dec 2025 18:45:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/__init__.py | 32 +- handlers/profile.py | 919 ++++++++++++++++++++++---------------- handlers/referrals.py | 182 +++++--- handlers/start.py | 171 +++++-- handlers/subscriptions.py | 4 +- keyboard/keyboards.py | 910 ++++++++++++++++++------------------- 6 files changed, 1261 insertions(+), 957 deletions(-) diff --git a/handlers/__init__.py b/handlers/__init__.py index c88386f..e3b161e 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -1,16 +1,16 @@ -from .start import router as start_router -from .profile import router as profile_router -from .subscriptions import router as subscriptions_router -from .support import router as support_router -from .referrals import router as referrals_router - -# Список всех роутеров бота -routers = [ - start_router, - profile_router, - subscriptions_router, - support_router, - referrals_router, -] - -__all__ = ["routers"] +from .start import router as start_router +from .profile import router as profile_router +from .subscriptions import router as subscriptions_router +from .support import router as support_router +from .referrals import router as referrals_router + +# Список всех роутеров бота +routers = [ + start_router, + profile_router, + subscriptions_router, + support_router, + referrals_router, +] + +__all__ = ["routers"] diff --git a/handlers/profile.py b/handlers/profile.py index 8a5061d..3c03f5a 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,385 +1,534 @@ -# Профиль. последнее изменение 24.11.2025 - -from aiogram import Router, types -from aiogram.types import CallbackQuery -import logging -from datetime import datetime -from aiogram.enums.parse_mode import ParseMode -import locale -from instences.config import BASE_URL_FASTAPI -import aiohttp -from keyboard.keyboards import ( - account_keyboard, - popup_keyboard, - tranhist_keyboard, - confirm_popup_keyboard, - guide_keyboard, - balance_keyboard, - payment_methods_keyboard, -) - -locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") - -router = Router() -logger = logging.getLogger(__name__) - - -async def call_api(method, endpoint, data=None, base_url=BASE_URL_FASTAPI): - """ - Выполняет HTTP-запрос к FastAPI. - """ - url = f"{base_url}{endpoint}" - logger.info(f"Инициализация запроса: {method} {url} с данными {data}") - - try: - async with aiohttp.ClientSession() as session: - async with session.request( - method, - url, - json=data, - headers={"Content-Type": "application/json"} - ) as response: - logger.info( - f"Получен ответ от {url}: статус {response.status}" - ) - - if response.status in {200, 201}: - result = await response.json() - logger.debug(f"Ответ JSON: {result}") - return result - if response.status == 404: - logger.debug(f"Код {response.status}, возвращаю ничего") - return None - logger.error( - f"Ошибка в запросе: статус {response.status}, причина {response.reason}" - ) - return "ERROR" - except Exception as e: - logger.exception(f"Исключение при выполнении запроса к {url}: {e}") - return "ERROR" - - -@router.callback_query(lambda callback: callback.data == "profile") -async def profile_callback_handler(callback: CallbackQuery): - """ - Профиль пользователя. - Логика работы с API сохранена, изменён только текст/визуал. - """ - try: - user_data = await call_api("GET", f"/user/{callback.from_user.id}") - if not user_data: - await callback.message.answer( - "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." - ) - await callback.answer() - return - - # Последняя подписка пользователя - sub_data = await call_api( - "GET", f"/subscription/{user_data['telegram_id']}/last" - ) - if sub_data == "ERROR" or not isinstance(sub_data, dict): - sub_data = None - - username = callback.from_user.username or "-" - balance = user_data.get("balance", 0) - - # Статус подписки: Активна / Нет активных - sub_status = "⚫ Нет активных" - - if sub_data: - expiry_date = sub_data.get("expiry_date") - if expiry_date: - try: - is_expired = datetime.fromisoformat( - expiry_date) < datetime.now() - except ValueError: - is_expired = True - else: - is_expired = True - - if not is_expired: - sub_status = "🟢 Активна" - - text = ( - "🥚 Профиль\n\n" - f"Пользователь: @{username}\n" - f"Баланс: {balance} ₽\n" - f"Статус подписки: {sub_status}\n\n" - "Выберите действие:" - ) - - await callback.message.edit_text( - text, - parse_mode=ParseMode.HTML, - reply_markup=account_keyboard(), - ) - except Exception as e: - logger.exception(f"Ошибка в обработчике профиля: {e}") - await callback.message.answer("Произошла ошибка. Попробуйте позже.") - finally: - await callback.answer() - - -@router.callback_query(lambda callback: callback.data == "balance") -async def balance_callback_handler(callback: CallbackQuery): - """ - При нажатии «Пополнить баланс» показываем выбор суммы пополнения. - """ - await callback.message.edit_text( - "💳 Выберите сумму пополнения:", - reply_markup=popup_keyboard(), - ) - await callback.answer() - - -@router.callback_query(lambda callback: callback.data == "popup") -async def popup_callback_handler(callback: CallbackQuery): - """ - Обработчик callback_query для выбора суммы пополнения. - """ - user = await call_api("GET", f"/user/{callback.from_user.id}") - if not user: - await callback.message.answer( - "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." - ) - await callback.answer() - return - - await callback.message.edit_text( - "Выбери сумму для пополнения баланса.", - reply_markup=popup_keyboard(), - ) - await callback.answer() - - -@router.callback_query(lambda callback: callback.data == "tranhist") -async def tranhist_callback_handler(callback: CallbackQuery): - """ - Обработчик callback_query для истории транзакций. - (Логику и формат Markdown_V2 не трогаем, чтобы не поймать новые баги) - """ - user_data = await call_api("GET", f"/user/{callback.from_user.id}") - if not user_data: - await callback.message.edit_text("Вы еще не зарегистрированы.") - await callback.answer() - return - - try: - transactions = await call_api( - "GET", f"/user/{user_data['telegram_id']}/transactions" - ) - if not transactions: - await callback.message.edit_text( - "У вас нет транзакций.", reply_markup=tranhist_keyboard() - ) - await callback.answer() - return - - result = "Ваши транзакции:```\n" - for count, tran in enumerate(transactions, start=1): - dt = datetime.fromisoformat(tran["created_at"]).strftime( - "%d.%m.%Y %H:%M:%S" - ) - result += f"{count}. Сумма: {tran['amount']}, Дата: {dt}\n" - if len(result) > 4000: - result += "...\nСлишком много транзакций для отображения." - break - result += "```" - await callback.message.edit_text( - result, - parse_mode=ParseMode.MARKDOWN_V2, - reply_markup=tranhist_keyboard(), - ) - except Exception as e: - logger.error(f"Ошибка обработки транзакций: {e}") - await callback.message.edit_text( - "Произошла ошибка. Попробуйте позже." - ) - finally: - await callback.answer() - - -@router.callback_query(lambda callback: callback.data.startswith("popup:")) -async def popup_confirm_callback_handler(callback: CallbackQuery): - """ - После выбора суммы показываем варианты оплаты. - Разрешены только суммы 200, 300, 600, 1000 ₽. - """ - try: - _, amount_raw = callback.data.split(":", maxsplit=1) - amount = int(float(amount_raw)) - except Exception: - await callback.message.answer("Некорректная сумма пополнения.") - await callback.answer() - return - - if amount not in {200, 300, 600, 1000}: - await callback.message.answer( - "Эта сумма пополнения недоступна. Выбери вариант от 200 до 1000 ₽." - ) - await callback.answer() - return - - text = ( - f"💰 Сумма пополнения: {amount} ₽\n\n" - "Выбери способ оплаты:" - ) - - await callback.message.edit_text( - text=text, - reply_markup=payment_methods_keyboard(amount), - ) - await callback.answer() - - -@router.callback_query(lambda callback: callback.data.startswith("method_stars_")) -async def method_stars_handler(callback: CallbackQuery): - """ - Заглушка: оплата через Telegram Stars. - """ - amount = callback.data.split("_")[-1] - await callback.message.edit_text( - f"⭐ Оплата через Telegram Stars на {amount} ₽ пока в разработке.\n\n" - "Позже сюда подвяжем реальный платёж.", - ) - await callback.answer() - - -@router.callback_query(lambda callback: callback.data.startswith("method_ykassa_")) -async def method_ykassa_handler(callback: CallbackQuery): - """ - Обработчик оплаты через YooKassa. - """ - amount = callback.data.split("_")[-1] - - # Сразу отвечаем на callback, чтобы Telegram не считал запрос "старым" - try: - await callback.answer() - except Exception: - # Если по какой-то причине уже отвечали — просто игнорируем - pass - - # Формируем URL с query parameters вместо JSON body - endpoint = ( - f"/billing/payments/init?" - f"user_id={callback.from_user.id}&amount={float(amount)}&provider=yookassa" - ) - - logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") - - # Отправляем POST запрос с пустым телом (параметры в URL) - result = await call_api("POST", endpoint, None, "http://billing:8000") - - # Биллинг вообще не ответил/упал - if result == "ERROR" or not isinstance(result, dict): - await callback.message.edit_text( - "❌ Произошла ошибка при создании платежа. Попробуйте позже." - ) - return - - # Биллинг вернул ошибку (success = False) - if not result.get("success", False): - error_msg = ( - result.get("error") - or result.get("detail") - or "Неизвестная ошибка" - ) - await callback.message.edit_text(f"❌ Ошибка: {error_msg}") - return - - payment_url = result.get("confirmation_url", "#") - payment_id = result.get("payment_id", "") - - await callback.message.edit_text( - f"💵 Оплата через YooKassa\n\n" - f"💰 Сумма: {amount} руб\n" - f"📋 ID платежа: {payment_id}\n\n" - f"➡️ Перейти к оплате\n\n" - f"После оплаты нажмите кнопку 'Проверить оплату'", - parse_mode=ParseMode.HTML, - disable_web_page_preview=True, - reply_markup=types.InlineKeyboardMarkup( - inline_keyboard=[[ - types.InlineKeyboardButton( - text="🔄 Проверить оплату", - callback_data=f"check_payment:{payment_id}", - ) - ]] - ), - ) - - await callback.answer() - - -@router.callback_query(lambda callback: callback.data.startswith("method_crypto_")) -async def method_crypto_handler(callback: CallbackQuery): - """ - Оплата через CryptoBot. - """ - amount = callback.data.split("_")[-1] - - # Сразу отвечаем на callback, чтобы избежать таймаута - try: - await callback.answer() - except Exception: - pass - - endpoint = ( - f"/billing/payments/init?" - f"user_id={callback.from_user.id}&amount={float(amount)}&provider=cryptobot" - ) - - logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") - - result = await call_api("POST", endpoint, None, "http://billing:8000") - - if result == "ERROR" or not isinstance(result, dict): - await callback.message.edit_text( - "❌ Произошла ошибка при создании платежа. Попробуйте позже." - ) - return - - if not result.get("success", False): - error_msg = ( - result.get("error") - or result.get("detail") - or "Неизвестная ошибка" - ) - await callback.message.edit_text(f"❌ Ошибка: {error_msg}") - return - - payment_url = result.get("confirmation_url", "#") - payment_id = result.get("payment_id", "") - - await callback.message.edit_text( - f"💵 Оплата через Сryptobot\n\n" - f"💰 Сумма: {amount} руб\n" - f"📋 ID платежа: {payment_id}\n\n" - f"➡️ Перейти к оплате\n\n" - f"После оплаты нажмите кнопку 'Проверить оплату'", - parse_mode=ParseMode.HTML, - disable_web_page_preview=True, - reply_markup=types.InlineKeyboardMarkup( - inline_keyboard=[[ - types.InlineKeyboardButton( - text="🔄 Проверить оплату", - callback_data=f"check_payment:{payment_id}", - ) - ]] - ), - ) - - await callback.answer() - - -@router.callback_query(lambda callback: callback.data == "guide") -async def guide_callback_handler(callback: CallbackQuery): - """ - Обработчик callback_query для руководства. - """ - await callback.message.edit_text( - "Выбери платформу, для которой нужно руководство по подключению:", - reply_markup=guide_keyboard(), - ) - await callback.answer() +# Профиль. последнее изменениеы + +from aiogram import Router, types, F +from aiogram.types import ( + CallbackQuery, + LabeledPrice, + Message, + PreCheckoutQuery, +) +import logging +from datetime import datetime +from aiogram.enums.parse_mode import ParseMode +import locale +from instences.config import BASE_URL_FASTAPI +import aiohttp +from keyboard.keyboards import ( + account_keyboard, + popup_keyboard, + tranhist_keyboard, + confirm_popup_keyboard, + guide_keyboard, + balance_keyboard, + payment_methods_keyboard, +) + +locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") + +router = Router() +logger = logging.getLogger(__name__) + + +async def call_api(method, endpoint, data=None, base_url=BASE_URL_FASTAPI): + """ + Выполняет HTTP-запрос к FastAPI. + """ + url = f"{base_url}{endpoint}" + logger.info(f"Инициализация запроса: {method} {url} с данными {data}") + + try: + async with aiohttp.ClientSession() as session: + async with session.request( + method, + url, + json=data, + headers={"Content-Type": "application/json"} + ) as response: + logger.info( + f"Получен ответ от {url}: статус {response.status}" + ) + + if response.status in {200, 201}: + result = await response.json() + logger.debug(f"Ответ JSON: {result}") + return result + if response.status == 404: + logger.debug(f"Код {response.status}, возвращаю ничего") + return None + logger.error( + f"Ошибка в запросе: статус {response.status}, причина {response.reason}" + ) + return "ERROR" + except Exception as e: + logger.exception(f"Исключение при выполнении запроса к {url}: {e}") + return "ERROR" + + +@router.callback_query(lambda callback: callback.data == "profile") +async def profile_callback_handler(callback: CallbackQuery): + """ + Профиль пользователя. + Логика работы с API сохранена, изменён только текст/визуал. + """ + try: + user_data = await call_api("GET", f"/user/{callback.from_user.id}") + if not user_data: + await callback.message.answer( + "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." + ) + await callback.answer() + return + + sub_data = await call_api( + "GET", f"/subscription/{user_data['telegram_id']}/last" + ) + if sub_data == "ERROR" or not isinstance(sub_data, dict): + sub_data = None + + username = callback.from_user.username or "-" + balance = user_data.get("balance", 0) + + # Статус подписки: Активна / Нет активных + sub_status = "⚫ Нет активных" + + if sub_data: + expiry_date = sub_data.get("end_date") + if expiry_date: + try: + is_expired = datetime.fromisoformat( + expiry_date) < datetime.now() + except ValueError: + is_expired = True + else: + is_expired = True + + if not is_expired: + sub_status = "🟢 Активна" + + text = ( + "🥚 Профиль\n\n" + f"Пользователь: @{username}\n" + f"Баланс: {balance} ₽\n" + f"Статус подписки: {sub_status}\n\n" + "Выберите действие:" + ) + + await callback.message.edit_text( + text, + parse_mode=ParseMode.HTML, + reply_markup=account_keyboard(), + ) + except Exception as e: + logger.exception(f"Ошибка в обработчике профиля: {e}") + await callback.message.answer("Произошла ошибка. Попробуйте позже.") + finally: + await callback.answer() + + +@router.callback_query(lambda callback: callback.data == "balance") +async def balance_callback_handler(callback: CallbackQuery): + """ + При нажатии «Пополнить баланс» показываем выбор суммы пополнения. + """ + await callback.message.edit_text( + "💳 Выберите сумму пополнения:", + reply_markup=popup_keyboard(), + ) + await callback.answer() + + +@router.callback_query(lambda callback: callback.data == "popup") +async def popup_callback_handler(callback: CallbackQuery): + """ + Обработчик callback_query для выбора суммы пополнения. + """ + user = await call_api("GET", f"/user/{callback.from_user.id}") + if not user: + await callback.message.answer( + "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." + ) + await callback.answer() + return + + await callback.message.edit_text( + "Выбери сумму для пополнения баланса.", + reply_markup=popup_keyboard(), + ) + await callback.answer() + + +@router.callback_query(lambda callback: callback.data == "tranhist") +async def tranhist_callback_handler(callback: CallbackQuery): + """ + Обработчик callback_query для истории транзакций. + (Логику и формат Markdown_V2 не трогаем, чтобы не поймать новые баги) + """ + user_data = await call_api("GET", f"/user/{callback.from_user.id}") + if not user_data: + await callback.message.edit_text("Вы еще не зарегистрированы.") + await callback.answer() + return + + try: + transactions = await call_api( + "GET", f"/user/{user_data['telegram_id']}/transactions" + ) + if not transactions: + await callback.message.edit_text( + "У вас нет транзакций.", reply_markup=tranhist_keyboard() + ) + await callback.answer() + return + + result = "Ваши транзакции:```\n" + for count, tran in enumerate(transactions, start=1): + dt = datetime.fromisoformat(tran["created_at"]).strftime( + "%d.%m.%Y %H:%M:%S" + ) + result += f"{count}. Сумма: {tran['amount']}, Дата: {dt}\n" + if len(result) > 4000: + result += "...\nСлишком много транзакций для отображения." + break + result += "```" + await callback.message.edit_text( + result, + parse_mode=ParseMode.MARKDOWN_V2, + reply_markup=tranhist_keyboard(), + ) + except Exception as e: + logger.error(f"Ошибка обработки транзакций: {e}") + await callback.message.edit_text( + "Произошла ошибка. Попробуйте позже." + ) + finally: + await callback.answer() + + +@router.callback_query(lambda callback: callback.data.startswith("popup:")) +async def popup_confirm_callback_handler(callback: CallbackQuery): + """ + После выбора суммы показываем варианты оплаты. + Разрешены только суммы 200, 300, 600, 1000 ₽. + """ + try: + _, amount_raw = callback.data.split(":", maxsplit=1) + amount = int(float(amount_raw)) + except Exception: + await callback.message.answer("Некорректная сумма пополнения.") + await callback.answer() + return + + if amount not in {200, 300, 600, 1000}: + await callback.message.answer( + "Эта сумма пополнения недоступна. Выбери вариант от 200 до 1000 ₽." + ) + await callback.answer() + return + + text = ( + f"💰 Сумма пополнения: {amount} ₽\n\n" + "Выбери способ оплаты:" + ) + + await callback.message.edit_text( + text=text, + reply_markup=payment_methods_keyboard(amount), + ) + await callback.answer() + + +# ===== Telegram Stars ===== + +@router.callback_query(lambda callback: callback.data.startswith("method_stars_")) +async def method_stars_handler(callback: CallbackQuery): + """ + Оплата через Telegram Stars. + Формируем invoice прямо из бота, без отдельного биллинга. + """ + try: + amount_str = callback.data.split("_")[-1] + amount_rub = int(float(amount_str)) + except Exception: + await callback.message.answer("Некорректная сумма для оплаты.") + await callback.answer() + return + + payload = f"stars_topup:{callback.from_user.id}:{amount_rub}" + + stars_amount = amount_rub + + prices = [ + LabeledPrice( + label=f"Пополнение баланса на {amount_rub} ₽", + amount=stars_amount, + ) + ] + + try: + await callback.message.answer_invoice( + title="Пополнение баланса Lark VPN", + description=( + f"Пополнение баланса на {amount_rub} ₽ через Telegram Stars.\n\n" + "После успешной оплаты баланс будет зачислен автоматически." + ), + payload=payload, + provider_token="", # для Stars провайдер пустой + currency="XTR", + prices=prices, + ) + await callback.answer() + except Exception as e: + logger.exception(f"Ошибка при отправке invoice Telegram Stars: {e}") + await callback.message.answer( + "Не удалось создать счёт в Telegram Stars. Попробуй позже или выбери другой способ оплаты." + ) + await callback.answer() + + +@router.callback_query(lambda callback: callback.data.startswith("method_ykassa_")) +async def method_ykassa_handler(callback: CallbackQuery): + """ + Обработчик оплаты через YooKassa. + """ + amount = callback.data.split("_")[-1] + + try: + await callback.answer() + except Exception: + pass + + endpoint = ( + f"/billing/payments/init?" + f"user_id={callback.from_user.id}&amount={float(amount)}&provider=yookassa" + ) + + logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") + + result = await call_api("POST", endpoint, None, "http://billing:8000") + + if result == "ERROR" or not isinstance(result, dict): + await callback.message.edit_text( + "❌ Произошла ошибка при создании платежа. Попробуйте позже." + ) + return + + if not result.get("success", False): + error_msg = ( + result.get("error") + or result.get("detail") + or "Неизвестная ошибка" + ) + await callback.message.edit_text(f"❌ Ошибка: {error_msg}") + return + + payment_url = result.get("confirmation_url", "#") + payment_id = result.get("payment_id", "") + + await callback.message.edit_text( + f"💵 Оплата через YooKassa\n\n" + f"💰 Сумма: {amount} руб\n" + f"📋 ID платежа: {payment_id}\n\n" + f"➡️ Перейти к оплате\n\n" + f"После оплаты нажмите кнопку 'Проверить оплату'", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[[ + types.InlineKeyboardButton( + text="🔄 Проверить оплату", + callback_data=f"check_payment:{payment_id}", + ) + ]] + ), + ) + + await callback.answer() + + +@router.callback_query(lambda callback: callback.data.startswith("method_crypto_")) +async def method_crypto_handler(callback: CallbackQuery): + """ + Оплата через CryptoBot. + """ + amount = callback.data.split("_")[-1] + + try: + await callback.answer() + except Exception: + pass + + endpoint = ( + f"/billing/payments/init?" + f"user_id={callback.from_user.id}&amount={float(amount)}&provider=cryptobot" + ) + + logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") + + result = await call_api("POST", endpoint, None, "http://billing:8000") + + if result == "ERROR" or not isinstance(result, dict): + await callback.message.edit_text( + "❌ Произошла ошибка при создании платежа. Попробуйте позже." + ) + return + + if not result.get("success", False): + error_msg = ( + result.get("error") + or result.get("detail") + or "Неизвестная ошибка" + ) + await callback.message.edit_text(f"❌ Ошибка: {error_msg}") + return + + payment_url = result.get("confirmation_url", "#") + payment_id = result.get("payment_id", "") + + await callback.message.edit_text( + f"💵 Оплата через Сryptobot\n\n" + f"💰 Сумма: {amount} руб\n" + f"📋 ID платежа: {payment_id}\n\n" + f"➡️ Перейти к оплате\n\n" + f"После оплаты нажмите кнопку 'Проверить оплату'", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[[ + types.InlineKeyboardButton( + text="🔄 Проверить оплату", + callback_data=f"check_payment:{payment_id}", + ) + ]] + ), + ) + + await callback.answer() + + +@router.callback_query(lambda callback: callback.data.startswith("method_sbp_")) +async def method_sbp_handler(callback: types.CallbackQuery): + try: + amount = int(callback.data.split("_")[2]) + except Exception: + await callback.message.answer("Ошибка суммы платежа.") + await callback.answer() + return + + endpoint = ( + f"/billing/payments/init?" + f"user_id={callback.from_user.id}&amount={float(amount)}&provider=sbp" + ) + + result = await call_api("POST", endpoint,None ,"http://billing:8000") + + if result == "ERROR" or not isinstance(result, dict): + await callback.message.answer("Ошибка при инициализации платежа.") + await callback.answer() + return + + url = result.get("confirmation_url") + if not url: + await callback.message.answer("СБП временно недоступно.") + await callback.answer() + return + + await callback.message.answer( + f"🏦 Платёж через СБП:\nПерейди по ссылке для оплаты:\n{url}" + ) + await callback.answer() + + +@router.callback_query(lambda callback: callback.data == "guide") +async def guide_callback_handler(callback: CallbackQuery): + """ + Обработчик callback_query для руководства. + """ + await callback.message.edit_text( + "Выбери платформу, для которой нужно руководство по подключению:", + reply_markup=guide_keyboard(), + ) + await callback.answer() + + +# ===== Служебные хендлеры для платежей Telegram Stars ===== + +@router.pre_checkout_query() +async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery): + """ + Обязательный шаг для Telegram Payments: + подтверждаем pre_checkout_query, иначе платёж не пройдёт. + """ + try: + await pre_checkout_query.bot.answer_pre_checkout_query( + pre_checkout_query.id, + ok=True, + ) + except Exception as e: + logger.exception(f"Ошибка при answer_pre_checkout_query: {e}") + + +async def _process_stars_topup(message: Message): + """ + Логика зачисления средств после успешной оплаты Stars. + """ + sp = message.successful_payment + if not sp: + return + + payload = sp.invoice_payload or "" + parts = payload.split(":") + if len(parts) != 3 or parts[0] != "stars_topup": + logger.info( + "successful_payment не относится к пополнению баланса Stars.") + return + + _, telegram_id_str, amount_str = parts + + try: + amount_rub = int(amount_str) + except ValueError: + # На всякий случай fallback к total_amount + amount_rub = sp.total_amount + + data = { + "telegram_id": telegram_id_str, + "amount": amount_rub, + "currency": sp.currency, + "provider": "telegram_stars", + "telegram_payment_charge_id": sp.telegram_payment_charge_id, + } + + logger.info( + f"Обработка успешного платежа Telegram Stars: " + f"user={telegram_id_str}, amount={amount_rub}, currency={sp.currency}" + ) + + result = await call_api("POST", "/user/deposit", data) + + if result == "ERROR" or result is None: + await message.answer( + "⭐ Оплата через Telegram Stars прошла, но не удалось автоматически обновить баланс.\n" + "Если баланс не изменился — напиши, пожалуйста, в поддержку и укажи время платежа." + ) + return + + await message.answer( + f"⭐ Оплата через Telegram Stars успешно проведена.\n" + f"На твой баланс зачислено {amount_rub} ₽." + ) + + +@router.message(F.successful_payment) +async def successful_payment_handler(message: Message): + """ + Глобальный хендлер успешных платежей. + Сейчас используем только для пополнения баланса через Stars. + """ + try: + await _process_stars_topup(message) + except Exception as e: + logger.exception(f"Ошибка при обработке successful_payment: {e}") + await message.answer( + "Оплата прошла, но произошла ошибка при обработке. " + "Если баланс не изменился — напиши, пожалуйста, в поддержку." + ) diff --git a/handlers/referrals.py b/handlers/referrals.py index 283891a..a390b6b 100644 --- a/handlers/referrals.py +++ b/handlers/referrals.py @@ -1,59 +1,123 @@ -from aiogram import Router, types -from aiogram.filters import Command -from aiogram.enums.parse_mode import ParseMode -import logging - -router = Router() -logger = logging.getLogger(__name__) - - -async def _build_referral_text(bot, user_id: int) -> str: - me = await bot.get_me() - bot_username = me.username or "LarkVPN_bot" - - link = f"https://t.me/{bot_username}?start=ref_{user_id}" - - text = ( - "👥 Реферальная программа\n\n" - "Зови друзей в Lark VPN и получай бонусы на баланс.\n\n" - f"🔗 Твоя ссылка:\n{link}\n\n" - "👤 Приглашено: —\n" - "💰 Начислено бонусов: — ₽\n\n" - "Бонусы падают автоматически, когда приглашённые пополняют баланс." - ) - return text - - -@router.message(Command("referrals")) -async def referrals_command(message: types.Message): - """ - Команда /referrals — показывает текст реферальной программы. - """ - logger.info(f"Получена команда /referrals от {message.from_user.id}") - try: - text = await _build_referral_text(message.bot, message.from_user.id) - await message.answer(text, parse_mode=ParseMode.HTML) - except Exception as e: - logger.exception(f"Ошибка в обработчике /referrals: {e}") - await message.answer("Произошла ошибка. Попробуй позже.") - - -@router.callback_query(lambda callback: callback.data == "referral") -async def referrals_callback(callback: types.CallbackQuery): - """ - Кнопка «Реферальная программа» в главном меню. - """ - try: - text = await _build_referral_text( - callback.message.bot, - callback.from_user.id, - ) - await callback.message.edit_text( - text, - parse_mode=ParseMode.HTML, - ) - except Exception as e: - logger.exception(f"Ошибка в обработчике callback 'referral': {e}") - await callback.message.answer("Произошла ошибка. Попробуй позже.") - finally: - await callback.answer() +from aiogram import Router, types +from aiogram.filters import Command +from aiogram.enums.parse_mode import ParseMode +import logging +import aiohttp + +from instences.config import BASE_URL_FASTAPI + +router = Router() +logger = logging.getLogger(__name__) + + +async def call_api(method: str, endpoint: str, data=None, base_url: str = BASE_URL_FASTAPI): + """ + Универсальный HTTP-запрос к FastAPI для рефералок. + + Ожидаем: + GET /user/{telegram_id}/referrals -> { + "invited_count": int + } + """ + url = f"{base_url}{endpoint}" + logger.info(f"[referrals] Запрос: {method} {url} с данными {data}") + + try: + async with aiohttp.ClientSession() as session: + async with session.request( + method, + url, + json=data, + headers={"Content-Type": "application/json"}, + ) as response: + logger.info( + f"[referrals] Ответ от {url}: статус {response.status}" + ) + + if response.status in {200, 201}: + result = await response.json() + logger.debug(f"[referrals] Ответ JSON: {result}") + return result + if response.status == 404: + logger.debug("[referrals] 404, возвращаю None") + return None + + logger.error( + f"[referrals] Ошибка в запросе: статус {response.status}, " + f"причина {response.reason}" + ) + return "ERROR" + except Exception as e: + logger.exception(f"[referrals] Исключение при запросе к {url}: {e}") + return "ERROR" + + +async def _build_referral_text(bot, user_id: int) -> str: + """ + Текст реферальной программы + количество приглашённых. + """ + me = await bot.get_me() + bot_username = me.username or "LarkVPN_bot" + + link = f"https://t.me/{bot_username}?start=ref_{user_id}" + + text = ( + "👥 Реферальная программа\n\n" + "Зови друзей в Lark VPN и получай бонусы на баланс.\n\n" + f"🔗 Твоя ссылка:\n{link}\n\n" + ) + + invited_count = 0 + + stats = await call_api("GET", f"/user/{user_id}/referrals") + if isinstance(stats, dict): + raw = stats.get("invited_count") + try: + invited_count = int(raw) + except (TypeError, ValueError): + invited_count = 0 + elif stats == "ERROR": + logger.warning( + f"[referrals] Ошибка при получении статистики для user_id={user_id}" + ) + + text += f"👤 Приглашено: {invited_count}\n\n" + text += "Бонусы начисляются, когда приглашённые пополняют баланс." + + return text + + +@router.message(Command("referrals")) +async def referrals_command(message: types.Message): + """ + Команда /referrals — показывает текст реферальной программы. + """ + logger.info(f"[referrals] Команда /referrals от {message.from_user.id}") + try: + text = await _build_referral_text(message.bot, message.from_user.id) + await message.answer(text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.exception(f"[referrals] Ошибка в обработчике /referrals: {e}") + await message.answer("Произошла ошибка. Попробуй позже.") + + +@router.callback_query(lambda callback: callback.data == "referral") +async def referrals_callback(callback: types.CallbackQuery): + """ + Кнопка «Реферальная программа» (если где-то есть). + """ + try: + text = await _build_referral_text( + callback.message.bot, + callback.from_user.id, + ) + await callback.message.edit_text( + text, + parse_mode=ParseMode.HTML, + ) + except Exception as e: + logger.exception( + f"[referrals] Ошибка в обработчике callback 'referral': {e}") + await callback.message.answer("Произошла ошибка. Попробуй позже.") + finally: + await callback.answer() diff --git a/handlers/start.py b/handlers/start.py index 69c20eb..1ef80a3 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,92 +1,174 @@ from aiogram import Router, types from aiogram.filters import Command from aiogram.types import Message, CallbackQuery -import logging -from instences.config import BASE_URL_FASTAPI -import aiohttp -from keyboard.keyboards import main_keyboard from aiogram.enums.parse_mode import ParseMode +import logging +import aiohttp + +from instences.config import BASE_URL_FASTAPI +from keyboard.keyboards import main_keyboard from .referrals import _build_referral_text router = Router() logger = logging.getLogger(__name__) -async def call_api(method, endpoint, data=None): +async def call_api(method: str, endpoint: str, data=None): """ Выполняет HTTP-запрос к FastAPI. - :param method: HTTP метод (GET, POST, и т.д.) - :param endpoint: конечная точка API - :param data: тело запроса (если необходимо) - :return: JSON-ответ или "ERROR" при неуспехе + Возвращает: + - dict при 200/201 + - None при 404 + - "ERROR" при остальных ошибках """ url = f"{BASE_URL_FASTAPI}{endpoint}" - logger.info(f"Инициализация запроса: {method} {url} с данными {data}") + logger.info(f"[start] Запрос: {method} {url} с данными {data}") try: async with aiohttp.ClientSession() as session: async with session.request(method, url, json=data) as response: logger.info( - f"Получен ответ от {url}: статус {response.status}" + f"[start] Ответ от {url}: статус {response.status}" ) if response.status in {200, 201}: result = await response.json() - logger.debug(f"Ответ JSON: {result}") + logger.debug(f"[start] Ответ JSON: {result}") return result if response.status == 404: - logger.debug(f"Код {response.status}, возвращаю ничего") + logger.debug("[start] Получен 404, возвращаю None") return None + logger.error( - f"Ошибка в запросе: статус {response.status}, причина {response.reason}" + f"[start] Ошибка в запросе: статус {response.status}, " + f"причина {response.reason}" ) return "ERROR" except Exception as e: - logger.exception(f"Исключение при выполнении запроса к {url}: {e}") + logger.exception(f"[start] Исключение при запросе к {url}: {e}") return "ERROR" def _welcome_text(username: str | None) -> str: """ Текст приветствия в /start и в главном меню. - Имя пока не используем — оставляем сигнатуру на будущее. """ return "🥚 Lark Security\n\nВыберите действие из меню ниже." +def _parse_referrer_id(message: Message) -> int | None: + """ + Достаём ref_ из /start. + Примеры: + /start + /start ref_123456789 + """ + text = message.text or "" + parts = text.split(maxsplit=1) + if len(parts) < 2: + return None + + arg = parts[1].strip() + if not arg.startswith("ref_"): + return None + + raw_id = arg[4:] + if not raw_id.isdigit(): + return None + + try: + return int(raw_id) + except ValueError: + return None + + @router.message(Command("start")) async def start_command(message: Message): """ - Обработчик команды /start. - Визуал и текст — обновлены, логика работы с API не тронута. + /start c обработкой реферального параметра. """ + user_id = message.from_user.id + username = message.from_user.username + referrer_id = _parse_referrer_id(message) + logger.info( - f"Получена команда /start от пользователя: " - f"{message.from_user.id} ({message.from_user.username})" + f"[start] Команда /start от {user_id} (@{username}), " + f"text={message.text!r}, referrer_id={referrer_id}" ) try: - user_data = await call_api("GET", f"/user/{message.from_user.id}") - if not user_data: - logger.debug( - "Пользователь не найден в базе, создаем новую запись." - ) - await call_api( + # 1. Проверяем, есть ли пользователь в БД + existing = await call_api("GET", f"/user/{user_id}") + user_exists = existing not in (None, "ERROR") + + # 2. Если пользователя нет — создаём + if not user_exists: + logger.debug(f"[start] Пользователь {user_id} не найден, создаю.") + create_result = await call_api( "POST", "/user/create", - {"telegram_id": message.from_user.id}, + {"telegram_id": user_id}, ) + if create_result == "ERROR": + logger.error( + f"[start] Не удалось создать пользователя {user_id} в БД" + ) - logger.debug("Отправка приветственного сообщения пользователю.") + # 3. Обработка рефералки, если параметр есть + if referrer_id is not None: + # 3.1. Самореферал + if referrer_id == user_id: + logger.info( + f"[start] Пользователь {user_id} попытался зайти " + f"по своей реферальной ссылке." + ) + await message.answer( + "Нельзя переходить по своей же реферальной ссылке." + ) + + # 3.2. Пользователь уже зарегистрирован в боте + elif user_exists: + logger.info( + f"[start] Пользователь {user_id} уже есть в БД, " + f"реферальная ссылка {referrer_id} не сработает." + ) + await message.answer( + "Вы уже зарегистрированы в боте, " + "реферальная ссылка не сработает." + ) + + # 3.3. Новый пользователь + чужая рефералка → регистрируем реферал + else: + payload = { + "referrer_id": referrer_id, + "telegram_id": user_id, + } + logger.info( + f"[start] Фиксирую реферала в бекенде: {payload}" + ) + result = await call_api( + "POST", + "/user/referrals/track", + payload, + ) + if result == "ERROR": + logger.error( + f"[start] Ошибка при фиксации реферала: {payload}" + ) + await message.answer( + "Вы вошли по реферальной ссылке." + ) + + # 4. В любом случае показываем главное меню await message.answer( - _welcome_text(message.from_user.username), + _welcome_text(username), reply_markup=main_keyboard(), ) - logger.info("Приветственное сообщение отправлено.") + logger.info(f"[start] Главное меню отправлено пользователю {user_id}.") + except Exception as e: logger.exception( - f"Ошибка при обработке команды /start для пользователя " - f"{message.from_user.id}: {e}" + f"[start] Ошибка при обработке /start для пользователя {user_id}: {e}" ) await message.answer("Произошла ошибка. Попробуйте позже.") @@ -98,16 +180,16 @@ async def referrals_menu_command(message: Message): Показывает текст реферальной программы. """ logger.info( - f"Получена команда /referrals от пользователя: " - f"{message.from_user.id} ({message.from_user.username})" + f"[start] Команда /referrals от {message.from_user.id} " + f"(@{message.from_user.username})" ) try: text = await _build_referral_text(message.bot, message.from_user.id) await message.answer(text, parse_mode=ParseMode.HTML) - logger.info("Реферальная программа отправлена пользователю.") + logger.info("[start] Реферальная программа отправлена пользователю.") except Exception as e: logger.exception( - f"Ошибка при обработке команды /referrals для пользователя " + f"[start] Ошибка при обработке /referrals для пользователя " f"{message.from_user.id}: {e}" ) await message.answer("Произошла ошибка. Попробуйте позже.") @@ -116,24 +198,27 @@ async def referrals_menu_command(message: Message): @router.callback_query(lambda callback: callback.data == "base") async def start_callback_handler(callback: CallbackQuery): """ - Обработчик callback_query с data="base". - Возвращает пользователя в главное меню. + Callback с data="base" — возврат в главное меню. """ try: - user_data = await call_api("GET", f"/user/{callback.from_user.id}") - if not user_data: + user_id = callback.from_user.id + username = callback.from_user.username + logger.info(f"[start] callback 'base' от {user_id} (@{username})") + + user_data = await call_api("GET", f"/user/{user_id}") + if user_data in (None, "ERROR"): await call_api( "POST", "/user/create", - {"telegram_id": callback.from_user.id}, + {"telegram_id": user_id}, ) await callback.message.edit_text( - _welcome_text(callback.from_user.username), + _welcome_text(username), reply_markup=main_keyboard(), ) except Exception as e: - logger.exception(f"Ошибка при обработке callback с data='base': {e}") + logger.exception(f"[start] Ошибка при обработке callback 'base': {e}") await callback.message.answer("Произошла ошибка. Попробуйте позже.") finally: await callback.answer() diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index c02730c..097daa8 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -319,7 +319,7 @@ async def cb_sub_renew(callback: types.CallbackQuery): result = await call_api( "POST", "/subscription/buy", - {"telegram_id": str(callback.from_user.id), "plan_id": plan_id}, + {"telegram_id": str(callback.from_user.id), "plan_name": plan_id}, ) # Ошибки backend (detail) @@ -472,7 +472,7 @@ async def confirm_callback_handler(callback: types.CallbackQuery): result = await call_api( "POST", "/subscription/buy", - {"telegram_id": str(callback.from_user.id), "plan_id": plan_id}, + {"telegram_id": str(callback.from_user.id), "plan_name": plan_id}, ) if result == "ERROR" or not isinstance(result, dict): diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index e7e9879..f1862fe 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -1,452 +1,458 @@ -from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder -from aiogram.types import InlineKeyboardButton, KeyboardButton - - -def main_keyboard(): - """ - Главное меню - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="📜 Профиль", - callback_data="profile", - ) - ) - # ------ - builder.row( - InlineKeyboardButton( - text="❔ FAQ ❔", - callback_data="faq", - ) - ) - builder.row( - InlineKeyboardButton( - text="ℹ️ О нас", - url="https://www.youtube.com/watch?v=Zirn-CKck-c", - ) - ) - return builder.as_markup() - - -def account_keyboard(): - """ - Клавиатура профиля: - пополнить баланс, история транзакций, назад в главное меню. - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🪙 Пополнить баланс", - callback_data="balance", - ) - ) - builder.row( - InlineKeyboardButton( - text="🧾 История транзакций", - callback_data="tranhist", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="base", - ) - ) - return builder.as_markup() - - -def balance_keyboard(): - """ - Экран баланса - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🪙 Пополнить баланс", - callback_data="popup", - ) - ) - builder.row( - InlineKeyboardButton( - text="🧾 История транзакций", - callback_data="tranhist", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", - ) - ) - return builder.as_markup() - - -def popup_keyboard(): - """ - Суммы пополнения: 200, 300, 600, 1000 ₽. - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="200 ₽", callback_data="popup:200"), - ) - builder.row( - InlineKeyboardButton(text="300 ₽", callback_data="popup:300"), - ) - builder.row( - InlineKeyboardButton(text="600 ₽", callback_data="popup:600"), - ) - builder.row( - InlineKeyboardButton(text="1000 ₽", callback_data="popup:1000"), - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", # назад в профиль - ) - ) - return builder.as_markup() - - -def payment_methods_keyboard(amount: int): - """ - Способы оплаты для выбранной суммы. - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="⭐ Telegram Stars", - callback_data=f"method_stars_{amount}", - ) - ) - builder.row( - InlineKeyboardButton( - text="💵 YooKassa", - callback_data=f"method_ykassa_{amount}", - ) - ) - builder.row( - InlineKeyboardButton( - text="🪙 CryptoBot", - callback_data=f"method_crypto_{amount}", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="popup", - ) - ) - return builder.as_markup() - - -def ticket_list_keyboard(tickets): - builder = InlineKeyboardBuilder() - for ticket in tickets: - builder.row( - InlineKeyboardButton( - text=f"Тикет: {ticket['subject']}", - callback_data=f"ticket_{ticket['id']}", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="main_sup", - ) - ) - return builder.as_markup() - - -def sup_keyboard(): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="📝 Создать запрос", - callback_data="make_ticket", - ) - ) - builder.row( - InlineKeyboardButton( - text="📂 Мои запросы", - callback_data="my_tickets", - ) - ) - return builder.as_markup() - - -def ticket_keyboard(): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Отмена", - callback_data="cancel", - ) - ) - return builder.as_markup() - - -def buy_keyboard(): - """ - Меню выбора тарифа. - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🐣 Lark Basic", - callback_data="subs", - ) - ) - builder.row( - InlineKeyboardButton( - text="🦅 Lark Pro", - callback_data="subs_pro", - ) - ) - builder.row( - InlineKeyboardButton( - text="👨‍👩‍👧 Lark Family", - callback_data="subs_family", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", - ) - ) - return builder.as_markup() - - -def tarif_Lark_keyboard(): - """ - Тариф Lark Basic (Standart) - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🐣 Lark 1 месяц", - callback_data="Lark:Standart:1", - ) - ) - builder.row( - InlineKeyboardButton( - text="🐣 Lark 6 месяцев", - callback_data="Lark:Standart:6", - ) - ) - builder.row( - InlineKeyboardButton( - text="🐣 Lark 12 месяцев", - callback_data="Lark:Standart:12", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def tarif_Lark_pro_keyboard(): - """ - Тариф Lark Pro - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🦅 Lark Pro 1 месяц", - callback_data="Lark:Pro:1", - ) - ) - builder.row( - InlineKeyboardButton( - text="🦅 Lark Pro 6 месяцев", - callback_data="Lark:Pro:6", - ) - ) - builder.row( - InlineKeyboardButton( - text="🦅 Lark Pro 12 месяцев", - callback_data="Lark:Pro:12", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def tarif_Lark_family_keyboard(): - """ - Тариф Lark Family. - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="👨‍👩‍👧 Lark Family 1 месяц", - callback_data="Lark:Family:1", - ) - ) - builder.row( - InlineKeyboardButton( - text="👨‍👩‍👧 Lark Family 6 месяцев", - callback_data="Lark:Family:6", - ) - ) - builder.row( - InlineKeyboardButton( - text="👨‍👩‍👧 Lark Family 12 месяцев", - callback_data="Lark:Family:12", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def subscriptions_card_keyboard(sub_id: str, index: int, total: int): - """ - Карточка подписки: - навигация, конфиг, продление, новая, назад. - """ - builder = InlineKeyboardBuilder() - - nav = [] - if index > 0: - nav.append( - InlineKeyboardButton( - text="⬅️", - callback_data=f"sub_prev:{index-1}", - ) - ) - if index < total - 1: - nav.append( - InlineKeyboardButton( - text="➡️", - callback_data=f"sub_next:{index+1}", - ) - ) - if nav: - builder.row(*nav) - - builder.row( - InlineKeyboardButton( - text="🔑 Конфиг", - callback_data=f"sub_cfg:{sub_id}", - ), - InlineKeyboardButton( - text="🔁 Продлить", - callback_data=f"sub_renew:{sub_id}", - ), - ) - builder.row( - InlineKeyboardButton( - text="➕ Новая", - callback_data="buy_subscription", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", - ) - ) - return builder.as_markup() - - -def guide_keyboard(): - """ - Руководство по подключению - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="📱 iOS / Android", - callback_data="mob", - ) - ) - builder.row( - InlineKeyboardButton( - text="💻 Windows / macOS", - callback_data="pc", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", - ) - ) - return builder.as_markup() - - -def faq_keyboard(): - """ - FAQ - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="base", - ) - ) - return builder.as_markup() - - -def tranhist_keyboard(): - """ - История транзакций - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", - ) - ) - return builder.as_markup() - - -def tarif_confirm_keyboard(name: str, amount: int, classif: str): - """ - Подтверждение покупки тарифа - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="✅ Подтвердить", - callback_data=f"confirm:{name}_{classif}_{amount}", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Отменить", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def confirm_popup_keyboard(): - """ - Подтверждение пополнения. - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="✅ Готово, вернуться в профиль", - callback_data="profile", - ) - ) - return builder.as_markup() +from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder +from aiogram.types import InlineKeyboardButton, KeyboardButton + + +def main_keyboard(): + """ + Главное меню + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="📜 Профиль", + callback_data="profile", + ) + ) + # ------ + builder.row( + InlineKeyboardButton( + text="❔ FAQ ❔", + callback_data="faq", + ) + ) + builder.row( + InlineKeyboardButton( + text="ℹ️ О нас", + url="https://www.youtube.com/watch?v=Zirn-CKck-c", + ) + ) + return builder.as_markup() + + +def account_keyboard(): + """ + Клавиатура профиля: + пополнить баланс, история транзакций, назад в главное меню. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🪙 Пополнить баланс", + callback_data="balance", + ) + ) + builder.row( + InlineKeyboardButton( + text="🧾 История транзакций", + callback_data="tranhist", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="base", + ) + ) + return builder.as_markup() + + +def balance_keyboard(): + """ + Экран баланса + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🪙 Пополнить баланс", + callback_data="popup", + ) + ) + builder.row( + InlineKeyboardButton( + text="🧾 История транзакций", + callback_data="tranhist", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="profile", + ) + ) + return builder.as_markup() + + +def popup_keyboard(): + """ + Суммы пополнения: 200, 300, 600, 1000 ₽. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="200 ₽", callback_data="popup:200"), + ) + builder.row( + InlineKeyboardButton(text="300 ₽", callback_data="popup:300"), + ) + builder.row( + InlineKeyboardButton(text="600 ₽", callback_data="popup:600"), + ) + builder.row( + InlineKeyboardButton(text="1000 ₽", callback_data="popup:1000"), + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="profile", # назад в профиль + ) + ) + return builder.as_markup() + + +def payment_methods_keyboard(amount: int): + """ + Способы оплаты для выбранной суммы. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="⭐ Telegram Stars", + callback_data=f"method_stars_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="💵 YooKassa", + callback_data=f"method_ykassa_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🪙 CryptoBot", + callback_data=f"method_crypto_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🏦 СБП (Система быстрых платежей)", + callback_data=f"method_sbp_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="popup", + ) + ) + return builder.as_markup() + + +def ticket_list_keyboard(tickets): + builder = InlineKeyboardBuilder() + for ticket in tickets: + builder.row( + InlineKeyboardButton( + text=f"Тикет: {ticket['subject']}", + callback_data=f"ticket_{ticket['id']}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="main_sup", + ) + ) + return builder.as_markup() + + +def sup_keyboard(): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="📝 Создать запрос", + callback_data="make_ticket", + ) + ) + builder.row( + InlineKeyboardButton( + text="📂 Мои запросы", + callback_data="my_tickets", + ) + ) + return builder.as_markup() + + +def ticket_keyboard(): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🔙 Отмена", + callback_data="cancel", + ) + ) + return builder.as_markup() + + +def buy_keyboard(): + """ + Меню выбора тарифа. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🐣 Lark Basic", + callback_data="subs", + ) + ) + builder.row( + InlineKeyboardButton( + text="🦅 Lark Pro", + callback_data="subs_pro", + ) + ) + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family", + callback_data="subs_family", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="profile", + ) + ) + return builder.as_markup() + + +def tarif_Lark_keyboard(): + """ + Тариф Lark Basic (Standart) + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🐣 Lark 1 месяц", + callback_data="Lark:Standart:1", + ) + ) + builder.row( + InlineKeyboardButton( + text="🐣 Lark 6 месяцев", + callback_data="Lark:Standart:6", + ) + ) + builder.row( + InlineKeyboardButton( + text="🐣 Lark 12 месяцев", + callback_data="Lark:Standart:12", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + +def tarif_Lark_pro_keyboard(): + """ + Тариф Lark Pro + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🦅 Lark Pro 1 месяц", + callback_data="Lark:Pro:1", + ) + ) + builder.row( + InlineKeyboardButton( + text="🦅 Lark Pro 6 месяцев", + callback_data="Lark:Pro:6", + ) + ) + builder.row( + InlineKeyboardButton( + text="🦅 Lark Pro 12 месяцев", + callback_data="Lark:Pro:12", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + +def tarif_Lark_family_keyboard(): + """ + Тариф Lark Family. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 1 месяц", + callback_data="Lark:Family:1", + ) + ) + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 6 месяцев", + callback_data="Lark:Family:6", + ) + ) + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 12 месяцев", + callback_data="Lark:Family:12", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + +def subscriptions_card_keyboard(sub_id: str, index: int, total: int): + """ + Карточка подписки: + навигация, конфиг, продление, новая, назад. + """ + builder = InlineKeyboardBuilder() + + nav = [] + if index > 0: + nav.append( + InlineKeyboardButton( + text="⬅️", + callback_data=f"sub_prev:{index-1}", + ) + ) + if index < total - 1: + nav.append( + InlineKeyboardButton( + text="➡️", + callback_data=f"sub_next:{index+1}", + ) + ) + if nav: + builder.row(*nav) + + builder.row( + InlineKeyboardButton( + text="🔑 Конфиг", + callback_data=f"sub_cfg:{sub_id}", + ), + InlineKeyboardButton( + text="🔁 Продлить", + callback_data=f"sub_renew:{sub_id}", + ), + ) + builder.row( + InlineKeyboardButton( + text="➕ Новая", + callback_data="buy_subscription", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="profile", + ) + ) + return builder.as_markup() + + +def guide_keyboard(): + """ + Руководство по подключению + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="📱 iOS / Android", + callback_data="mob", + ) + ) + builder.row( + InlineKeyboardButton( + text="💻 Windows / macOS", + callback_data="pc", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="profile", + ) + ) + return builder.as_markup() + + +def faq_keyboard(): + """ + FAQ + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="base", + ) + ) + return builder.as_markup() + + +def tranhist_keyboard(): + """ + История транзакций + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="profile", + ) + ) + return builder.as_markup() + + +def tarif_confirm_keyboard(name: str, amount: int, classif: str): + """ + Подтверждение покупки тарифа + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="✅ Подтвердить", + callback_data=f"confirm:{name}_{classif}_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Отменить", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + +def confirm_popup_keyboard(): + """ + Подтверждение пополнения. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="✅ Готово, вернуться в профиль", + callback_data="profile", + ) + ) + return builder.as_markup()