ForcePilot/backend/package/yuxi/channel/extensions/gitlab/webhook.py
Kris 4a4e256955 feat(gitlab): 新增完整 GitLab 渠道插件支持
实现了 GitLab 全功能集成,包含 Webhook 处理、API 客户端、消息收发、会话管理、安全校验、限流器等完整模块,支持 Issues、Merge Requests、流水线等事件监听与交互。
2026-05-21 10:49:01 +08:00

108 lines
3.4 KiB
Python

from __future__ import annotations
import hashlib
import hmac
import json
import logging
from yuxi.channel.extensions.gitlab.dedupe import GitLabDeduplicator
logger = logging.getLogger(__name__)
class GitLabWebhookHandler:
MAX_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2 MB
def __init__(self, webhook_token: str, bot_username: str, signing_secret: str = ""):
self._token = webhook_token
self._signing_secret = signing_secret
self._bot_username = bot_username
self._deduplicator = GitLabDeduplicator(max_size=10000)
def _verify_hmac(self, body: bytes, headers: dict) -> bool:
sig_parts = headers.get("webhook-signature", "").split(",")
sig_map = {}
for part in sig_parts:
if "=" in part:
k, v = part.split("=", 1)
sig_map[k.strip()] = v.strip()
t = sig_map.get("t", "")
v1 = sig_map.get("v1", "")
if not t or not v1:
return False
signed_payload = f"{t}.{body.decode('utf-8', errors='replace')}"
expected = hmac.new(
self._signing_secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, v1)
def verify_token(self, token_header: str | None) -> bool:
if not token_header or not self._token:
return False
return hmac.compare_digest(token_header, self._token)
async def handle_webhook(self, body: bytes, headers: dict) -> dict:
if len(body) > self.MAX_PAYLOAD_SIZE:
logger.warning(
"GitLab webhook: payload too large (%d bytes > %d)",
len(body),
self.MAX_PAYLOAD_SIZE,
)
return {"status": "payload_too_large"}
event_type = headers.get("X-Gitlab-Event", "")
event_uuid = headers.get("X-Gitlab-Event-Uuid", "")
authorized = False
if self._signing_secret and headers.get("webhook-signature"):
authorized = self._verify_hmac(body, headers)
if not authorized:
token_header = headers.get("X-Gitlab-Token", "")
authorized = self.verify_token(token_header)
if not authorized:
logger.warning("GitLab webhook: authentication failed")
return {"status": "unauthorized"}
try:
payload = json.loads(body)
except json.JSONDecodeError:
logger.error("GitLab webhook: invalid JSON payload")
return {"status": "invalid_payload"}
object_kind = payload.get("object_kind", "")
if not object_kind and event_type not in (
"Push Hook",
"Tag Push Hook",
"Wiki Page Hook",
"Feature Flag Hook",
"Release Hook",
"Milestone Hook",
"Emoji Hook",
"Vulnerability Hook",
):
return {"status": "unknown_event"}
if self._deduplicator.is_duplicate(payload, event_uuid):
logger.debug("GitLab webhook: duplicate event %s", event_uuid)
return {"status": "duplicate"}
user = payload.get("user", {})
username = user.get("username", "")
if username and username == self._bot_username:
return {"status": "self_message"}
return {
"status": "ok",
"object_kind": object_kind,
"event_type": event_type,
"payload": payload,
}