该提交实现了完整的ClickUp聊天渠道插件,包含以下核心功能: 1. 基础的@提及提取与格式化能力 2. 账号配对与会话管理 3. 消息流与流式回复支持 4. 重试限流与消息去重 5. 富文本格式转换与内容 sanitize 6. 安全策略与配置校验 7. Webhook接收与自动轮询回退 8. 消息收发、编辑、删除与回复 9. 频道与私信管理、反应功能 10. 完整的状态监控与健康检查
72 lines
2.2 KiB
Python
72 lines
2.2 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from yuxi.channel.extensions.clickup.config import _env_or_config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/webhook/clickup", tags=["clickup"])
|
|
|
|
|
|
@router.post("/automation")
|
|
async def receive_automation(request: Request):
|
|
headers = request.headers
|
|
body = await request.body()
|
|
|
|
if not body:
|
|
logger.warning("ClickUp webhook received empty body")
|
|
return JSONResponse({"status": "ok"})
|
|
|
|
if not _verify_webhook_secret(headers, body):
|
|
logger.warning("ClickUp webhook secret verification failed")
|
|
raise HTTPException(status_code=403, detail="Invalid webhook secret")
|
|
|
|
try:
|
|
payload = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
logger.error("ClickUp webhook: invalid JSON payload")
|
|
return JSONResponse({"status": "error", "reason": "invalid-json"}, status_code=400)
|
|
|
|
logger.debug("ClickUp webhook received: %s", json.dumps(payload, indent=2, ensure_ascii=False))
|
|
|
|
from yuxi.channel.extensions.clickup.gateway import _get_webhook_queue
|
|
|
|
queue = _get_webhook_queue()
|
|
if queue is not None:
|
|
await queue.put(payload)
|
|
|
|
return JSONResponse({"status": "ok"})
|
|
|
|
|
|
def _verify_webhook_secret(headers, raw_body: bytes | None = None) -> bool:
|
|
expected = _env_or_config("webhook_secret")
|
|
if not expected:
|
|
logger.warning("CLICKUP_WEBHOOK_SECRET not set, skipping webhook secret verification")
|
|
return True
|
|
|
|
signature_header = headers.get("X-Signature")
|
|
if signature_header and raw_body is not None:
|
|
return _verify_signature(raw_body, expected, signature_header)
|
|
|
|
provided = headers.get("Authorization", "").replace("Bearer ", "").strip()
|
|
if not provided:
|
|
provided = headers.get("X-ClickUp-Secret", "").strip()
|
|
|
|
return hmac.compare_digest(provided, expected)
|
|
|
|
|
|
def _verify_signature(raw_body: bytes, secret: str, signature_header: str | None) -> bool:
|
|
if not signature_header:
|
|
return False
|
|
expected = hmac.new(
|
|
secret.encode("utf-8"),
|
|
raw_body,
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
return hmac.compare_digest(expected, signature_header)
|