diff --git a/handlers/profile.py b/handlers/profile.py
index c1ef4bb..b060b4f 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -93,11 +93,12 @@ async def profile_callback_handler(callback: CallbackQuery):
sub_status = "⚫ Нет активных"
if sub_data:
- expiry_date = sub_data.get("expiry_date")
- if expiry_date:
+ end_date_str = sub_data.get("end_date") # Исправил
+ if end_date_str:
try:
- is_expired = datetime.fromisoformat(
- expiry_date) < datetime.now()
+ is_expired = (
+ datetime.fromisoformat(end_date_str) < datetime.now()
+ )
except ValueError:
is_expired = True
else:
diff --git a/handlers/referrals.py b/handlers/referrals.py
index 0a470aa..85e2ff6 100644
--- a/handlers/referrals.py
+++ b/handlers/referrals.py
@@ -2,126 +2,40 @@ 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
+from .start import call_api # используем уже готовый HTTP-хелпер
router = Router()
logger = logging.getLogger(__name__)
-async def call_api(method, endpoint, data=None, base_url=BASE_URL_FASTAPI):
- """
- Универсальный HTTP-запрос к FastAPI для рефералок.
-
- Ожидаем от бекенда:
- GET /user/{telegram_id}/referrals -> {
- "invited_count": int,
- "bonus_total": float
- }
- """
- 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(
- f"[referrals] Код {response.status}, возвращаю None"
- )
- return None
- logger.error(
- f"[referrals] Ошибка в запросе: статус {response.status}, причина {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}"
- # Базовый текст: без фейковых тире и нулевых рублей
+ invited_count = "—"
+ try:
+ data = await call_api("GET", f"/user/{user_id}/referrals")
+ if isinstance(data, dict):
+ invited_count = str(data.get("invited_count", 0))
+ except Exception as e:
+ logger.exception(
+ f"Ошибка при получении количества рефералов для {user_id}: {e}"
+ )
+
text = (
"👥 Реферальная программа\n\n"
"Зови друзей в Lark VPN и получай бонусы на баланс.\n\n"
f"🔗 Твоя ссылка:\n{link}\n\n"
+ f"👤 Приглашено: {invited_count}\n\n"
)
-
- invited_line = ""
- bonus_line = ""
-
- stats = await call_api("GET", f"/user/{user_id}/referrals")
- if isinstance(stats, dict):
- invited = stats.get("invited_count")
- bonus = stats.get("bonus_total")
-
- if invited is not None:
- try:
- invited_int = int(invited)
- except (TypeError, ValueError):
- invited_int = None
-
- if invited_int is not None:
- invited_line = f"👤 Приглашено: {invited_int}\n"
-
- if bonus is not None:
- try:
- bonus_val = float(bonus)
- except (TypeError, ValueError):
- bonus_val = None
-
- # Строку с бонусами показываем только если реально что-то начислено
- if bonus_val is not None and bonus_val > 0:
- bonus_line = f"💰 Начислено бонусов: {bonus_val:.2f} ₽\n"
- else:
- logger.warning(
- f"[referrals] Не удалось получить статистику для user_id={user_id}: {stats}"
- )
-
- if invited_line:
- text += invited_line
- if bonus_line:
- text += bonus_line
-
- if invited_line or bonus_line:
- text += "\n"
-
- text += (
- "Бонусы падают автоматически, когда приглашённые пополняют баланс."
- )
-
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)
@@ -133,9 +47,6 @@ async def referrals_command(message: types.Message):
@router.callback_query(lambda callback: callback.data == "referral")
async def referrals_callback(callback: types.CallbackQuery):
- """
- Кнопка «Реферальная программа» (если где-то есть).
- """
try:
text = await _build_referral_text(
callback.message.bot,
diff --git a/handlers/start.py b/handlers/start.py
index 9570b62..3debb69 100644
--- a/handlers/start.py
+++ b/handlers/start.py
@@ -1,62 +1,52 @@
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 aiogram.types import Message, CallbackQuery
+
+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):
- """
- Выполняет HTTP-запрос к FastAPI.
- :param method: HTTP метод (GET, POST, и т.д.)
- :param endpoint: конечная точка API
- :param data: тело запроса (если необходимо)
- :return: JSON-ответ или "ERROR" при неуспехе
- """
+async def call_api(method: str, endpoint: str, data: dict | None = None):
+ """Мини-обёртка для запросов к backend API."""
url = f"{BASE_URL_FASTAPI}{endpoint}"
- logger.info(f"Инициализация запроса: {method} {url} с данными {data}")
+ logger.info(f"API {method} {url} data={data}")
try:
async with aiohttp.ClientSession() as session:
- async with session.request(method, url, json=data) 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
+ async with session.request(method, url, json=data) as resp:
+ logger.info(f"API response {resp.status} {resp.reason}")
+ if resp.status in {200, 201}:
+ return await resp.json()
+ if resp.status in {400, 404}:
+ try:
+ return await resp.json()
+ except Exception:
+ return None
logger.error(
- f"Ошибка в запросе: статус {response.status}, причина {response.reason}"
+ f"Unexpected status {resp.status}: {await resp.text()}"
)
return "ERROR"
except Exception as e:
- logger.exception(f"Исключение при выполнении запроса к {url}: {e}")
+ logger.exception(f"API exception {url}: {e}")
return "ERROR"
def _welcome_text(username: str | None) -> str:
- """
- Текст приветствия в /start и в главном меню.
- Имя пока не используем — оставляем сигнатуру на будущее.
- """
+ """Текст приветствия в /start и в главном меню."""
return "🥚 Lark Security\n\nВыберите действие из меню ниже."
def _parse_referrer_id(message: Message) -> int | None:
- """
- Достаём ref_ из /start.
+ """Достаём ref_ из /start.
+
Примеры:
/start
/start ref_123456789
@@ -82,86 +72,117 @@ def _parse_referrer_id(message: Message) -> int | None:
@router.message(Command("start"))
async def start_command(message: Message):
- """
- Обработчик команды /start.
- Добавлена обработка реферального payload'а ref_.
- """
- logger.info(
- f"Получена команда /start от пользователя: "
- f"{message.from_user.id} ({message.from_user.username}) | text='{message.text}'"
- )
+ """/start с обработкой реферального параметра.
+ Логика:
+ - проверяем, есть ли пользователь в БД по telegram_id;
+ - если нет — создаём через /user/create;
+ - если есть корректный ref_ и пользователь новый — вызываем
+ /user/{referrer_id}/add_referral с invited_id = telegram_id.
+ """
user_id = message.from_user.id
+ username = message.from_user.username
referrer_id = _parse_referrer_id(message)
+ logger.info(
+ f"[start] Команда /start от {user_id} (@{username}), "
+ f"text={message.text!r}, referrer_id={referrer_id}"
+ )
+
try:
- # 1. Проверяем/создаём пользователя в базе
- user_data = await call_api("GET", f"/user/{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": user_id},
)
-
- # 2. Если есть реферер и он не сам пользователь — отправляем инфу в бекенд
- if referrer_id and referrer_id != user_id:
- payload = {
- "referrer_id": referrer_id,
- "telegram_id": user_id,
- }
- logger.info(
- f"Отправка данных о реферале в бекенд: {payload}"
- )
- result = await call_api(
- "POST",
- "/user/referrals/track",
- payload,
- )
- if result == "ERROR":
+ if create_result == "ERROR":
logger.error(
- f"Не удалось зафиксировать реферала в бекенде: {payload}"
+ f"[start] Не удалось создать пользователя {user_id} в БД"
)
- elif referrer_id == user_id:
- logger.info(
- "Обнаружена попытка самореферала, запрос в бекенд не отправляем."
- )
- # 3. Приветственное сообщение и главное меню
- 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 = {
+ "invited_id": user_id,
+ }
+ logger.info(
+ f"[start] Фиксирую реферала в бекенде: "
+ f"referrer_id={referrer_id}, payload={payload}"
+ )
+ result = await call_api(
+ "POST",
+ f"/user/{referrer_id}/add_referral",
+ payload,
+ )
+ if result == "ERROR":
+ logger.error(
+ f"[start] Ошибка при фиксации реферала через "
+ f"/user/{referrer_id}/add_referral: referrer={referrer_id}, "
+ f"invited={user_id}"
+ )
+ 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"{user_id}: {e}"
+ f"[start] Ошибка при обработке /start для пользователя {user_id}: {e}"
)
await message.answer("Произошла ошибка. Попробуйте позже.")
@router.message(Command("referrals"))
async def referrals_menu_command(message: Message):
- """
- Команда /referrals из бокового меню Telegram.
+ """Команда /referrals из бокового меню Telegram.
+
Показывает текст реферальной программы.
"""
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("Произошла ошибка. Попробуйте позже.")
@@ -169,25 +190,26 @@ 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 da28b62..2140ad9 100644
--- a/handlers/subscriptions.py
+++ b/handlers/subscriptions.py
@@ -129,15 +129,15 @@ async def _show_subscriptions_view(
index = max(0, min(index, len(subs) - 1))
sub = subs[index]
- plan_title = _plan_human_title(sub.get("plan"))
- expiry_raw = sub.get("expiry_date")
+ plan_title = _plan_human_title(sub.get("plan_name"))
+ end_raw = sub.get("end_date") # Исправил
date_str = "—"
days_left = None
status_emoji = "🟢"
- if expiry_raw:
+ if end_raw:
try:
- dt = datetime.fromisoformat(expiry_raw)
+ dt = datetime.fromisoformat(end_raw)
date_str = dt.strftime("%d.%m.%Y")
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now()
days_left = (dt.date() - now.date()).days
@@ -272,19 +272,22 @@ async def cb_sub_cfg(callback: types.CallbackQuery):
await callback.answer()
return
- if not isinstance(detail, str) or not detail:
+ uri = None
+ if isinstance(detail, str):
+ uri = detail.strip()
+ elif isinstance(detail, list) and detail:
+ uri = str(detail[0]).strip()
+
+ if not uri:
await callback.message.answer(
"Что-то пошло не так при выдаче конфига."
)
await callback.answer()
return
- escaped = escape_markdown_v2(detail)
- text = f"Твой конфиг:\n```{escaped}```"
-
await callback.message.answer(
- text,
- parse_mode=ParseMode.MARKDOWN_V2,
+ f"Твой конфиг:\n{uri}",
+ parse_mode=ParseMode.HTML,
)
await callback.answer()
@@ -310,7 +313,7 @@ async def cb_sub_renew(callback: types.CallbackQuery):
await callback.answer()
return
- plan_id = target.get("plan")
+ plan_id = target.get("plan_name")
if not plan_id:
await callback.message.answer("Не удалось определить тариф для продления.")
await callback.answer()
diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py
index e7e9879..176c67a 100644
--- a/keyboard/keyboards.py
+++ b/keyboard/keyboards.py
@@ -121,7 +121,7 @@ def payment_methods_keyboard(amount: int):
)
builder.row(
InlineKeyboardButton(
- text="💵 YooKassa",
+ text="💵 СБП",
callback_data=f"method_ykassa_{amount}",
)
)
@@ -317,10 +317,11 @@ def tarif_Lark_family_keyboard():
def subscriptions_card_keyboard(sub_id: str, index: int, total: int):
"""
Карточка подписки:
- навигация, конфиг, продление, новая, назад.
+ навигация, конфиг, назад в главное меню.
"""
builder = InlineKeyboardBuilder()
+ # Навигация по подпискам
nav = []
if index > 0:
nav.append(
@@ -339,28 +340,22 @@ def subscriptions_card_keyboard(sub_id: str, index: int, total: int):
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",
+ callback_data="base", # было "profile" изменил
)
)
+
return builder.as_markup()