from __future__ import annotations import logging from fastapi import APIRouter, HTTPException, Request from yuxi.channel.extensions.pinduoduo.dedupe import PddMessageDeduplicator from yuxi.channel.extensions.pinduoduo.monitor import PinduoduoMonitor from yuxi.channel.extensions.pinduoduo.signature import verify_push_sign from yuxi.channel.extensions.pinduoduo.types import PinduoduoAccount from yuxi.channel.extensions.pinduoduo.window import PinduoduoWindowTracker logger = logging.getLogger(__name__) _handlers: dict[str, dict] = {} def register_handler( account_id: str, account: PinduoduoAccount, monitor: PinduoduoMonitor, deduplicator: PddMessageDeduplicator, put_fn, window_tracker: PinduoduoWindowTracker | None = None, ) -> None: _handlers[account_id] = { "account": account, "monitor": monitor, "deduplicator": deduplicator, "put_fn": put_fn, "window_tracker": window_tracker, } logger.info("Webhook handler registered for account=%s", account_id) def unregister_handler(account_id: str) -> None: _handlers.pop(account_id, None) logger.info("Webhook handler unregistered for account=%s", account_id) def create_webhook_router() -> APIRouter: router = APIRouter(prefix="/webhook/pinduoduo", tags=["pinduoduo"]) @router.post("/callback") async def handle_push(request: Request): try: body = await request.json() except Exception: raise HTTPException(status_code=400, detail="Invalid JSON") sign = request.headers.get("X-Pdd-Sign", "") mall_id = str(body.get("mall_id", "")) handler = None for h in _handlers.values(): if h["account"].mall_id == mall_id: handler = h break if handler is None: logger.warning("No handler found for mall_id=%s", mall_id) return {"status": "no_handler"} if handler["account"].client_secret: body_str = (await request.body()).decode("utf-8") if not verify_push_sign(body_str, handler["account"].client_secret, sign, handler["account"].sign_method): logger.warning("Push signature verification failed for mall_id=%s", mall_id) raise HTTPException(status_code=403, detail="Invalid signature") monitor: PinduoduoMonitor = handler["monitor"] messages = monitor.parse_push_event(body) for msg in messages: if handler["deduplicator"].is_duplicate(msg.msg_id): continue if handler.get("window_tracker") and msg.buyer_id: handler["window_tracker"].record(msg.buyer_id) unified = monitor.to_unified_message(msg) await handler["put_fn"](unified) return {"status": "ok", "message_count": len(messages)} @router.post("/callback/verify") async def verify_push_url(request: Request): params = request.query_params echostr = params.get("echostr", "") if echostr: return {"echostr": echostr} return {"status": "ok"} return router