mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-02 08:11:32 +00:00
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding via structlog contextvars (aiogram) and http_method/http_path (FastAPI) - Use bound_contextvars() context manager instead of clear_contextvars() to safely restore previous state instead of wiping all context - Register ContextVarsMiddleware as outermost middleware (before GlobalError) so all error logs include user context - Replace structlog.get_logger() with structlog.get_logger(__name__) across 270 calls in 265 files for meaningful logger names - Switch wrapper_class from BoundLogger to make_filtering_bound_logger() for pre-processor level filtering (performance optimization) - Migrate 1411 %-style positional arg logger calls to structlog kwargs style across 161 files via AST script - Migrate log_rotation_service.py from stdlib logging to structlog - Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking to Telegram notifications and general log files - Fix LoggingMiddleware: add from_user null-safety for channel posts, switch time.time() to time.monotonic() for duration measurement - Remove duplicate logger assignments in purchase.py, config.py, inline.py, and admin/payments.py
158 lines
6.4 KiB
Python
158 lines
6.4 KiB
Python
import hashlib
|
||
import hmac
|
||
import json
|
||
from typing import Any
|
||
|
||
import structlog
|
||
|
||
from app.config import settings
|
||
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
class TributeService:
|
||
def __init__(self):
|
||
self.api_key = settings.TRIBUTE_API_KEY
|
||
self.donate_link = settings.TRIBUTE_DONATE_LINK
|
||
|
||
async def create_payment_link(
|
||
self, user_id: int, amount_kopeks: int = 0, description: str = 'Пополнение баланса'
|
||
) -> str | None:
|
||
if not settings.TRIBUTE_ENABLED:
|
||
logger.warning('Tribute платежи отключены')
|
||
return None
|
||
|
||
try:
|
||
payment_url = f'{self.donate_link}&user_id={user_id}'
|
||
|
||
logger.info('Создана ссылка Tribute для пользователя', user_id=user_id)
|
||
return payment_url
|
||
|
||
except Exception as e:
|
||
logger.error('Ошибка создания Tribute ссылки', error=e)
|
||
return None
|
||
|
||
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
|
||
if not self.api_key:
|
||
logger.warning('API key не настроен, пропускаем проверку')
|
||
return True
|
||
|
||
try:
|
||
expected_signature = hmac.new(self.api_key.encode(), payload.encode(), hashlib.sha256).hexdigest()
|
||
|
||
is_valid = hmac.compare_digest(signature, expected_signature)
|
||
|
||
if is_valid:
|
||
logger.info('✅ Подпись Tribute webhook проверена успешно')
|
||
else:
|
||
logger.error('❌ Неверная подпись Tribute webhook')
|
||
|
||
return is_valid
|
||
|
||
except Exception as e:
|
||
logger.error('Ошибка проверки подписи webhook', error=e)
|
||
return False
|
||
|
||
async def process_webhook(self, payload_or_data) -> dict[str, Any] | None:
|
||
try:
|
||
logger.info('🔄 Начинаем обработку Tribute webhook')
|
||
|
||
if isinstance(payload_or_data, str):
|
||
try:
|
||
webhook_data = json.loads(payload_or_data)
|
||
logger.info('📊 Распарсенные данные', webhook_data=webhook_data)
|
||
except json.JSONDecodeError as e:
|
||
logger.error('❌ Ошибка парсинга JSON', error=e)
|
||
return None
|
||
else:
|
||
webhook_data = payload_or_data
|
||
|
||
payment_id = None
|
||
status = None
|
||
amount_kopeks = 0
|
||
telegram_user_id = None
|
||
|
||
payment_id = webhook_data.get('id') or webhook_data.get('payment_id')
|
||
status = webhook_data.get('status')
|
||
amount_kopeks = webhook_data.get('amount', 0)
|
||
telegram_user_id = webhook_data.get('telegram_user_id') or webhook_data.get('user_id')
|
||
|
||
if not payment_id and 'payload' in webhook_data:
|
||
data = webhook_data['payload']
|
||
payment_id = data.get('id') or data.get('payment_id')
|
||
status = data.get('status')
|
||
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 webhook_data:
|
||
event_name = webhook_data.get('name')
|
||
data = webhook_data.get('payload', {})
|
||
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 in ('new_donation', 'recurrent_donation'):
|
||
status = 'paid'
|
||
elif event_name == 'cancelled_subscription':
|
||
status = 'cancelled'
|
||
else:
|
||
status = 'unknown'
|
||
|
||
logger.info(
|
||
'📝 Извлеченные данные: payment_id=, status=, amount_kopeks=, user_id',
|
||
payment_id=payment_id,
|
||
status=status,
|
||
amount_kopeks=amount_kopeks,
|
||
telegram_user_id=telegram_user_id,
|
||
)
|
||
|
||
if not telegram_user_id:
|
||
logger.error('❌ Не найден telegram_user_id в webhook данных')
|
||
logger.error(
|
||
'🔍 Полные данные для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2)
|
||
)
|
||
return None
|
||
|
||
try:
|
||
telegram_user_id = int(telegram_user_id)
|
||
except (ValueError, TypeError):
|
||
logger.error('❌ Некорректный telegram_user_id', telegram_user_id=telegram_user_id)
|
||
return None
|
||
|
||
result = {
|
||
'event_type': 'payment',
|
||
'payment_id': payment_id or f'tribute_{telegram_user_id}_{amount_kopeks}',
|
||
'user_id': telegram_user_id,
|
||
'amount_kopeks': int(amount_kopeks) if amount_kopeks else 0,
|
||
'status': status or 'paid',
|
||
'external_id': f'donation_{payment_id or "unknown"}',
|
||
'payment_system': 'tribute',
|
||
}
|
||
|
||
logger.info('✅ Tribute webhook обработан успешно', result=result)
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error('❌ Ошибка обработки Tribute webhook', error=e, exc_info=True)
|
||
logger.error('🔍 Webhook data для отладки', dumps=json.dumps(webhook_data, ensure_ascii=False, indent=2))
|
||
return None
|
||
|
||
async def get_payment_status(self, payment_id: str) -> dict[str, Any] | None:
|
||
try:
|
||
logger.info('Запрос статуса платежа', payment_id=payment_id)
|
||
return {'status': 'unknown', 'payment_id': payment_id}
|
||
except Exception as e:
|
||
logger.error('Ошибка получения статуса платежа', error=e)
|
||
return None
|
||
|
||
async def refund_payment(
|
||
self, payment_id: str, amount_kopeks: int | None = None, reason: str = 'Возврат по запросу'
|
||
) -> dict[str, Any] | None:
|
||
try:
|
||
logger.info('Создание возврата для платежа', payment_id=payment_id)
|
||
return {'refund_id': f'refund_{payment_id}', 'status': 'pending'}
|
||
except Exception as e:
|
||
logger.error('Ошибка создания возврата', error=e)
|
||
return None
|