from __future__ import annotations import asyncio from fastapi import APIRouter, Request, status from fastapi.responses import JSONResponse from yuxi.channels.manager import channel_manager from yuxi.channels.registry import BUILTIN_ADAPTERS from yuxi.utils.logging_config import logger webhook = APIRouter(prefix="/webhook", tags=["webhook"]) @webhook.post("/feishu") async def feishu_webhook(request: Request): adapter = channel_manager._adapters.get("feishu") if not adapter: return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"code": 503, "data": None, "message": "Feishu channel is not running"}, ) body = await request.body() headers = dict(request.headers) source_ip = request.client.host if request.client else "default" try: from yuxi.channels.adapters.feishu.webhook_server import handle_feishu_webhook result = await handle_feishu_webhook(body, headers, adapter, source_ip) code = result.get("code", 0) if code == 0: response_content = {"code": 0, "data": None, "message": "ok"} if "challenge" in result: response_content["challenge"] = result["challenge"] return JSONResponse(content=response_content) if code == 413: return JSONResponse( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, content={"code": 413, "message": result.get("message", "")}, ) if code == 403: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, content={"code": 403, "message": result.get("message", "")}, ) return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={"code": 400, "message": result.get("message", "")}, ) except Exception as e: logger.error(f"[FeishuWebhook] Handler error: {e}") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"code": 500, "message": str(e)}, ) @webhook.post("/{channel_type}") async def webhook_dispatch(channel_type: str, request: Request): adapter = channel_manager._adapters.get(channel_type) adapter_cls = BUILTIN_ADAPTERS.get(channel_type) if not adapter: if adapter_cls and adapter_cls.webhook_path: return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"code": 503, "data": None, "message": f"Channel '{channel_type}' is not running"}, ) return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={"code": 404, "data": None, "message": f"Webhook channel '{channel_type}' not found"}, ) body = await request.body() headers = dict(request.headers) source_ip = request.client.host if request.client else "unknown" adapter._last_webhook_headers = headers adapter._last_source_ip = source_ip max_body_bytes = getattr(adapter, "_MAX_WEBHOOK_BODY_BYTES", 256 * 1024) if len(body) > max_body_bytes: logger.warning(f"Webhook body too large for {channel_type}: {len(body)} > {max_body_bytes}") return JSONResponse( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, content={"code": 413, "message": "Request body too large"}, ) try: if not await adapter.verify_webhook_signature(headers, body): return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, content={"code": 403, "data": None, "message": "Invalid webhook signature"}, ) except NotImplementedError: pass import json try: body_data = json.loads(body) except json.JSONDecodeError as e: logger.error(f"Webhook JSON decode failed for {channel_type}: {e}") return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={"code": 400, "data": None, "message": f"Invalid JSON payload: {e}"}, ) try: if hasattr(adapter, "handle_webhook"): handle_timeout = getattr(adapter, "_WEBHOOK_HANDLE_TIMEOUT_SECONDS", 30) message = await asyncio.wait_for(adapter.handle_webhook(body_data), timeout=handle_timeout) if message is None or isinstance(message, int): if isinstance(message, int): return JSONResponse( status_code=message, content={"code": message, "message": "Webhook rejected"}, ) return JSONResponse(content={"code": 0, "data": None, "message": "ok"}) asyncio.create_task(adapter._handle_message(message)) return JSONResponse(content={"code": 0, "data": None, "message": "ok"}) else: message = adapter.normalize_inbound(body_data) except asyncio.TimeoutError: logger.error(f"Webhook processing timed out for {channel_type}") return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"code": 503, "message": "Webhook processing timed out"}, ) except Exception as e: logger.error(f"Failed to parse webhook for {channel_type}: {e}") return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={"code": 400, "data": None, "message": f"Failed to parse webhook payload: {e}"}, ) asyncio.create_task(adapter._handle_message(message)) return JSONResponse(content={"code": 0, "data": None, "message": "ok"})