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()