mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-17 09:30:35 +00:00
156 lines
5.1 KiB
Python
156 lines
5.1 KiB
Python
"""Flask webhook server for PayPalych postbacks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import threading
|
|
from asyncio import AbstractEventLoop
|
|
from typing import Any, Dict, Optional
|
|
|
|
from flask import Flask, jsonify, request
|
|
from werkzeug.serving import make_server
|
|
|
|
from app.config import settings
|
|
from app.database.database import get_db
|
|
from app.services.pal24_service import Pal24Service, Pal24APIError
|
|
from app.services.payment_service import PaymentService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _normalize_payload() -> Dict[str, str]:
|
|
if request.is_json:
|
|
payload = request.get_json(silent=True) or {}
|
|
if isinstance(payload, dict):
|
|
return {k: str(v) for k, v in payload.items()}
|
|
logger.warning("Pal24 webhook JSON payload не является объектом: %s", payload)
|
|
return {}
|
|
|
|
if request.form:
|
|
return {k: v for k, v in request.form.items()}
|
|
|
|
try:
|
|
raw_body = request.data.decode("utf-8")
|
|
if raw_body:
|
|
payload = json.loads(raw_body)
|
|
if isinstance(payload, dict):
|
|
return {k: str(v) for k, v in payload.items()}
|
|
except json.JSONDecodeError:
|
|
logger.debug("Pal24 webhook body не удалось распарсить как JSON")
|
|
|
|
return {}
|
|
|
|
|
|
def create_pal24_flask_app(
|
|
payment_service: PaymentService,
|
|
loop: AbstractEventLoop,
|
|
) -> Flask:
|
|
pal24_service = Pal24Service()
|
|
app = Flask(__name__)
|
|
|
|
@app.route(settings.PAL24_WEBHOOK_PATH, methods=["POST"])
|
|
def pal24_webhook() -> tuple:
|
|
if not pal24_service.is_configured:
|
|
logger.error("Pal24 webhook получен, но сервис не настроен")
|
|
return jsonify({"status": "error", "reason": "service_not_configured"}), 503
|
|
|
|
payload = _normalize_payload()
|
|
if not payload:
|
|
logger.warning("Пустой Pal24 webhook")
|
|
return jsonify({"status": "error", "reason": "empty_payload"}), 400
|
|
|
|
try:
|
|
parsed_payload = pal24_service.parse_postback(payload)
|
|
except Pal24APIError as error:
|
|
logger.error("Ошибка валидации Pal24 webhook: %s", error)
|
|
return jsonify({"status": "error", "reason": str(error)}), 400
|
|
|
|
async def process() -> bool:
|
|
async for db in get_db():
|
|
try:
|
|
return await payment_service.process_pal24_postback(db, parsed_payload)
|
|
finally:
|
|
await db.close()
|
|
|
|
try:
|
|
future = asyncio.run_coroutine_threadsafe(process(), loop)
|
|
processed = future.result()
|
|
except Exception as error: # pragma: no cover - defensive
|
|
logger.exception("Критическая ошибка обработки Pal24 webhook: %s", error)
|
|
return jsonify({"status": "error", "reason": "internal_error"}), 500
|
|
|
|
if processed:
|
|
return jsonify({"status": "ok"}), 200
|
|
return jsonify({"status": "error", "reason": "not_processed"}), 400
|
|
|
|
@app.route(settings.PAL24_WEBHOOK_PATH, methods=["GET"])
|
|
def pal24_health() -> tuple:
|
|
return jsonify({
|
|
"status": "ok",
|
|
"service": "pal24_webhook",
|
|
"enabled": settings.is_pal24_enabled(),
|
|
}), 200
|
|
|
|
@app.route("/pal24/health", methods=["GET"])
|
|
def pal24_additional_health() -> tuple:
|
|
return jsonify({
|
|
"status": "ok",
|
|
"service": "pal24_webhook",
|
|
"path": settings.PAL24_WEBHOOK_PATH,
|
|
}), 200
|
|
|
|
return app
|
|
|
|
|
|
class Pal24WebhookServer:
|
|
"""Threaded Flask server for Pal24 postbacks."""
|
|
|
|
def __init__(self, payment_service: PaymentService, loop: AbstractEventLoop) -> None:
|
|
self.app = create_pal24_flask_app(payment_service, loop)
|
|
self._server: Optional[Any] = None
|
|
self._thread: Optional[threading.Thread] = None
|
|
|
|
def start(self) -> None:
|
|
if self._server:
|
|
logger.warning("Pal24 webhook server уже запущен")
|
|
return
|
|
|
|
self._server = make_server(
|
|
host="0.0.0.0",
|
|
port=settings.PAL24_WEBHOOK_PORT,
|
|
app=self.app,
|
|
threaded=True,
|
|
)
|
|
|
|
def _serve() -> None:
|
|
logger.info(
|
|
"Pal24 webhook сервер запущен на %s:%s%s",
|
|
"0.0.0.0",
|
|
settings.PAL24_WEBHOOK_PORT,
|
|
settings.PAL24_WEBHOOK_PATH,
|
|
)
|
|
self._server.serve_forever()
|
|
|
|
self._thread = threading.Thread(target=_serve, daemon=True)
|
|
self._thread.start()
|
|
|
|
def stop(self) -> None:
|
|
if self._server:
|
|
logger.info("Останавливаем Pal24 webhook сервер")
|
|
self._server.shutdown()
|
|
self._server = None
|
|
|
|
if self._thread and self._thread.is_alive():
|
|
self._thread.join(timeout=5)
|
|
self._thread = None
|
|
|
|
|
|
async def start_pal24_webhook_server(payment_service: PaymentService) -> Pal24WebhookServer:
|
|
loop = asyncio.get_running_loop()
|
|
server = Pal24WebhookServer(payment_service, loop)
|
|
await loop.run_in_executor(None, server.start)
|
|
return server
|
|
|