import hashlib import hmac import json import logging from fastapi import APIRouter, Request, HTTPException, Query from fastapi.responses import PlainTextResponse, JSONResponse router = APIRouter(prefix="/api/channel/messenger", tags=["messenger"]) logger = logging.getLogger(__name__) @router.get("/webhook") async def verify_webhook( hub_mode: str = Query(None, alias="hub.mode"), hub_challenge: str = Query(None, alias="hub.challenge"), hub_verify_token: str = Query(None, alias="hub.verify_token"), ): from yuxi.channel.extensions.messenger.config import MessengerConfigAdapter adapter = MessengerConfigAdapter() account = adapter._build_account("default", {}) verify_token = account.get("verify_token", "") if not verify_token: raise HTTPException(status_code=500, detail="verify_token not configured") if hub_mode == "subscribe" and hub_verify_token == verify_token: logger.info("messenger webhook verified successfully") return PlainTextResponse(hub_challenge or "", status_code=200) logger.warning("messenger webhook verification failed: token mismatch") raise HTTPException(status_code=403, detail="Verification failed") @router.post("/webhook") async def receive_webhook(request: Request): body = await request.body() if not _verify_signature(request.headers, body): logger.warning("messenger webhook signature verification failed") raise HTTPException(status_code=403, detail="Invalid signature") payload = json.loads(body) logger.debug(f"messenger webhook received: entries={len(payload.get('entry', []))}") from yuxi.channel.extensions.messenger.gateway import _get_webhook_queue queue = _get_webhook_queue() if queue is not None: await queue.put(payload) return JSONResponse({"status": "ok"}) def _verify_signature(headers, body: bytes) -> bool: from yuxi.channel.extensions.messenger.config import MessengerConfigAdapter adapter = MessengerConfigAdapter() account = adapter._build_account("default", {}) app_secret = account.get("app_secret", "") if not app_secret: logger.warning("messenger app_secret not set, skipping signature verification") return True expected = headers.get("X-Hub-Signature-256", "") if not expected.startswith("sha256="): return False actual = hmac.new(app_secret.encode("utf-8"), body, hashlib.sha256).hexdigest() return hmac.compare_digest(f"sha256={actual}", expected)