mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-07 14:33:09 +00:00
Add files via upload
This commit is contained in:
@@ -51,13 +51,17 @@ async def get_transaction_by_id(db: AsyncSession, transaction_id: int) -> Option
|
||||
|
||||
async def get_transaction_by_external_id(
|
||||
db: AsyncSession,
|
||||
external_id: str
|
||||
external_id: str,
|
||||
payment_method: PaymentMethod
|
||||
) -> Optional[Transaction]:
|
||||
"""Получить транзакцию по external_id"""
|
||||
result = await db.execute(
|
||||
select(Transaction)
|
||||
.options(selectinload(Transaction.user))
|
||||
.where(Transaction.external_id == external_id)
|
||||
.where(
|
||||
and_(
|
||||
Transaction.external_id == external_id,
|
||||
Transaction.payment_method == payment_method.value
|
||||
)
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@@ -273,17 +277,16 @@ async def get_revenue_by_period(
|
||||
|
||||
return [{"date": row.date, "amount_kopeks": row.amount} for row in result]
|
||||
|
||||
|
||||
async def find_tribute_transactions_by_payment_id(
|
||||
db: AsyncSession,
|
||||
payment_id: str,
|
||||
user_telegram_id: Optional[int] = None
|
||||
) -> List[Transaction]:
|
||||
"""Найти все Tribute транзакции по payment_id"""
|
||||
|
||||
query = select(Transaction).options(selectinload(Transaction.user))
|
||||
|
||||
conditions = [
|
||||
Transaction.external_id == f"tribute_{payment_id}",
|
||||
Transaction.external_id == f"donation_{payment_id}",
|
||||
Transaction.external_id == payment_id,
|
||||
Transaction.external_id.like(f"%{payment_id}%")
|
||||
@@ -331,14 +334,15 @@ async def create_unique_tribute_transaction(
|
||||
amount_kopeks: int,
|
||||
description: str
|
||||
) -> Transaction:
|
||||
"""Создать уникальную Tribute транзакцию с защитой от дубликатов"""
|
||||
|
||||
external_id = f"tribute_{payment_id}"
|
||||
external_id = f"donation_{payment_id}"
|
||||
|
||||
existing = await get_transaction_by_external_id(db, external_id)
|
||||
existing = await get_transaction_by_external_id(db, external_id, PaymentMethod.TRIBUTE)
|
||||
|
||||
if existing:
|
||||
timestamp = int(datetime.utcnow().timestamp())
|
||||
external_id = f"tribute_{payment_id}_{amount_kopeks}_{timestamp}"
|
||||
external_id = f"donation_{payment_id}_{amount_kopeks}_{timestamp}"
|
||||
|
||||
logger.info(f"Создан уникальный external_id для избежания дубликатов: {external_id}")
|
||||
|
||||
@@ -352,70 +356,3 @@ async def create_unique_tribute_transaction(
|
||||
external_id=external_id,
|
||||
is_completed=True
|
||||
)
|
||||
|
||||
|
||||
class TransactionCRUD:
|
||||
|
||||
async def create_transaction(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
transaction_data: dict
|
||||
) -> Optional[Transaction]:
|
||||
try:
|
||||
transaction = Transaction(
|
||||
user_id=transaction_data['user_id'],
|
||||
type=transaction_data['transaction_type'],
|
||||
amount_kopeks=transaction_data['amount_kopeks'],
|
||||
description=transaction_data['description'],
|
||||
external_id=transaction_data.get('external_id'),
|
||||
payment_method=transaction_data.get('payment_system'),
|
||||
is_completed=transaction_data.get('status') == 'completed'
|
||||
)
|
||||
|
||||
db.add(transaction)
|
||||
await db.commit()
|
||||
await db.refresh(transaction)
|
||||
|
||||
logger.info(f"Создана транзакция: {transaction.id} на {transaction.amount_kopeks} коп.")
|
||||
return transaction
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания транзакции: {e}")
|
||||
await db.rollback()
|
||||
return None
|
||||
|
||||
async def get_transaction_by_external_id(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
external_id: str
|
||||
) -> Optional[Transaction]:
|
||||
return await get_transaction_by_external_id(db, external_id)
|
||||
|
||||
async def update_transaction_status(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
transaction_id: int,
|
||||
status: str
|
||||
) -> bool:
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Transaction).where(Transaction.id == transaction_id)
|
||||
)
|
||||
transaction = result.scalar_one_or_none()
|
||||
|
||||
if not transaction:
|
||||
logger.error(f"Транзакция {transaction_id} не найдена")
|
||||
return False
|
||||
|
||||
transaction.is_completed = (status == 'completed')
|
||||
if transaction.is_completed:
|
||||
transaction.completed_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Статус транзакции {transaction_id} обновлен на {status}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления статуса транзакции: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
@@ -156,25 +156,25 @@ async def add_user_balance(
|
||||
|
||||
async def add_user_balance_by_id(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
telegram_id: int,
|
||||
amount_kopeks: int,
|
||||
description: str = "Пополнение баланса"
|
||||
) -> bool:
|
||||
"""Пополнить баланс пользователя по ID"""
|
||||
try:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
|
||||
user = await get_user_by_telegram_id(db, telegram_id)
|
||||
if not user:
|
||||
user = await get_user_by_telegram_id(db, user_id)
|
||||
|
||||
if not user:
|
||||
logger.error(f"Пользователь с ID {user_id} не найден")
|
||||
logger.error(f"Пользователь с telegram_id {telegram_id} не найден")
|
||||
return False
|
||||
|
||||
return await add_user_balance(db, user, amount_kopeks, description)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка пополнения баланса пользователя {user_id}: {e}")
|
||||
logger.error(f"Ошибка пополнения баланса пользователя {telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка пополнения баланса пользователя {user_id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
async def subtract_user_balance(
|
||||
@@ -356,14 +356,3 @@ async def get_users_statistics(db: AsyncSession) -> dict:
|
||||
"new_week": new_week,
|
||||
"new_month": new_month
|
||||
}
|
||||
|
||||
class UserCRUD:
|
||||
|
||||
async def get_user(self, db: AsyncSession, user_id: int) -> Optional[User]:
|
||||
return await get_user_by_id(db, user_id)
|
||||
|
||||
async def get_user_by_telegram_id(self, db: AsyncSession, telegram_id: int) -> Optional[User]:
|
||||
return await get_user_by_telegram_id(db, telegram_id)
|
||||
|
||||
async def add_balance(self, db: AsyncSession, user_id: int, amount_kopeks: int) -> bool:
|
||||
return await add_user_balance_by_id(db, user_id, amount_kopeks)
|
||||
|
||||
108
app/external/tribute.py
vendored
108
app/external/tribute.py
vendored
@@ -28,16 +28,10 @@ class TributeService:
|
||||
return None
|
||||
|
||||
try:
|
||||
payment_url = f"{self.donate_link}?user_id={user_id}"
|
||||
|
||||
if amount_kopeks > 0:
|
||||
amount_rubles = amount_kopeks / 100
|
||||
payment_url += f"&amount={amount_rubles:.2f}"
|
||||
payment_url = f"{self.donate_link}&user_id={user_id}"
|
||||
|
||||
if description:
|
||||
payment_url += f"&description={description}"
|
||||
|
||||
logger.info(f"Создана ссылка Tribute для пользователя {user_id}: {amount_kopeks} коп.")
|
||||
logger.info(f"Создана ссылка Tribute для пользователя {user_id}")
|
||||
return payment_url
|
||||
|
||||
except Exception as e:
|
||||
@@ -45,28 +39,19 @@ class TributeService:
|
||||
return None
|
||||
|
||||
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
|
||||
"""Проверяет подпись webhook"""
|
||||
|
||||
if not self.webhook_secret:
|
||||
logger.warning("Webhook secret не настроен, пропускаем проверку подписи")
|
||||
logger.warning("Webhook secret не настроен")
|
||||
return True
|
||||
|
||||
try:
|
||||
if signature.startswith('sha256='):
|
||||
signature = signature[7:]
|
||||
|
||||
expected_signature = hmac.new(
|
||||
self.webhook_secret.encode('utf-8'),
|
||||
payload.encode('utf-8'),
|
||||
self.webhook_secret.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
result = hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
if not result:
|
||||
logger.warning(f"Неверная подпись webhook. Получено: {signature[:10]}..., ожидалось: {expected_signature[:10]}...")
|
||||
|
||||
return result
|
||||
return hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки подписи webhook: {e}")
|
||||
@@ -75,104 +60,49 @@ class TributeService:
|
||||
async def process_webhook(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
|
||||
try:
|
||||
logger.debug(f"Обработка Tribute webhook: {json.dumps(payload, ensure_ascii=False, indent=2)}")
|
||||
|
||||
payment_id = None
|
||||
status = None
|
||||
amount_kopeks = 0
|
||||
telegram_user_id = None
|
||||
|
||||
|
||||
payment_id = payload.get("id") or payload.get("payment_id") or payload.get("donation_id")
|
||||
payment_id = payload.get("id") or payload.get("payment_id")
|
||||
status = payload.get("status")
|
||||
amount_kopeks = payload.get("amount", 0)
|
||||
amount_kopeks = payload.get("amount", 0)
|
||||
telegram_user_id = payload.get("telegram_user_id") or payload.get("user_id")
|
||||
|
||||
if not payment_id and "payload" in payload:
|
||||
data = payload["payload"]
|
||||
payment_id = data.get("id") or data.get("payment_id") or data.get("donation_id")
|
||||
payment_id = data.get("id") or data.get("payment_id")
|
||||
status = data.get("status")
|
||||
amount_kopeks = data.get("amount", 0)
|
||||
amount_kopeks = data.get("amount", 0)
|
||||
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
|
||||
|
||||
if not payment_id and "name" in payload:
|
||||
event_name = payload.get("name")
|
||||
data = payload.get("payload", {})
|
||||
|
||||
payment_id = (
|
||||
data.get("donation_request_id") or
|
||||
data.get("donation_id") or
|
||||
data.get("id") or
|
||||
data.get("payment_id")
|
||||
)
|
||||
|
||||
amount_kopeks = data.get("amount", 0)
|
||||
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
|
||||
payment_id = str(data.get("donation_request_id"))
|
||||
amount_kopeks = data.get("amount", 0)
|
||||
telegram_user_id = data.get("telegram_user_id")
|
||||
|
||||
if event_name == "new_donation":
|
||||
status = "paid"
|
||||
elif event_name == "donation_completed":
|
||||
status = "completed"
|
||||
elif event_name == "cancelled_subscription":
|
||||
status = "cancelled"
|
||||
else:
|
||||
status = "unknown"
|
||||
logger.warning(f"Неизвестное событие Tribute: {event_name}")
|
||||
|
||||
if not payment_id and "data" in payload:
|
||||
data = payload["data"]
|
||||
payment_id = data.get("id") or data.get("donation_id")
|
||||
status = data.get("status", "paid")
|
||||
amount_kopeks = data.get("amount", 0)
|
||||
telegram_user_id = data.get("telegram_user_id") or data.get("user_id")
|
||||
|
||||
if isinstance(amount_kopeks, (int, float)):
|
||||
if amount_kopeks > 1000:
|
||||
amount_kopeks = int(amount_kopeks)
|
||||
else:
|
||||
amount_kopeks = int(amount_kopeks * 100)
|
||||
else:
|
||||
amount_kopeks = 0
|
||||
|
||||
logger.info(f"Извлеченные данные Tribute webhook:")
|
||||
logger.info(f" - payment_id: {payment_id}")
|
||||
logger.info(f" - status: {status}")
|
||||
logger.info(f" - amount_kopeks: {amount_kopeks}")
|
||||
logger.info(f" - telegram_user_id: {telegram_user_id}")
|
||||
logger.info(f"Обработка Tribute webhook: payment_id={payment_id}, status={status}, amount_kopeks={amount_kopeks}, user_id={telegram_user_id}")
|
||||
|
||||
if not telegram_user_id:
|
||||
logger.error("Не найден telegram_user_id в webhook данных")
|
||||
logger.error(f"Полные данные webhook: {json.dumps(payload, ensure_ascii=False)}")
|
||||
return None
|
||||
|
||||
if amount_kopeks <= 0:
|
||||
logger.error(f"Неверная сумма платежа: {amount_kopeks}")
|
||||
return None
|
||||
|
||||
if not payment_id:
|
||||
import time
|
||||
payment_id = f"tribute_{telegram_user_id}_{int(time.time())}"
|
||||
logger.info(f"Сгенерирован payment_id: {payment_id}")
|
||||
else:
|
||||
payment_id = str(payment_id)
|
||||
|
||||
if not status:
|
||||
status = "paid"
|
||||
|
||||
result = {
|
||||
return {
|
||||
"event_type": "payment",
|
||||
"payment_id": payment_id,
|
||||
"payment_id": payment_id or f"tribute_{telegram_user_id}_{amount_kopeks}",
|
||||
"user_id": int(telegram_user_id),
|
||||
"amount_kopeks": amount_kopeks,
|
||||
"status": status,
|
||||
"external_id": f"tribute_{payment_id}",
|
||||
"provider": "tribute"
|
||||
"status": status or "paid",
|
||||
"external_id": f"donation_{payment_id}"
|
||||
}
|
||||
|
||||
logger.info(f"✅ Успешно обработан Tribute webhook: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обработки Tribute webhook: {e}", exc_info=True)
|
||||
logger.error(f"Ошибка обработки Tribute webhook: {e}")
|
||||
logger.error(f"Webhook payload: {json.dumps(payload, ensure_ascii=False)}")
|
||||
return None
|
||||
|
||||
251
app/external/webhook_server.py
vendored
251
app/external/webhook_server.py
vendored
@@ -1,247 +1,98 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Optional, List
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram import Bot
|
||||
|
||||
from app.config import settings
|
||||
from app.external.tribute import TributeService
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.external.yookassa_webhook import YooKassaWebhookHandler
|
||||
from app.services.tribute_service import TributeService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebhookServer:
|
||||
|
||||
def __init__(self, bot: Bot, payment_service: PaymentService):
|
||||
def __init__(self, bot: Bot):
|
||||
self.bot = bot
|
||||
self.payment_service = payment_service
|
||||
self.tribute_service = TributeService()
|
||||
self.yookassa_handler = YooKassaWebhookHandler(payment_service)
|
||||
|
||||
self.tribute_app = None
|
||||
self.tribute_runner = None
|
||||
self.tribute_site = None
|
||||
|
||||
self.yookassa_app = None
|
||||
self.yookassa_runner = None
|
||||
self.yookassa_site = None
|
||||
self.app = None
|
||||
self.runner = None
|
||||
self.site = None
|
||||
self.tribute_service = TributeService(bot)
|
||||
|
||||
def _create_logging_middleware(self):
|
||||
@web.middleware
|
||||
async def logging_middleware(request, handler):
|
||||
start_time = request.loop.time()
|
||||
|
||||
try:
|
||||
response = await handler(request)
|
||||
process_time = request.loop.time() - start_time
|
||||
|
||||
logger.info(f"{request.method} {request.path_qs} -> {response.status} ({process_time:.3f}s)")
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
process_time = request.loop.time() - start_time
|
||||
logger.error(f"{request.method} {request.path_qs} -> ERROR ({process_time:.3f}s): {e}")
|
||||
raise
|
||||
async def create_app(self) -> web.Application:
|
||||
|
||||
return logging_middleware
|
||||
self.app = web.Application()
|
||||
|
||||
self.app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
|
||||
self.app.router.add_get('/health', self._health_check)
|
||||
|
||||
logger.info(f"Webhook сервер настроен:")
|
||||
logger.info(f" - Tribute webhook: {settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
logger.info(f" - Health check: /health")
|
||||
|
||||
return self.app
|
||||
|
||||
async def create_tribute_app(self) -> web.Application:
|
||||
self.tribute_app = web.Application()
|
||||
|
||||
self.tribute_app.middlewares.append(self._create_logging_middleware())
|
||||
|
||||
self.tribute_app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
|
||||
self.tribute_app.router.add_get('/health', self._tribute_health_check)
|
||||
|
||||
logger.info(f"Tribute webhook настроен:")
|
||||
logger.info(f" - Webhook path: {settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
logger.info(f" - Port: {settings.TRIBUTE_WEBHOOK_PORT}")
|
||||
|
||||
return self.tribute_app
|
||||
|
||||
async def create_yookassa_app(self) -> web.Application:
|
||||
self.yookassa_app = web.Application()
|
||||
|
||||
self.yookassa_app.middlewares.append(self._create_logging_middleware())
|
||||
|
||||
self.yookassa_app.router.add_post(settings.YOOKASSA_WEBHOOK_PATH, self.yookassa_handler.handle_webhook)
|
||||
self.yookassa_app.router.add_get('/health', self._yookassa_health_check)
|
||||
|
||||
logger.info(f"YooKassa webhook настроен:")
|
||||
logger.info(f" - Webhook path: {settings.YOOKASSA_WEBHOOK_PATH}")
|
||||
logger.info(f" - Port: {settings.YOOKASSA_WEBHOOK_PORT}")
|
||||
|
||||
return self.yookassa_app
|
||||
|
||||
async def start_tribute_server(self):
|
||||
if not settings.TRIBUTE_ENABLED:
|
||||
logger.info("Tribute отключен, сервер не запускается")
|
||||
return
|
||||
async def start(self):
|
||||
|
||||
try:
|
||||
await self.create_tribute_app()
|
||||
if not self.app:
|
||||
await self.create_app()
|
||||
|
||||
self.tribute_runner = web.AppRunner(self.tribute_app)
|
||||
await self.tribute_runner.setup()
|
||||
self.runner = web.AppRunner(self.app)
|
||||
await self.runner.setup()
|
||||
|
||||
self.tribute_site = web.TCPSite(
|
||||
self.tribute_runner,
|
||||
host='0.0.0.0',
|
||||
self.site = web.TCPSite(
|
||||
self.runner,
|
||||
host='0.0.0.0',
|
||||
port=settings.TRIBUTE_WEBHOOK_PORT
|
||||
)
|
||||
|
||||
await self.tribute_site.start()
|
||||
await self.site.start()
|
||||
|
||||
logger.info(f"✅ Tribute webhook сервер запущен на 0.0.0.0:{settings.TRIBUTE_WEBHOOK_PORT}")
|
||||
logger.info(f"✅ Webhook сервер запущен на порту {settings.TRIBUTE_WEBHOOK_PORT}")
|
||||
logger.info(f"🎯 Tribute webhook URL: http://your-server:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка запуска Tribute webhook сервера: {e}")
|
||||
logger.error(f"❌ Ошибка запуска webhook сервера: {e}")
|
||||
raise
|
||||
|
||||
async def start_yookassa_server(self):
|
||||
if not settings.is_yookassa_enabled():
|
||||
logger.info("YooKassa отключен, сервер не запускается")
|
||||
return
|
||||
|
||||
try:
|
||||
await self.create_yookassa_app()
|
||||
|
||||
self.yookassa_runner = web.AppRunner(self.yookassa_app)
|
||||
await self.yookassa_runner.setup()
|
||||
|
||||
self.yookassa_site = web.TCPSite(
|
||||
self.yookassa_runner,
|
||||
host='0.0.0.0',
|
||||
port=settings.YOOKASSA_WEBHOOK_PORT
|
||||
)
|
||||
|
||||
await self.yookassa_site.start()
|
||||
|
||||
logger.info(f"✅ YooKassa webhook сервер запущен на 0.0.0.0:{settings.YOOKASSA_WEBHOOK_PORT}")
|
||||
logger.info(f"🎯 YooKassa webhook URL: http://your-server:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка запуска YooKassa webhook сервера: {e}")
|
||||
raise
|
||||
|
||||
async def start(self):
|
||||
tasks = []
|
||||
|
||||
if settings.TRIBUTE_ENABLED:
|
||||
tasks.append(self.start_tribute_server())
|
||||
|
||||
if settings.is_yookassa_enabled():
|
||||
tasks.append(self.start_yookassa_server())
|
||||
|
||||
if not tasks:
|
||||
logger.warning("Ни один платежный провайдер не включен!")
|
||||
return
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
logger.info("🚀 Все webhook серверы запущены!")
|
||||
|
||||
async def stop(self):
|
||||
tasks = []
|
||||
|
||||
if self.tribute_site:
|
||||
tasks.append(self.tribute_site.stop())
|
||||
if self.tribute_runner:
|
||||
tasks.append(self.tribute_runner.cleanup())
|
||||
|
||||
if self.yookassa_site:
|
||||
tasks.append(self.yookassa_site.stop())
|
||||
if self.yookassa_runner:
|
||||
tasks.append(self.yookassa_runner.cleanup())
|
||||
|
||||
if tasks:
|
||||
try:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
logger.info("🛑 Все webhook серверы остановлены")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки webhook серверов: {e}")
|
||||
try:
|
||||
if self.site:
|
||||
await self.site.stop()
|
||||
logger.info("Webhook сайт остановлен")
|
||||
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
logger.info("Webhook runner очищен")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки webhook сервера: {e}")
|
||||
|
||||
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
|
||||
|
||||
try:
|
||||
raw_body = await request.read()
|
||||
|
||||
if not raw_body:
|
||||
logger.warning("Получен пустой Tribute webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "empty_body"},
|
||||
status=400
|
||||
)
|
||||
|
||||
try:
|
||||
payload = raw_body.decode('utf-8')
|
||||
import json
|
||||
webhook_data = json.loads(payload)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
||||
logger.error(f"Ошибка декодирования Tribute webhook: {e}")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "invalid_payload"},
|
||||
status=400
|
||||
)
|
||||
payload = raw_body.decode('utf-8')
|
||||
|
||||
signature = request.headers.get('X-Tribute-Signature')
|
||||
if signature and not self.tribute_service.verify_webhook_signature(payload, signature):
|
||||
logger.error("Неверная подпись Tribute webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "invalid_signature"},
|
||||
status=400
|
||||
)
|
||||
|
||||
result = await self.tribute_service.process_webhook(webhook_data)
|
||||
result = await self.tribute_service.process_webhook(payload, signature)
|
||||
|
||||
if result:
|
||||
from app.database.database import get_db
|
||||
async with get_db() as db:
|
||||
success = await self.payment_service.process_tribute_payment(
|
||||
db=db,
|
||||
user_id=result['user_id'],
|
||||
amount_kopeks=result['amount_kopeks'],
|
||||
payment_id=result['payment_id']
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"✅ Успешно обработан Tribute платеж: {result['payment_id']}")
|
||||
return web.json_response({"status": "ok"}, status=200)
|
||||
else:
|
||||
logger.error(f"❌ Ошибка обработки Tribute платежа: {result['payment_id']}")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "processing_failed"},
|
||||
status=500
|
||||
)
|
||||
else:
|
||||
logger.error("Не удалось обработать Tribute webhook данные")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "invalid_data"},
|
||||
status=400
|
||||
)
|
||||
return web.json_response(result, status=200)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Критическая ошибка обработки Tribute webhook: {e}", exc_info=True)
|
||||
logger.error(f"Ошибка обработки Tribute webhook: {e}")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "internal_error"},
|
||||
{"status": "error", "reason": "internal_error"},
|
||||
status=500
|
||||
)
|
||||
|
||||
async def _tribute_health_check(self, request: web.Request) -> web.Response:
|
||||
async def _health_check(self, request: web.Request) -> web.Response:
|
||||
|
||||
return web.json_response({
|
||||
"status": "ok",
|
||||
"service": "tribute-webhooks",
|
||||
"tribute_enabled": settings.TRIBUTE_ENABLED,
|
||||
"port": settings.TRIBUTE_WEBHOOK_PORT
|
||||
})
|
||||
|
||||
async def _yookassa_health_check(self, request: web.Request) -> web.Response:
|
||||
return web.json_response({
|
||||
"status": "ok",
|
||||
"service": "yookassa-webhooks",
|
||||
"yookassa_enabled": settings.is_yookassa_enabled(),
|
||||
"port": settings.YOOKASSA_WEBHOOK_PORT
|
||||
})
|
||||
"tribute_enabled": settings.TRIBUTE_ENABLED
|
||||
})
|
||||
@@ -500,7 +500,7 @@ async def check_yookassa_payment_status(
|
||||
emoji = status_emoji.get(payment.status, "❓")
|
||||
status = status_text.get(payment.status, "Неизвестно")
|
||||
|
||||
message_text = (f"💳 Статус платежа\n\n"
|
||||
message_text = (f"💳 <b>Статус платежа</b>\n\n"
|
||||
f"🆔 ID: {payment.yookassa_payment_id[:8]}...\n"
|
||||
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n"
|
||||
f"📊 Статус: {emoji} {status}\n"
|
||||
|
||||
@@ -342,26 +342,6 @@ class RussianTexts(Texts):
|
||||
💳 Требуется: {required}
|
||||
|
||||
Пополните баланс и продлите подписку вручную.
|
||||
"""
|
||||
|
||||
PAYMENT_SUCCESS_TRIBUTE = """
|
||||
✅ <b>Пополнение выполнено!</b>
|
||||
|
||||
💰 Сумма: {amount} ₽
|
||||
🎯 Способ: Tribute
|
||||
🆔 ID платежа: {payment_id}
|
||||
|
||||
Средства зачислены на ваш баланс!
|
||||
"""
|
||||
|
||||
PAYMENT_SUCCESS_YOOKASSA = """
|
||||
✅ <b>Пополнение выполнено!</b>
|
||||
|
||||
💰 Сумма: {amount} ₽
|
||||
🏦 Способ: Банковская карта (YooKassa)
|
||||
🆔 ID платежа: {payment_id}
|
||||
|
||||
Средства зачислены на ваш баланс!
|
||||
"""
|
||||
|
||||
SUPPORT_INFO = f"""
|
||||
@@ -411,6 +391,29 @@ class RussianTexts(Texts):
|
||||
|
||||
Не забудьте продлить подписку, чтобы не потерять доступ к серверам.
|
||||
"""
|
||||
|
||||
SUBSCRIPTION_EXPIRED = """
|
||||
❌ <b>Подписка истекла</b>
|
||||
|
||||
Ваша подписка истекла. Для восстановления доступа продлите подписку.
|
||||
"""
|
||||
|
||||
AUTOPAY_SUCCESS = """
|
||||
✅ <b>Автоплатеж выполнен</b>
|
||||
|
||||
Ваша подписка автоматически продлена на {days} дней.
|
||||
Списано с баланса: {amount}
|
||||
"""
|
||||
|
||||
AUTOPAY_FAILED = """
|
||||
❌ <b>Ошибка автоплатежа</b>
|
||||
|
||||
Не удалось списать средства для продления подписки.
|
||||
Недостаточно средств на балансе: {balance}
|
||||
Требуется: {required}
|
||||
|
||||
Пополните баланс и продлите подписку вручную.
|
||||
"""
|
||||
|
||||
|
||||
class EnglishTexts(Texts):
|
||||
@@ -504,20 +507,3 @@ def clear_rules_cache():
|
||||
global _cached_rules
|
||||
_cached_rules.clear()
|
||||
print("✅ Кеш правил очищен")
|
||||
|
||||
def get_text(key: str, language: str = "ru") -> str:
|
||||
texts = get_texts(language)
|
||||
|
||||
text_mapping = {
|
||||
"payment_success_tribute": getattr(texts, "PAYMENT_SUCCESS_TRIBUTE",
|
||||
"✅ Пополнение через Tribute выполнено! Сумма: {amount} ₽, ID: {payment_id}"),
|
||||
"payment_success_yookassa": getattr(texts, "PAYMENT_SUCCESS_YOOKASSA",
|
||||
"✅ Пополнение через YooKassa выполнено! Сумма: {amount} ₽, ID: {payment_id}"),
|
||||
"welcome": texts.WELCOME,
|
||||
"back": texts.BACK,
|
||||
"cancel": texts.CANCEL,
|
||||
"confirm": texts.CONFIRM,
|
||||
"continue": texts.CONTINUE,
|
||||
}
|
||||
|
||||
return text_mapping.get(key, f"Текст для ключа '{key}' не найден")
|
||||
|
||||
@@ -35,14 +35,12 @@ class MaintenanceService:
|
||||
return self._status
|
||||
|
||||
def is_maintenance_active(self) -> bool:
|
||||
"""Проверяет, активен ли режим техработ"""
|
||||
return self._status.is_active
|
||||
|
||||
def get_maintenance_message(self) -> str:
|
||||
"""Получает сообщение о техработах"""
|
||||
if self._status.auto_enabled:
|
||||
return f"""
|
||||
🔧 Технические работы
|
||||
🔧 Технические работы!
|
||||
|
||||
Сервис временно недоступен из-за проблем с подключением к серверам.
|
||||
|
||||
@@ -67,10 +65,6 @@ class MaintenanceService:
|
||||
await self._save_status_to_cache()
|
||||
|
||||
logger.warning(f"🔧 Режим техработ ВКЛЮЧЕН. Причина: {self._status.reason}")
|
||||
|
||||
if auto:
|
||||
await self._notify_admins_maintenance_enabled(reason)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -83,8 +77,6 @@ class MaintenanceService:
|
||||
logger.info("Режим техработ уже выключен")
|
||||
return True
|
||||
|
||||
was_auto_enabled = self._status.auto_enabled
|
||||
|
||||
self._status.is_active = False
|
||||
self._status.enabled_at = None
|
||||
self._status.reason = None
|
||||
@@ -94,10 +86,6 @@ class MaintenanceService:
|
||||
await self._save_status_to_cache()
|
||||
|
||||
logger.info("✅ Режим техработ ВЫКЛЮЧЕН")
|
||||
|
||||
if was_auto_enabled:
|
||||
await self._notify_admins_maintenance_disabled()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -105,7 +93,6 @@ class MaintenanceService:
|
||||
return False
|
||||
|
||||
async def start_monitoring(self) -> bool:
|
||||
"""Запускает мониторинг API RemnaWave"""
|
||||
try:
|
||||
if self._check_task and not self._check_task.done():
|
||||
logger.warning("Мониторинг уже запущен")
|
||||
@@ -194,10 +181,9 @@ class MaintenanceService:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в цикле мониторинга: {e}")
|
||||
await asyncio.sleep(30)
|
||||
await asyncio.sleep(30)
|
||||
|
||||
async def _save_status_to_cache(self):
|
||||
"""Сохраняет состояние в кеше"""
|
||||
try:
|
||||
status_data = {
|
||||
"is_active": self._status.is_active,
|
||||
@@ -278,84 +264,5 @@ class MaintenanceService:
|
||||
"consecutive_failures": self._status.consecutive_failures
|
||||
}
|
||||
|
||||
def set_bot(self, bot):
|
||||
self._bot = bot
|
||||
|
||||
async def _notify_admins_maintenance_enabled(self, reason: Optional[str] = None):
|
||||
if not hasattr(self, '_bot') or not self._bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
admin_ids = settings.get_admin_ids()
|
||||
if not admin_ids:
|
||||
return
|
||||
|
||||
enabled_time = self._status.enabled_at.strftime("%d.%m.%Y %H:%M:%S") if self._status.enabled_at else "неизвестно"
|
||||
|
||||
message = f"""
|
||||
🔧 <b>Режим техработ автоматически ВКЛЮЧЕН</b>
|
||||
|
||||
⏰ <b>Время:</b> {enabled_time}
|
||||
📝 <b>Причина:</b> {reason or 'Недоступность API'}
|
||||
🔄 <b>Неудачных проверок:</b> {self._status.consecutive_failures}
|
||||
|
||||
ℹ️ Обычные пользователи заблокированы до восстановления API.
|
||||
⚙️ Админы имеют полный доступ к боту.
|
||||
|
||||
🔍 Для управления используйте админ-панель → Техработы
|
||||
"""
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔧 Панель техработ", callback_data="maintenance_panel")]
|
||||
])
|
||||
|
||||
for admin_id in admin_ids:
|
||||
try:
|
||||
await self._bot.send_message(
|
||||
admin_id,
|
||||
message,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомлений админам о включении техработ: {e}")
|
||||
|
||||
async def _notify_admins_maintenance_disabled(self):
|
||||
if not hasattr(self, '_bot') or not self._bot:
|
||||
return
|
||||
|
||||
try:
|
||||
admin_ids = settings.get_admin_ids()
|
||||
if not admin_ids:
|
||||
return
|
||||
|
||||
disabled_time = datetime.utcnow().strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
message = f"""
|
||||
✅ <b>Режим техработ автоматически ОТКЛЮЧЕН</b>
|
||||
|
||||
⏰ <b>Время:</b> {disabled_time}
|
||||
🔄 <b>API восстановлено:</b> Подключение к Remnawave работает
|
||||
|
||||
ℹ️ Пользователи снова могут использовать бота.
|
||||
"""
|
||||
|
||||
for admin_id in admin_ids:
|
||||
try:
|
||||
await self._bot.send_message(
|
||||
admin_id,
|
||||
message,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомлений админам о выключении техработ: {e}")
|
||||
|
||||
maintenance_service = MaintenanceService()
|
||||
|
||||
@@ -131,81 +131,6 @@ class PaymentService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания платежа YooKassa: {e}")
|
||||
return None
|
||||
|
||||
async def process_tribute_payment(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
amount_kopeks: int,
|
||||
payment_id: str
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
logger.info(f"Обработка Tribute платежа: telegram_id={user_id}, amount={amount_kopeks} коп., payment_id={payment_id}")
|
||||
|
||||
from app.database.crud.user import UserCRUD
|
||||
user_crud = UserCRUD()
|
||||
|
||||
user = await user_crud.get_user_by_telegram_id(db, user_id)
|
||||
if not user:
|
||||
logger.error(f"Пользователь с telegram_id {user_id} не найден для обработки Tribute платежа")
|
||||
return False
|
||||
|
||||
from app.database.crud.transaction import TransactionCRUD
|
||||
transaction_crud = TransactionCRUD()
|
||||
|
||||
existing_transaction = await transaction_crud.get_transaction_by_external_id(
|
||||
db, f"tribute_{payment_id}"
|
||||
)
|
||||
|
||||
if existing_transaction:
|
||||
logger.info(f"Платеж {payment_id} уже был обработан ранее")
|
||||
return True
|
||||
|
||||
transaction_data = {
|
||||
"user_id": user.id,
|
||||
"amount_kopeks": amount_kopeks,
|
||||
"transaction_type": "deposit",
|
||||
"status": "completed",
|
||||
"external_id": f"tribute_{payment_id}",
|
||||
"payment_system": "tribute",
|
||||
"description": f"Пополнение через Tribute: {payment_id}"
|
||||
}
|
||||
|
||||
transaction = await transaction_crud.create_transaction(db, transaction_data)
|
||||
|
||||
if not transaction:
|
||||
logger.error(f"Не удалось создать транзакцию для Tribute платежа {payment_id}")
|
||||
return False
|
||||
|
||||
success = await user_crud.add_balance(db, user.id, amount_kopeks)
|
||||
|
||||
if not success:
|
||||
logger.error(f"Не удалось пополнить баланс пользователя {user.id}")
|
||||
await transaction_crud.update_transaction_status(db, transaction.id, "failed")
|
||||
return False
|
||||
|
||||
try:
|
||||
from app.localization.texts import get_text
|
||||
|
||||
amount_rubles = amount_kopeks / 100
|
||||
message = get_text("payment_success_tribute").format(
|
||||
amount=f"{amount_rubles:.2f}",
|
||||
payment_id=payment_id
|
||||
)
|
||||
|
||||
if hasattr(self, 'bot') and self.bot:
|
||||
await self.bot.send_message(user.telegram_id, message)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось отправить уведомление о платеже пользователю {user.telegram_id}: {e}")
|
||||
|
||||
logger.info(f"✅ Успешно обработан Tribute платеж {payment_id} для пользователя {user.telegram_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обработки Tribute платежа {payment_id}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def process_yookassa_webhook(self, db: AsyncSession, webhook_data: dict) -> bool:
|
||||
try:
|
||||
|
||||
@@ -20,7 +20,7 @@ async def process_referral_registration(
|
||||
referrer = await get_user_by_id(db, referrer_id)
|
||||
|
||||
if not new_user or not referrer:
|
||||
logger.error(f"Пользователи не найдены: new_user_id={new_user_id}, referrer_id={referrer_id}")
|
||||
logger.error(f"Пользователи не найдены: {new_user_id}, {referrer_id}")
|
||||
return False
|
||||
|
||||
if new_user.referred_by_id != referrer_id:
|
||||
@@ -81,13 +81,9 @@ async def process_referral_purchase(
|
||||
|
||||
logger.info(f"🔍 Покупка реферала {user_id}: первая = {is_first_purchase}, сумма = {purchase_amount_kopeks/100}₽")
|
||||
|
||||
if is_first_purchase and settings.REFERRAL_REGISTRATION_REWARD > 0:
|
||||
if is_first_purchase:
|
||||
reward_amount = settings.REFERRAL_REGISTRATION_REWARD
|
||||
|
||||
if reward_amount > 1000000:
|
||||
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: reward_amount = {reward_amount} слишком большой! Проверьте настройки REFERRAL_REGISTRATION_REWARD")
|
||||
reward_amount = 10000
|
||||
|
||||
await add_user_balance(
|
||||
db, referrer, reward_amount,
|
||||
f"Реферальная награда за первую покупку {user.full_name}"
|
||||
@@ -104,18 +100,12 @@ async def process_referral_purchase(
|
||||
|
||||
logger.info(f"🎉 Первая покупка реферала: {referrer.telegram_id} получил {reward_amount/100}₽")
|
||||
|
||||
if not (0 <= settings.REFERRAL_COMMISSION_PERCENT <= 100):
|
||||
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: REFERRAL_COMMISSION_PERCENT = {settings.REFERRAL_COMMISSION_PERCENT} некорректный! Должен быть от 0 до 100")
|
||||
commission_percent = 10
|
||||
else:
|
||||
commission_percent = settings.REFERRAL_COMMISSION_PERCENT
|
||||
|
||||
commission_amount = int(purchase_amount_kopeks * commission_percent / 100)
|
||||
commission_amount = int(purchase_amount_kopeks * settings.REFERRAL_COMMISSION_PERCENT / 100)
|
||||
|
||||
if commission_amount > 0:
|
||||
await add_user_balance(
|
||||
db, referrer, commission_amount,
|
||||
f"Комиссия {commission_percent}% с покупки {user.full_name}"
|
||||
f"Комиссия {settings.REFERRAL_COMMISSION_PERCENT}% с покупки {user.full_name}"
|
||||
)
|
||||
|
||||
await create_referral_earning(
|
||||
@@ -138,8 +128,6 @@ async def process_referral_purchase(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки покупки реферала: {e}")
|
||||
import traceback
|
||||
logger.error(f"Полный traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -153,7 +141,7 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
|
||||
invited_count_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.referred_by_id == user_id)
|
||||
)
|
||||
invited_count = invited_count_result.scalar() or 0
|
||||
invited_count = invited_count_result.scalar()
|
||||
|
||||
paid_referrals_result = await db.execute(
|
||||
select(func.count(User.id)).where(
|
||||
@@ -161,13 +149,13 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
|
||||
User.has_had_paid_subscription == True
|
||||
)
|
||||
)
|
||||
paid_referrals_count = paid_referrals_result.scalar() or 0
|
||||
paid_referrals_count = paid_referrals_result.scalar()
|
||||
|
||||
total_earned = await get_referral_earnings_sum(db, user_id) or 0
|
||||
total_earned = await get_referral_earnings_sum(db, user_id)
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
month_ago = datetime.utcnow() - timedelta(days=30)
|
||||
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago) or 0
|
||||
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago)
|
||||
|
||||
return {
|
||||
"invited_count": invited_count,
|
||||
@@ -183,4 +171,4 @@ async def get_referral_stats_for_user(db: AsyncSession, user_id: int) -> dict:
|
||||
"paid_referrals_count": 0,
|
||||
"total_earned_kopeks": 0,
|
||||
"month_earned_kopeks": 0
|
||||
}
|
||||
}
|
||||
@@ -306,109 +306,43 @@ class SubscriptionService:
|
||||
try:
|
||||
needs_cleanup = False
|
||||
|
||||
if not isinstance(subscription.connected_squads, list):
|
||||
logger.warning(f"Исправляем connected_squads для пользователя {user.telegram_id}")
|
||||
subscription.connected_squads = []
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.connected_squads:
|
||||
unique_squads = list(set([squad for squad in subscription.connected_squads if squad and isinstance(squad, str)]))
|
||||
if len(unique_squads) != len(subscription.connected_squads):
|
||||
logger.info(f"Очищены дубликаты в connected_squads для пользователя {user.telegram_id}")
|
||||
subscription.connected_squads = unique_squads
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.traffic_limit_gb < 0:
|
||||
logger.warning(f"Отрицательный traffic_limit_gb исправлен на 0 для пользователя {user.telegram_id}")
|
||||
subscription.traffic_limit_gb = 0
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.traffic_used_gb < 0:
|
||||
logger.warning(f"Отрицательный traffic_used_gb исправлен на 0 для пользователя {user.telegram_id}")
|
||||
subscription.traffic_used_gb = 0.0
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.device_limit < 1:
|
||||
logger.warning(f"device_limit < 1 исправлен на 1 для пользователя {user.telegram_id}")
|
||||
subscription.device_limit = 1
|
||||
needs_cleanup = True
|
||||
elif subscription.device_limit > 10:
|
||||
logger.warning(f"device_limit > 10 исправлен на 10 для пользователя {user.telegram_id}")
|
||||
subscription.device_limit = 10
|
||||
needs_cleanup = True
|
||||
|
||||
from datetime import datetime
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
if subscription.start_date > current_time + timedelta(days=1):
|
||||
logger.warning(f"Некорректная start_date исправлена для пользователя {user.telegram_id}")
|
||||
subscription.start_date = current_time
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.end_date < subscription.start_date:
|
||||
logger.warning(f"end_date раньше start_date исправлено для пользователя {user.telegram_id}")
|
||||
subscription.end_date = subscription.start_date + timedelta(days=1)
|
||||
needs_cleanup = True
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
async with self.api as api:
|
||||
remnawave_user = await api.get_user_by_uuid(user.remnawave_uuid)
|
||||
|
||||
if not remnawave_user:
|
||||
logger.warning(f"Пользователь {user.telegram_id} имеет UUID {user.remnawave_uuid}, но не найден в панели")
|
||||
logger.warning(f"⚠️ Пользователь {user.telegram_id} имеет UUID {user.remnawave_uuid}, но не найден в панели")
|
||||
needs_cleanup = True
|
||||
else:
|
||||
if remnawave_user.telegram_id != user.telegram_id:
|
||||
logger.warning(f"Несоответствие telegram_id для пользователя {user.telegram_id}")
|
||||
logger.warning(f"⚠️ Несоответствие telegram_id для пользователя {user.telegram_id}")
|
||||
needs_cleanup = True
|
||||
|
||||
if remnawave_user.subscription_url and not subscription.subscription_url:
|
||||
subscription.subscription_url = remnawave_user.subscription_url
|
||||
logger.info(f"Восстановлена subscription_url из панели для пользователя {user.telegram_id}")
|
||||
needs_cleanup = True
|
||||
|
||||
except Exception as api_error:
|
||||
logger.error(f"Ошибка проверки пользователя в панели: {api_error}")
|
||||
logger.error(f"❌ Ошибка проверки пользователя в панели: {api_error}")
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.remnawave_short_uuid and not user.remnawave_uuid:
|
||||
logger.warning(f"У подписки есть short_uuid, но у пользователя нет remnawave_uuid")
|
||||
logger.warning(f"⚠️ У подписки есть short_uuid, но у пользователя нет remnawave_uuid")
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.subscription_url:
|
||||
if not subscription.subscription_url.startswith(('http://', 'https://')):
|
||||
logger.warning(f"Некорректный subscription_url для пользователя {user.telegram_id}")
|
||||
subscription.subscription_url = ""
|
||||
needs_cleanup = True
|
||||
|
||||
if needs_cleanup and (
|
||||
not user.remnawave_uuid or
|
||||
subscription.remnawave_short_uuid and not user.remnawave_uuid
|
||||
):
|
||||
logger.info(f"Очищаем критические мусорные данные подписки для пользователя {user.telegram_id}")
|
||||
|
||||
if needs_cleanup:
|
||||
logger.info(f"🧹 Очищаем мусорные данные подписки для пользователя {user.telegram_id}")
|
||||
|
||||
subscription.remnawave_short_uuid = None
|
||||
subscription.subscription_url = ""
|
||||
|
||||
subscription.connected_squads = []
|
||||
|
||||
user.remnawave_uuid = None
|
||||
|
||||
if needs_cleanup:
|
||||
subscription.updated_at = current_time
|
||||
await db.commit()
|
||||
logger.info(f"Исправления применены для пользователя {user.telegram_id}")
|
||||
logger.info(f"✅ Мусорные данные очищены для пользователя {user.telegram_id}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
|
||||
import traceback
|
||||
logger.error(f"Полный traceback: {traceback.format_exc()}")
|
||||
try:
|
||||
await db.rollback()
|
||||
except:
|
||||
pass
|
||||
logger.error(f"❌ Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
async def get_countries_price_by_uuids(
|
||||
|
||||
Reference in New Issue
Block a user