import hashlib import hmac import json import logging from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import JSONResponse, PlainTextResponse logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/channel/workplace", tags=["workplace-webhook"]) @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"), ): if hub_mode != "subscribe": raise HTTPException(status_code=400, detail="Invalid hub.mode") from yuxi.channel.extensions.workplace.config import WorkplaceConfigAdapter adapter = WorkplaceConfigAdapter() account = await adapter.resolve_account("default") expected_token = account.get("verify_token", "") if not expected_token or hub_verify_token != expected_token: logger.warning("Webhook verify_token mismatch") raise HTTPException(status_code=403, detail="Verify token mismatch") logger.info("Webhook verified successfully") return PlainTextResponse(hub_challenge or "", status_code=200) @router.post("/webhook") async def receive_webhook(request: Request): body = await request.body() signature = request.headers.get("X-Hub-Signature-256", "") from yuxi.channel.extensions.workplace.config import WorkplaceConfigAdapter adapter = WorkplaceConfigAdapter() account = await adapter.resolve_account("default") app_secret = account.get("app_secret", "") if app_secret and not _verify_signature(body, signature, app_secret): logger.warning("Webhook signature verification failed") raise HTTPException(status_code=403, detail="Invalid signature") try: payload = json.loads(body) except json.JSONDecodeError: logger.error("Webhook payload is not valid JSON") raise HTTPException(status_code=400, detail="Invalid JSON") from yuxi.channel.extensions.workplace.gateway import _get_webhook_queue queue = _get_webhook_queue() if queue is not None: entries = payload.get("entry", []) enqueued = 0 for entry in entries: for messaging_event in entry.get("messaging", []): await queue.put(messaging_event) enqueued += 1 for change in entry.get("changes", []): await queue.put(change) enqueued += 1 if enqueued == 0: await queue.put(payload) else: logger.debug("Webhook enqueued %d events", enqueued) return JSONResponse({"status": "ok"}) def _verify_signature(body: bytes, signature: str, app_secret: str) -> bool: if not signature or not app_secret: return False expected = hmac.new( app_secret.encode("utf-8"), body, hashlib.sha256, ).hexdigest() expected_header = f"sha256={expected}" return hmac.compare_digest(expected_header.encode("utf-8"), signature.encode("utf-8"))