94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
|
|
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
|