Files
remnawave-bedolaga-telegram…/app/external/heleket_webhook.py
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- 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
2026-02-16 09:18:12 +03:00

119 lines
4.3 KiB
Python

import asyncio
import json
from typing import Any
import structlog
from aiohttp import web
from app.config import settings
from app.database.database import AsyncSessionLocal
from app.external.heleket import HeleketService
from app.services.payment_service import PaymentService
logger = structlog.get_logger(__name__)
class HeleketWebhookHandler:
def __init__(self, payment_service: PaymentService) -> None:
self.payment_service = payment_service
self.service = HeleketService()
async def handle(self, request: web.Request) -> web.Response:
if not settings.is_heleket_enabled():
logger.warning('Получен Heleket webhook, но сервис отключен')
return web.json_response({'status': 'error', 'reason': 'disabled'}, status=503)
try:
payload: dict[str, Any] = await request.json()
except json.JSONDecodeError:
logger.error('Некорректный JSON Heleket webhook')
return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400)
if not self.service.verify_webhook_signature(payload):
return web.json_response({'status': 'error', 'reason': 'invalid_signature'}, status=401)
processed: bool | None = None
async with AsyncSessionLocal() as db:
try:
processed = await self.payment_service.process_heleket_webhook(db, payload)
await db.commit()
except Exception as e:
logger.error('Ошибка обработки Heleket webhook', error=e)
await db.rollback()
return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500)
if processed:
return web.json_response({'status': 'ok'}, status=200)
return web.json_response({'status': 'error', 'reason': 'not_processed'}, status=400)
async def health_check(self, _: web.Request) -> web.Response:
return web.json_response(
{
'status': 'ok',
'service': 'heleket_webhook',
'enabled': settings.is_heleket_enabled(),
'path': settings.HELEKET_WEBHOOK_PATH,
}
)
async def options_handler(self, _: web.Request) -> web.Response:
return web.Response(
status=200,
headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
)
def create_heleket_app(payment_service: PaymentService) -> web.Application:
handler = HeleketWebhookHandler(payment_service)
app = web.Application()
app.router.add_post(settings.HELEKET_WEBHOOK_PATH, handler.handle)
app.router.add_get('/heleket/health', handler.health_check)
app.router.add_get('/health', handler.health_check)
app.router.add_options(settings.HELEKET_WEBHOOK_PATH, handler.options_handler)
return app
async def start_heleket_webhook_server(payment_service: PaymentService) -> None:
if not settings.is_heleket_enabled():
logger.info('Heleket отключен, webhook сервер не запускается')
return
app = create_heleket_app(payment_service)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(
runner,
host=settings.HELEKET_WEBHOOK_HOST,
port=settings.HELEKET_WEBHOOK_PORT,
)
try:
await site.start()
logger.info(
'Heleket webhook сервер запущен на',
HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST,
HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT,
)
logger.info(
'Heleket webhook URL: http://',
HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST,
HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT,
HELEKET_WEBHOOK_PATH=settings.HELEKET_WEBHOOK_PATH,
)
while True:
await asyncio.sleep(1)
except asyncio.CancelledError:
logger.info('Heleket webhook сервер остановлен по запросу')
finally:
await site.stop()
await runner.cleanup()
logger.info('Heleket webhook сервер корректно остановлен')