reposync/issue_sync_module/webhook/webhook_handler.py

385 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Webhook处理器 - 自动同步Gitee和GitLink之间的Issues
支持防循环机制,避免无限同步
"""
import time
import json
import hashlib
import requests
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
# 兼容导入优先使用环境变量后备使用src模块
try:
from src.utils.logger import logger
except ImportError:
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class LoopDetector:
"""循环检测器 - 防止无限同步循环"""
def __init__(self, window_minutes: int = 5):
self.window_minutes = window_minutes
self.recent_syncs = {} # {issue_key: timestamp}
def _get_issue_key(self, platform: str, org: str, repo: str, issue_title: str) -> str:
"""生成Issue的唯一标识"""
return f"{platform}:{org}/{repo}:{issue_title}"
def should_sync(self, platform: str, org: str, repo: str, issue_title: str) -> bool:
"""检查是否应该同步(避免循环)"""
issue_key = self._get_issue_key(platform, org, repo, issue_title)
current_time = datetime.now()
# 清理过期记录
self._cleanup_old_records(current_time)
# 检查是否在时间窗口内已经同步过
if issue_key in self.recent_syncs:
last_sync = self.recent_syncs[issue_key]
if current_time - last_sync < timedelta(minutes=self.window_minutes):
logger.info(f"🔄 跳过同步 - Issue '{issue_title}'{self.window_minutes} 分钟内已同步过")
return False
# 记录本次同步
self.recent_syncs[issue_key] = current_time
logger.info(f"✅ 允许同步 - Issue '{issue_title}' 可以进行同步")
return True
def _cleanup_old_records(self, current_time: datetime):
"""清理过期的同步记录"""
cutoff_time = current_time - timedelta(minutes=self.window_minutes * 2)
expired_keys = [
key for key, timestamp in self.recent_syncs.items()
if timestamp < cutoff_time
]
for key in expired_keys:
del self.recent_syncs[key]
class WebhookHandler:
"""Webhook处理器"""
def __init__(self):
self.loop_detector = LoopDetector(window_minutes=5)
self.sync_api_url = "http://localhost:8002/sync/immediate"
# Webhook密码验证
self.webhook_password = "hzk200407140238"
# 仓库配置
self.repo_config = {
"gitee": {
"org": "ttk00",
"repo": "testdemo"
},
"gitlink": {
"org": "qinxinqi",
"repo": "testdemo"
}
}
def detect_platform(self, webhook_data: Dict[str, Any], headers: Dict[str, str] = None) -> Optional[str]:
"""检测webhook来源平台"""
# 检测Gitee webhook - 通过headers
if headers:
if 'x-gitee-event' in headers or 'x-git-oschina-event' in headers:
logger.info(f"🔍 检测到Gitee webhook (通过headers)")
return "gitee"
# 检测GitLink webhook - 通过headers (GitLink使用Gitea/Gogs格式)
if 'x-gitea-event' in headers or 'x-gogs-event' in headers or 'x-github-event' in headers:
logger.info(f"🔍 检测到GitLink webhook (通过headers)")
return "gitlink"
# 检测Gitee webhook - 通过数据结构
if 'repository' in webhook_data and 'html_url' in webhook_data.get('repository', {}):
repo_url = webhook_data['repository']['html_url']
if 'gitee.com' in repo_url:
logger.info(f"🔍 检测到Gitee webhook: {repo_url}")
return "gitee"
# 检测Gitee webhook - 通过project字段
if 'project' in webhook_data and 'html_url' in webhook_data.get('project', {}):
project_url = webhook_data['project']['html_url']
if 'gitee.com' in project_url:
logger.info(f"🔍 检测到Gitee webhook: {project_url}")
return "gitee"
# 检测GitLink webhook - 通过project字段和mirror_url
if 'project' in webhook_data and 'identifier' in webhook_data.get('project', {}):
project = webhook_data['project']
mirror_url = project.get('mirror_url', '')
if 'gitee.com' in mirror_url:
logger.info(f"🔍 检测到GitLink webhook (镜像自Gitee): {project.get('identifier', 'unknown')}")
return "gitlink"
else:
logger.info(f"🔍 检测到GitLink webhook: {project.get('identifier', 'unknown')}")
return "gitlink"
# 通过User-Agent检测
if headers and 'user-agent' in headers:
user_agent = headers['user-agent'].lower()
if 'git-oschina-hook' in user_agent:
return "gitee"
elif 'gitlink' in user_agent:
return "gitlink"
logger.warning(f"⚠️ 无法识别webhook来源平台数据结构: {list(webhook_data.keys())}")
if headers:
logger.warning(f"⚠️ Headers: {list(headers.keys())}")
return None
def extract_issue_info(self, webhook_data: Dict[str, Any], platform: str) -> Optional[Dict[str, str]]:
"""提取Issue信息"""
try:
if platform == "gitee":
# Gitee webhook结构 - 支持新格式
issue = webhook_data.get('issue', {})
return {
"title": issue.get('title', webhook_data.get('title', '')),
"action": webhook_data.get('action', ''),
"number": str(issue.get('number', webhook_data.get('iid', ''))),
"state": issue.get('state', webhook_data.get('state', '')),
"body": issue.get('body', issue.get('description', '')),
"user": issue.get('user', {}).get('login', '')
}
elif platform == "gitlink":
# GitLink webhook结构 - 支持新格式
issue = webhook_data.get('issue', {})
action = webhook_data.get('action', '')
# 处理GitLink的状态信息
status = issue.get('status', {})
status_name = status.get('name', '') if isinstance(status, dict) else issue.get('status_name', '')
return {
"title": issue.get('subject', ''),
"action": action,
"number": str(issue.get('id', issue.get('project_issues_index', ''))),
"state": status_name,
"body": issue.get('description', ''),
"user": issue.get('author', {}).get('login', ''),
"event_type": webhook_data.get('journal', {}).get('notes', '') if 'journal' in webhook_data else ''
}
except Exception as e:
logger.error(f"❌ 提取Issue信息失败: {str(e)}")
return None
return None
def verify_webhook_password(self, webhook_data: Dict[str, Any], headers: Dict[str, str] = None) -> bool:
"""验证webhook密码"""
# 检查数据中的密码字段
if 'password' in webhook_data:
provided_password = webhook_data['password']
if provided_password == self.webhook_password:
logger.info("✅ Webhook密码验证通过")
return True
else:
logger.warning(f"❌ Webhook密码验证失败: 提供的密码不匹配")
return False
# 检查headers中的token
if headers:
gitee_token = headers.get('x-gitee-token', '')
if gitee_token == self.webhook_password:
logger.info("✅ Gitee Token验证通过")
return True
logger.warning("⚠️ 未找到有效的密码或token跳过验证")
return True # 如果没有密码字段,暂时允许通过
def should_trigger_sync(self, webhook_data: Dict[str, Any], platform: str, headers: Dict[str, str] = None) -> bool:
"""判断是否应该触发同步"""
# 检查是否是Issue相关事件
if 'issue' not in webhook_data:
logger.info("📋 非Issue事件跳过同步")
return False
# 检查事件类型 - GitLink可能是评论事件
if headers:
event_type = headers.get('x-gitea-event', headers.get('x-gogs-event', headers.get('x-github-event', '')))
if event_type == 'issue_comment':
logger.info("📋 Issue评论事件暂不触发同步")
return False
# 检查动作类型
action = webhook_data.get('action', '')
valid_actions = ['opened', 'open', 'created', 'edited', 'updated', 'closed', 'reopened']
if action not in valid_actions:
logger.info(f"📋 动作 '{action}' 不需要同步")
return False
# 提取Issue信息
issue_info = self.extract_issue_info(webhook_data, platform)
if not issue_info or not issue_info.get('title'):
logger.warning("⚠️ 无法提取Issue标题跳过同步")
return False
# 检查是否是同步机器人创建的Issue防循环
if self._is_sync_created_issue(webhook_data, platform):
logger.info(f"🤖 检测到同步机器人创建的Issue跳过同步")
return False
# 使用循环检测器
source_config = self.repo_config[platform]
return self.loop_detector.should_sync(
platform,
source_config["org"],
source_config["repo"],
issue_info["title"]
)
def _is_sync_created_issue(self, webhook_data: Dict[str, Any], platform: str) -> bool:
"""检查是否是同步机器人创建的Issue"""
if platform == "gitee":
# 检查Gitee的用户信息
issue = webhook_data.get('issue', {})
user = issue.get('user', {})
username = user.get('login', '').lower()
# 如果是同步相关的用户名,认为是机器人操作
sync_usernames = ['sync-bot', 'reposync', 'auto-sync']
if any(sync_name in username for sync_name in sync_usernames):
return True
elif platform == "gitlink":
# 检查GitLink的用户信息
issue = webhook_data.get('issue', {})
author = issue.get('author', {})
username = author.get('login', '').lower()
# 如果是同步相关的用户名,认为是机器人操作
sync_usernames = ['sync-bot', 'reposync', 'auto-sync']
if any(sync_name in username for sync_name in sync_usernames):
return True
return False
def call_sync_api(self, source_platform: str) -> Dict[str, Any]:
"""调用同步API"""
# 根据源平台确定目标平台
if source_platform == "gitee":
target_platform = "gitlink"
source_config = self.repo_config["gitee"]
target_config = self.repo_config["gitlink"]
elif source_platform == "gitlink":
target_platform = "gitee"
source_config = self.repo_config["gitlink"]
target_config = self.repo_config["gitee"]
else:
return {"success": False, "message": f"不支持的源平台: {source_platform}"}
# 构建同步请求
sync_request = {
"source_platform": source_platform,
"source_org": source_config["org"],
"source_repo": source_config["repo"],
"target_platform": target_platform,
"target_org": target_config["org"],
"target_repo": target_config["repo"],
"sync_comments": False,
"sync_milestones": True,
"update_existing": True,
"enable_deletion": False
}
try:
logger.info(f"🚀 调用同步API: {source_platform}{target_platform}")
logger.info(f"📋 同步参数: {json.dumps(sync_request, indent=2, ensure_ascii=False)}")
response = requests.post(
self.sync_api_url,
json=sync_request,
headers={"Content-Type": "application/json"},
timeout=150 # 增加到150秒适应2分钟左右的同步时间
)
if response.status_code == 200:
result = response.json()
logger.info(f"✅ 同步API调用成功: {result}")
return result
else:
error_msg = f"同步API调用失败: HTTP {response.status_code}"
logger.error(f"{error_msg}")
return {"success": False, "message": error_msg}
except requests.exceptions.Timeout:
error_msg = "同步API调用超时"
logger.error(f"{error_msg}")
return {"success": False, "message": error_msg}
except Exception as e:
error_msg = f"同步API调用异常: {str(e)}"
logger.error(f"{error_msg}")
return {"success": False, "message": error_msg}
def process_webhook(self, webhook_data: Dict[str, Any], headers: Dict[str, str] = None) -> Dict[str, Any]:
"""处理webhook请求"""
logger.info("=" * 60)
logger.info("🎣 收到新的Webhook请求")
logger.info(f"📅 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 验证密码
if not self.verify_webhook_password(webhook_data, headers):
return {
"success": False,
"message": "Webhook密码验证失败",
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
}
# 检测平台
platform = self.detect_platform(webhook_data, headers)
if not platform:
return {
"success": False,
"message": "无法识别webhook来源平台",
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
}
logger.info(f"🔍 识别平台: {platform}")
# 判断是否需要同步
if not self.should_trigger_sync(webhook_data, platform, headers):
return {
"success": True,
"message": f"Webhook已接收但不需要触发同步",
"platform": platform,
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
}
# 提取Issue信息用于日志
issue_info = self.extract_issue_info(webhook_data, platform)
if issue_info:
logger.info(f"📋 Issue信息: {issue_info['title']} (动作: {issue_info['action']}, 用户: {issue_info['user']})")
# 调用同步API
sync_result = self.call_sync_api(platform)
logger.info("=" * 60)
return {
"success": True,
"message": f"Webhook处理完成: {platform}{'gitlink' if platform == 'gitee' else 'gitee'}",
"platform": platform,
"issue_info": issue_info,
"sync_result": sync_result,
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
}