2 Commits

Author SHA1 Message Date
unknown
1db24ace53 fixes 2025-12-06 19:41:13 +03:00
d3e47a662f Временные изменения 2025-12-05 18:45:25 +03:00
6 changed files with 1255 additions and 957 deletions

View File

@@ -1,7 +1,12 @@
# Профиль. последнее изменение 24.11.2025 # Профиль. последнее изменениеы
from aiogram import Router, types from aiogram import Router, types, F
from aiogram.types import CallbackQuery from aiogram.types import (
CallbackQuery,
LabeledPrice,
Message,
PreCheckoutQuery,
)
import logging import logging
from datetime import datetime from datetime import datetime
from aiogram.enums.parse_mode import ParseMode from aiogram.enums.parse_mode import ParseMode
@@ -74,7 +79,6 @@ async def profile_callback_handler(callback: CallbackQuery):
await callback.answer() await callback.answer()
return return
# Последняя подписка пользователя
sub_data = await call_api( sub_data = await call_api(
"GET", f"/subscription/{user_data['telegram_id']}/last" "GET", f"/subscription/{user_data['telegram_id']}/last"
) )
@@ -88,7 +92,7 @@ async def profile_callback_handler(callback: CallbackQuery):
sub_status = "⚫ Нет активных" sub_status = "⚫ Нет активных"
if sub_data: if sub_data:
expiry_date = sub_data.get("expiry_date") expiry_date = sub_data.get("end_date")
if expiry_date: if expiry_date:
try: try:
is_expired = datetime.fromisoformat( is_expired = datetime.fromisoformat(
@@ -233,15 +237,50 @@ async def popup_confirm_callback_handler(callback: CallbackQuery):
await callback.answer() await callback.answer()
# ===== Telegram Stars =====
@router.callback_query(lambda callback: callback.data.startswith("method_stars_")) @router.callback_query(lambda callback: callback.data.startswith("method_stars_"))
async def method_stars_handler(callback: CallbackQuery): async def method_stars_handler(callback: CallbackQuery):
""" """
Заглушка: оплата через Telegram Stars. Оплата через Telegram Stars.
Формируем invoice прямо из бота, без отдельного биллинга.
""" """
amount = callback.data.split("_")[-1] try:
await callback.message.edit_text( amount_str = callback.data.split("_")[-1]
f"⭐ Оплата через Telegram Stars на {amount} ₽ пока в разработке.\n\n" 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() await callback.answer()
@@ -253,14 +292,11 @@ async def method_ykassa_handler(callback: CallbackQuery):
""" """
amount = callback.data.split("_")[-1] amount = callback.data.split("_")[-1]
# Сразу отвечаем на callback, чтобы Telegram не считал запрос "старым"
try: try:
await callback.answer() await callback.answer()
except Exception: except Exception:
# Если по какой-то причине уже отвечали — просто игнорируем
pass pass
# Формируем URL с query parameters вместо JSON body
endpoint = ( endpoint = (
f"/billing/payments/init?" f"/billing/payments/init?"
f"user_id={callback.from_user.id}&amount={float(amount)}&provider=yookassa" f"user_id={callback.from_user.id}&amount={float(amount)}&provider=yookassa"
@@ -268,17 +304,14 @@ async def method_ykassa_handler(callback: CallbackQuery):
logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}")
# Отправляем POST запрос с пустым телом (параметры в URL)
result = await call_api("POST", endpoint, None, "http://billing:8000") result = await call_api("POST", endpoint, None, "http://billing:8000")
# Биллинг вообще не ответил/упал
if result == "ERROR" or not isinstance(result, dict): if result == "ERROR" or not isinstance(result, dict):
await callback.message.edit_text( await callback.message.edit_text(
"❌ Произошла ошибка при создании платежа. Попробуйте позже." "❌ Произошла ошибка при создании платежа. Попробуйте позже."
) )
return return
# Биллинг вернул ошибку (success = False)
if not result.get("success", False): if not result.get("success", False):
error_msg = ( error_msg = (
result.get("error") result.get("error")
@@ -319,7 +352,6 @@ async def method_crypto_handler(callback: CallbackQuery):
""" """
amount = callback.data.split("_")[-1] amount = callback.data.split("_")[-1]
# Сразу отвечаем на callback, чтобы избежать таймаута
try: try:
await callback.answer() await callback.answer()
except Exception: except Exception:
@@ -373,6 +405,39 @@ async def method_crypto_handler(callback: CallbackQuery):
await callback.answer() 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") @router.callback_query(lambda callback: callback.data == "guide")
async def guide_callback_handler(callback: CallbackQuery): async def guide_callback_handler(callback: CallbackQuery):
""" """
@@ -383,3 +448,87 @@ async def guide_callback_handler(callback: CallbackQuery):
reply_markup=guide_keyboard(), reply_markup=guide_keyboard(),
) )
await callback.answer() 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(
"Оплата прошла, но произошла ошибка при обработке. "
"Если баланс не изменился — напиши, пожалуйста, в поддержку."
)

View File

@@ -2,12 +2,60 @@ from aiogram import Router, types
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.enums.parse_mode import ParseMode from aiogram.enums.parse_mode import ParseMode
import logging import logging
import aiohttp
from instences.config import BASE_URL_FASTAPI
router = Router() router = Router()
logger = logging.getLogger(__name__) 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: async def _build_referral_text(bot, user_id: int) -> str:
"""
Текст реферальной программы + количество приглашённых.
"""
me = await bot.get_me() me = await bot.get_me()
bot_username = me.username or "LarkVPN_bot" bot_username = me.username or "LarkVPN_bot"
@@ -17,10 +65,25 @@ async def _build_referral_text(bot, user_id: int) -> str:
"👥 <b>Реферальная программа</b>\n\n" "👥 <b>Реферальная программа</b>\n\n"
"Зови друзей в Lark VPN и получай бонусы на баланс.\n\n" "Зови друзей в Lark VPN и получай бонусы на баланс.\n\n"
f"🔗 Твоя ссылка:\n<code>{link}</code>\n\n" f"🔗 Твоя ссылка:\n<code>{link}</code>\n\n"
"👤 Приглашено: —\n"
"💰 Начислено бонусов: — ₽\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 return text
@@ -29,19 +92,19 @@ async def referrals_command(message: types.Message):
""" """
Команда /referrals — показывает текст реферальной программы. Команда /referrals — показывает текст реферальной программы.
""" """
logger.info(f"Получена команда /referrals от {message.from_user.id}") logger.info(f"[referrals] Команда /referrals от {message.from_user.id}")
try: try:
text = await _build_referral_text(message.bot, message.from_user.id) text = await _build_referral_text(message.bot, message.from_user.id)
await message.answer(text, parse_mode=ParseMode.HTML) await message.answer(text, parse_mode=ParseMode.HTML)
except Exception as e: except Exception as e:
logger.exception(f"Ошибка в обработчике /referrals: {e}") logger.exception(f"[referrals] Ошибка в обработчике /referrals: {e}")
await message.answer("Произошла ошибка. Попробуй позже.") await message.answer("Произошла ошибка. Попробуй позже.")
@router.callback_query(lambda callback: callback.data == "referral") @router.callback_query(lambda callback: callback.data == "referral")
async def referrals_callback(callback: types.CallbackQuery): async def referrals_callback(callback: types.CallbackQuery):
""" """
Кнопка «Реферальная программа» в главном меню. Кнопка «Реферальная программа» (если где-то есть).
""" """
try: try:
text = await _build_referral_text( text = await _build_referral_text(
@@ -53,7 +116,8 @@ async def referrals_callback(callback: types.CallbackQuery):
parse_mode=ParseMode.HTML, parse_mode=ParseMode.HTML,
) )
except Exception as e: except Exception as e:
logger.exception(f"Ошибка в обработчике callback 'referral': {e}") logger.exception(
f"[referrals] Ошибка в обработчике callback 'referral': {e}")
await callback.message.answer("Произошла ошибка. Попробуй позже.") await callback.message.answer("Произошла ошибка. Попробуй позже.")
finally: finally:
await callback.answer() await callback.answer()

View File

@@ -1,92 +1,174 @@
from aiogram import Router, types from aiogram import Router, types
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery 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 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 from .referrals import _build_referral_text
router = Router() router = Router()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def call_api(method, endpoint, data=None): async def call_api(method: str, endpoint: str, data=None):
""" """
Выполняет HTTP-запрос к FastAPI. Выполняет HTTP-запрос к FastAPI.
:param method: HTTP метод (GET, POST, и т.д.) Возвращает:
:param endpoint: конечная точка API - dict при 200/201
:param data: тело запроса (если необходимо) - None при 404
:return: JSON-ответ или "ERROR" при неуспехе - "ERROR" при остальных ошибках
""" """
url = f"{BASE_URL_FASTAPI}{endpoint}" url = f"{BASE_URL_FASTAPI}{endpoint}"
logger.info(f"Инициализация запроса: {method} {url} с данными {data}") logger.info(f"[start] Запрос: {method} {url} с данными {data}")
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.request(method, url, json=data) as response: async with session.request(method, url, json=data) as response:
logger.info( logger.info(
f"Получен ответ от {url}: статус {response.status}" f"[start] Ответ от {url}: статус {response.status}"
) )
if response.status in {200, 201}: if response.status in {200, 201}:
result = await response.json() result = await response.json()
logger.debug(f"Ответ JSON: {result}") logger.debug(f"[start] Ответ JSON: {result}")
return result return result
if response.status == 404: if response.status == 404:
logger.debug(f"Код {response.status}, возвращаю ничего") logger.debug("[start] Получен 404, возвращаю None")
return None return None
logger.error( logger.error(
f"Ошибка в запросе: статус {response.status}, причина {response.reason}" f"[start] Ошибка в запросе: статус {response.status}, "
f"причина {response.reason}"
) )
return "ERROR" return "ERROR"
except Exception as e: except Exception as e:
logger.exception(f"Исключение при выполнении запроса к {url}: {e}") logger.exception(f"[start] Исключение при запросе к {url}: {e}")
return "ERROR" return "ERROR"
def _welcome_text(username: str | None) -> str: def _welcome_text(username: str | None) -> str:
""" """
Текст приветствия в /start и в главном меню. Текст приветствия в /start и в главном меню.
Имя пока не используем — оставляем сигнатуру на будущее.
""" """
return "🥚 Lark Security\n\nВыберите действие из меню ниже." return "🥚 Lark Security\n\nВыберите действие из меню ниже."
def _parse_referrer_id(message: Message) -> int | None:
"""
Достаём ref_<telegram_id> из /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")) @router.message(Command("start"))
async def start_command(message: Message): async def start_command(message: Message):
""" """
Обработчик команды /start. /start c обработкой реферального параметра.
Визуал и текст — обновлены, логика работы с API не тронута.
""" """
user_id = message.from_user.id
username = message.from_user.username
referrer_id = _parse_referrer_id(message)
logger.info( logger.info(
f"Получена команда /start от пользователя: " f"[start] Команда /start от {user_id} (@{username}), "
f"{message.from_user.id} ({message.from_user.username})" f"text={message.text!r}, referrer_id={referrer_id}"
) )
try: try:
user_data = await call_api("GET", f"/user/{message.from_user.id}") # 1. Проверяем, есть ли пользователь в БД
if not user_data: existing = await call_api("GET", f"/user/{user_id}")
logger.debug( user_exists = existing not in (None, "ERROR")
"Пользователь не найден в базе, создаем новую запись."
) # 2. Если пользователя нет — создаём
await call_api( if not user_exists:
logger.debug(f"[start] Пользователь {user_id} не найден, создаю.")
create_result = await call_api(
"POST", "POST",
"/user/create", "/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( await message.answer(
_welcome_text(message.from_user.username), "Нельзя переходить по своей же реферальной ссылке."
)
# 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(username),
reply_markup=main_keyboard(), reply_markup=main_keyboard(),
) )
logger.info("Приветственное сообщение отправлено.") logger.info(f"[start] Главное меню отправлено пользователю {user_id}.")
except Exception as e: except Exception as e:
logger.exception( logger.exception(
f"Ошибка при обработке команды /start для пользователя " f"[start] Ошибка при обработке /start для пользователя {user_id}: {e}"
f"{message.from_user.id}: {e}"
) )
await message.answer("Произошла ошибка. Попробуйте позже.") await message.answer("Произошла ошибка. Попробуйте позже.")
@@ -98,16 +180,16 @@ async def referrals_menu_command(message: Message):
Показывает текст реферальной программы. Показывает текст реферальной программы.
""" """
logger.info( logger.info(
f"Получена команда /referrals от пользователя: " f"[start] Команда /referrals от {message.from_user.id} "
f"{message.from_user.id} ({message.from_user.username})" f"(@{message.from_user.username})"
) )
try: try:
text = await _build_referral_text(message.bot, message.from_user.id) text = await _build_referral_text(message.bot, message.from_user.id)
await message.answer(text, parse_mode=ParseMode.HTML) await message.answer(text, parse_mode=ParseMode.HTML)
logger.info("Реферальная программа отправлена пользователю.") logger.info("[start] Реферальная программа отправлена пользователю.")
except Exception as e: except Exception as e:
logger.exception( logger.exception(
f"Ошибка при обработке команды /referrals для пользователя " f"[start] Ошибка при обработке /referrals для пользователя "
f"{message.from_user.id}: {e}" f"{message.from_user.id}: {e}"
) )
await message.answer("Произошла ошибка. Попробуйте позже.") await message.answer("Произошла ошибка. Попробуйте позже.")
@@ -116,24 +198,27 @@ async def referrals_menu_command(message: Message):
@router.callback_query(lambda callback: callback.data == "base") @router.callback_query(lambda callback: callback.data == "base")
async def start_callback_handler(callback: CallbackQuery): async def start_callback_handler(callback: CallbackQuery):
""" """
Обработчик callback_query с data="base". Callback с data="base" — возврат в главное меню.
Возвращает пользователя в главное меню.
""" """
try: try:
user_data = await call_api("GET", f"/user/{callback.from_user.id}") user_id = callback.from_user.id
if not user_data: 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( await call_api(
"POST", "POST",
"/user/create", "/user/create",
{"telegram_id": callback.from_user.id}, {"telegram_id": user_id},
) )
await callback.message.edit_text( await callback.message.edit_text(
_welcome_text(callback.from_user.username), _welcome_text(username),
reply_markup=main_keyboard(), reply_markup=main_keyboard(),
) )
except Exception as e: except Exception as e:
logger.exception(f"Ошибка при обработке callback с data='base': {e}") logger.exception(f"[start] Ошибка при обработке callback 'base': {e}")
await callback.message.answer("Произошла ошибка. Попробуйте позже.") await callback.message.answer("Произошла ошибка. Попробуйте позже.")
finally: finally:
await callback.answer() await callback.answer()

View File

@@ -319,7 +319,7 @@ async def cb_sub_renew(callback: types.CallbackQuery):
result = await call_api( result = await call_api(
"POST", "POST",
"/subscription/buy", "/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) # Ошибки backend (detail)
@@ -472,7 +472,7 @@ async def confirm_callback_handler(callback: types.CallbackQuery):
result = await call_api( result = await call_api(
"POST", "POST",
"/subscription/buy", "/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): if result == "ERROR" or not isinstance(result, dict):