实现了 GitLab 全功能集成,包含 Webhook 处理、API 客户端、消息收发、会话管理、安全校验、限流器等完整模块,支持 Issues、Merge Requests、流水线等事件监听与交互。
195 lines
7.3 KiB
Python
195 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import PlainTextResponse
|
|
|
|
from yuxi.channel.extensions.gitlab.config import GitLabConfigAdapter
|
|
from yuxi.channel.extensions.gitlab.monitor import GitLabMonitor
|
|
from yuxi.channel.extensions.gitlab.webhook import GitLabWebhookHandler
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/webhook/gitlab", tags=["gitlab"])
|
|
|
|
_current_gateway: GitLabGateway | None = None
|
|
_target_queue: asyncio.Queue | None = None
|
|
|
|
|
|
def set_target_queue(queue: asyncio.Queue) -> None:
|
|
global _target_queue
|
|
_target_queue = queue
|
|
|
|
|
|
@router.post("/events")
|
|
async def gitlab_webhook_events(request: Request):
|
|
if _current_gateway is None:
|
|
return PlainTextResponse("", status_code=503)
|
|
|
|
gateway = _current_gateway
|
|
if gateway._queue is None or gateway._webhook_handler is None:
|
|
return PlainTextResponse("", status_code=503)
|
|
|
|
try:
|
|
body = await request.body()
|
|
except Exception:
|
|
logger.exception("GitLab webhook: read body failed")
|
|
return PlainTextResponse("", status_code=400)
|
|
|
|
headers = {k.lower(): v for k, v in request.headers.items()}
|
|
result = await gateway._webhook_handler.handle_webhook(body, headers)
|
|
|
|
status = result.get("status", "error")
|
|
if status == "unauthorized":
|
|
return PlainTextResponse("", status_code=401)
|
|
if status == "invalid_payload":
|
|
return PlainTextResponse("", status_code=400)
|
|
if status in ("duplicate", "self_message"):
|
|
return PlainTextResponse("ok")
|
|
|
|
if status == "ok" and gateway._monitor and _target_queue:
|
|
try:
|
|
unified = gateway._monitor.parse_webhook_to_unified(
|
|
result.get("payload", {}),
|
|
result.get("account_id", "default"),
|
|
result.get("event_type", ""),
|
|
)
|
|
if unified:
|
|
_target_queue.put_nowait(unified)
|
|
except asyncio.QueueFull:
|
|
logger.warning("GitLab target queue full, dropping message")
|
|
|
|
return PlainTextResponse("ok")
|
|
|
|
|
|
class GitLabGateway:
|
|
def __init__(self):
|
|
self._running = False
|
|
self._tasks: list[asyncio.Task] = []
|
|
self._account: dict = {}
|
|
self._queue: asyncio.Queue | None = None
|
|
self._webhook_handler: GitLabWebhookHandler | None = None
|
|
self._monitor: GitLabMonitor | None = None
|
|
|
|
async def start(self, ctx) -> object:
|
|
global _current_gateway
|
|
|
|
config = getattr(ctx, "config", {}) if ctx else {}
|
|
account_id = getattr(ctx, "account_id", "default")
|
|
|
|
config_adapter = GitLabConfigAdapter()
|
|
config_adapter.list_account_ids(config)
|
|
self._account = await config_adapter.resolve_account(account_id)
|
|
|
|
if not config_adapter.is_configured(self._account):
|
|
logger.warning("gitlab account %s not configured, skipping start", account_id)
|
|
return {"running": False, "reason": "not-configured"}
|
|
|
|
probe_result = await self._probe_connection(self._account)
|
|
if not probe_result.get("healthy"):
|
|
logger.warning("gitlab connection probe failed for account %s: %s", account_id, probe_result.get("reason"))
|
|
return {"running": False, "reason": f"probe-failed: {probe_result.get('reason', 'unknown')}"}
|
|
|
|
self._queue = asyncio.Queue(maxsize=1000)
|
|
self._webhook_handler = GitLabWebhookHandler(
|
|
webhook_token=self._account.get("webhook_token", ""),
|
|
bot_username=self._account.get("bot_username", ""),
|
|
signing_secret=self._account.get("webhook_signing_secret", ""),
|
|
)
|
|
self._monitor = GitLabMonitor()
|
|
|
|
await self._register_webhook_if_needed(self._account)
|
|
|
|
self._running = True
|
|
_current_gateway = self
|
|
|
|
logger.info(
|
|
"gitlab gateway started for project %s (id=%s) on %s",
|
|
self._account.get("project_path"),
|
|
self._account.get("project_id"),
|
|
self._account.get("base_url"),
|
|
)
|
|
return {
|
|
"running": True,
|
|
"account_id": account_id,
|
|
"project_id": self._account.get("project_id"),
|
|
"base_url": self._account.get("base_url"),
|
|
}
|
|
|
|
async def stop(self, ctx) -> None:
|
|
global _current_gateway
|
|
|
|
self._running = False
|
|
|
|
for task in self._tasks:
|
|
task.cancel()
|
|
self._tasks.clear()
|
|
|
|
self._queue = None
|
|
self._webhook_handler = None
|
|
self._monitor = None
|
|
_current_gateway = None
|
|
|
|
logger.info("gitlab gateway stopped")
|
|
|
|
@staticmethod
|
|
async def _probe_connection(account: dict) -> dict:
|
|
try:
|
|
from yuxi.channel.extensions.gitlab.api import AsyncGitLabClient
|
|
|
|
client = AsyncGitLabClient(
|
|
base_url=account.get("base_url", "https://gitlab.com"),
|
|
private_token=account.get("private_token", ""),
|
|
ssl_verify=account.get("ssl_verify", True),
|
|
)
|
|
version = await client.probe()
|
|
if version:
|
|
return {"healthy": True, "version": version.get("version", "")}
|
|
return {"healthy": False, "reason": "version probe returned empty"}
|
|
except Exception as e:
|
|
return {"healthy": False, "reason": str(e)}
|
|
|
|
@staticmethod
|
|
async def _register_webhook_if_needed(account: dict) -> None:
|
|
project_id = account.get("project_id", 0)
|
|
webhook_path = account.get("webhook_path", "/api/webhook/gitlab")
|
|
|
|
if not project_id:
|
|
return
|
|
|
|
try:
|
|
from yuxi.channel.extensions.gitlab.api import AsyncGitLabClient
|
|
|
|
client = AsyncGitLabClient(
|
|
base_url=account.get("base_url", "https://gitlab.com"),
|
|
private_token=account.get("private_token", ""),
|
|
ssl_verify=account.get("ssl_verify", True),
|
|
)
|
|
hooks = await client.list_hooks(project_id)
|
|
existing = [h for h in hooks if getattr(h, "url", "").endswith(webhook_path)]
|
|
|
|
if not existing:
|
|
logger.info("gitlab webhook not found for project %s, creating...", project_id)
|
|
hook_data = {
|
|
"url": f"https://{account.get('webhook_domain', 'yuxi.example.com')}{webhook_path}",
|
|
"push_events": True,
|
|
"issues_events": account.get("listen_issues", True),
|
|
"merge_requests_events": account.get("listen_merge_requests", True),
|
|
"note_events": account.get("listen_notes", True),
|
|
"pipeline_events": account.get("listen_pipelines", True),
|
|
"deployment_events": account.get("listen_deployments", True),
|
|
"job_events": True,
|
|
"tag_push_events": True,
|
|
"releases_events": True,
|
|
"token": account.get("webhook_token", ""),
|
|
"enable_ssl_verification": True,
|
|
}
|
|
await client.create_hook(project_id, hook_data)
|
|
logger.info("gitlab webhook created for project %s", project_id)
|
|
else:
|
|
logger.info("gitlab webhook already exists for project %s", project_id)
|
|
except Exception:
|
|
logger.exception("gitlab webhook registration failed, manual setup may be required")
|