Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This product includes software developed by AptS-1547 (https://github.com/AptS-1
This software uses the following third-party libraries that are distributed
under their own licenses:

- APScheduler (https://github.com/agronholm/apscheduler) - MIT License
- FastAPI (https://fastapi.tiangolo.com/) - MIT License
- Pydantic (https://pydantic-docs.helpmanual.io/) - MIT License
- Pydantic-settings (https://github.com/pydantic/pydantic-settings) - MIT License
Expand Down
283 changes: 249 additions & 34 deletions app/api/github_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from fastapi import APIRouter, Request, HTTPException

from app.core import GitHubWebhookHandler
from app.onebot import text, get_onebot_client
from app.botclient import BotClient
from app.models import MessageSegment
from app.models.config import get_settings

router = APIRouter()
Expand All @@ -33,12 +34,11 @@
config = get_settings()

@router.post("")
async def github_webhook(
request: Request,
): # pylint: disable=too-many-return-statements
async def github_webhook(request: Request):

Check notice on line 37 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L37

Too many branches (15/12) (too-many-branches)

Check notice on line 37 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L37

Too many return statements (8/6) (too-many-return-statements)

Check notice on line 37 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L37

Too many statements (56/50) (too-many-statements)

Check notice on line 37 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L37

Too many local variables (16/15) (too-many-locals)
"""处理 GitHub webhook 请求"""
# TODO: 为了后期使用自定义模版,作为临时过渡,需要完全重构

Check notice on line 39 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L39

TODO: 为了后期使用自定义模版,作为临时过渡,需要完全重构 (fixme)

onebot_client = get_onebot_client()
onebot_client = BotClient.get_client("onebot")

if not onebot_client:
logger.error("OneBot 客户端未初始化,无法处理请求")
Expand All @@ -54,6 +54,11 @@
logger.info("缺少 X-GitHub-Event 头,忽略")
return {"status": "ignored", "message": "缺少 X-GitHub-Event 头"}

payload = await request.json()
if not payload:
logger.info("请求体为空,忽略")
return {"status": "ignored", "message": "请求体为空"}

try:
await GitHubWebhookHandler.verify_signature(
request,
Expand All @@ -64,26 +69,27 @@
logger.info("签名验证失败: %s", e.detail)
return {"status": "ignored", "message": f"签名验证失败: {e.detail}"}

payload = await request.json()
if not payload:
logger.info("请求体为空,忽略")
return {"status": "ignored", "message": "请求体为空"}

repo_name = payload.get("repository", {}).get("full_name")
branch = payload.get("ref", "").replace("refs/heads/", "")

# 根据事件类型获取分支/PR/Issue信息用于匹配
match_info = {}
if event_type == "push":
match_info["branch"] = payload.get("ref", "").replace("refs/heads/", "")

matched_webhook = GitHubWebhookHandler.find_matching_webhook(
repo_name,
branch,
match_info.get("branch", ""),
event_type,
config.GITHUB_WEBHOOK
)

if not matched_webhook:
logger.info("找不到匹配的 webhook 配置: 仓库 %s, 分支 %s, 事件类型 %s", repo_name, branch, event_type)
logger.info("找不到匹配的 webhook 配置: 仓库 %s, 事件类型 %s", repo_name, event_type)
return {"status": "ignored", "message": "找不到匹配的 webhook 配置"}

# 处理不同类型的事件,暂时只支持 push 事件
message = None

# 处理不同类型的事件
if event_type == "push":
push_data = GitHubWebhookHandler.extract_push_data(payload)

Expand All @@ -93,37 +99,103 @@
push_data["pusher"],
push_data["commit_count"])

message = format_github_push_message(
repo_name=push_data["repo_name"],
branch=push_data["branch"],
pusher=push_data["pusher"],
commit_count=push_data["commit_count"],
commits=push_data["commits"]
)

elif event_type == "pull_request":
pr_data = GitHubWebhookHandler.extract_pull_request_data(payload)

logger.info("发现新的 pull_request 事件,来自 %s 仓库", pr_data["repo_name"])
logger.info("PR #%s, 操作: %s, 用户: %s",
pr_data["pull_request_number"],
pr_data["action"],
pr_data["user"])

message = format_github_pull_request_message(
repo_name=pr_data["repo_name"],
action=pr_data["action"],
pull_request=pr_data["pull_request"],
user=pr_data["user"]
)

elif event_type == "issues":
issue_data = GitHubWebhookHandler.extract_issue_data(payload)

logger.info("发现新的 issue 事件,来自 %s 仓库", issue_data["repo_name"])
logger.info("Issue #%s, 操作: %s, 用户: %s",
issue_data["issue_number"],
issue_data["action"],
issue_data["user"])

message = format_github_issue_message(
repo_name=issue_data["repo_name"],
action=issue_data["action"],
issue=issue_data["issue"],
user=issue_data["user"]
)

elif event_type == "release":
release_data = GitHubWebhookHandler.extract_release_data(payload)

logger.info("发现新的 release 事件,来自 %s 仓库", release_data["repo_name"])
logger.info("版本: %s, 操作: %s, 用户: %s",

Check notice on line 146 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L146

Trailing whitespace (trailing-whitespace)
release_data["release_tag"],

Check notice on line 147 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L147

Trailing whitespace (trailing-whitespace)
release_data["action"],

Check notice on line 148 in app/api/github_webhook.py

View check run for this annotation

codefactor.io / CodeFactor

app/api/github_webhook.py#L148

Trailing whitespace (trailing-whitespace)
release_data["user"])

message = format_github_release_message(
repo_name=release_data["repo_name"],
action=release_data["action"],
release=release_data["release"],
user=release_data["user"]
)

elif event_type == "issue_comment":
comment_data = GitHubWebhookHandler.extract_issue_comment_data(payload)

logger.info("发现新的 issue_comment 事件,来自 %s 仓库", comment_data["repo_name"])
logger.info("Issue #%s, 操作: %s, 用户: %s",
comment_data["issue_number"],
comment_data["action"],
comment_data["user"])

message = format_github_issue_comment_message(
repo_name=comment_data["repo_name"],
action=comment_data["action"],
comment=comment_data["comment"],
issue_number=comment_data["issue_number"],
user=comment_data["user"]
)
else:
logger.info("收到 %s 事件,但尚未实现处理逻辑", event_type)
return {"status": "ignored", "message": f"暂不处理 {event_type} 类型的事件"}

if message:
# 向配置的所有 OneBot 目标发送通知
for target in matched_webhook.ONEBOT:
logger.info("正在发送消息到 QQ %s %s", target.type, target.id)

message = format_github_push_message(
repo_name=push_data["repo_name"],
branch=push_data["branch"],
pusher=push_data["pusher"],
commit_count=push_data["commit_count"],
commits=push_data["commits"]
)

# 使用已有的客户端发送消息
await onebot_client.send_message(target.type, target.id, message)

return {"status": "success", "message": "处理 push 事件成功"}
return {"status": "success", "message": f"处理 {event_type} 事件成功"}

logger.info("收到 %s 事件,但尚未实现处理逻辑", event_type)
return {"status": "ignored", "message": f"暂不处理 {event_type} 类型的事件"}
return {"status": "failed", "message": "处理事件时发生错误"}


def format_github_push_message(repo_name, branch, pusher, commit_count, commits):
"""格式化 GitHub 推送消息"""

message = [
text("📢 GitHub 推送通知\n"),
text(f"仓库:{repo_name}\n"),
text(f"分支:{branch}\n"),
text(f"推送者:{pusher}\n"),
text(f"提交数量:{commit_count}\n\n"),
text("最新提交:\n")
MessageSegment.text("📢 GitHub 推送通知\n"),
MessageSegment.text(f"仓库:{repo_name}\n"),
MessageSegment.text(f"分支:{branch}\n"),
MessageSegment.text(f"推送者:{pusher}\n"),
MessageSegment.text(f"提交数量:{commit_count}\n\n"),
MessageSegment.text("最新提交:\n")
]

# 最多展示3条最新提交
Expand All @@ -132,6 +204,149 @@
commit_message = commit["message"].split("\n")[0] # 只取第一行
author = commit.get("author", {}).get("name", "未知")

message.append(text(f"[{short_id}] {commit_message} (by {author})\n"))
message.append(MessageSegment.text(f"[{short_id}] {commit_message} (by {author})\n"))

return message

def format_github_pull_request_message(repo_name, action, pull_request, user):
"""格式化 GitHub Pull Request 消息"""

# 针对不同动作定制消息内容
action_text = {
"opened": "创建了",
"closed": "关闭了" if not pull_request.get("merged") else "合并了",
"reopened": "重新打开了",
"assigned": "被分配了",
"unassigned": "被取消分配了",
"review_requested": "请求审核",
"review_request_removed": "取消审核请求",
"labeled": "被添加了标签",
"unlabeled": "被移除了标签",
"synchronize": "同步了",
}.get(action, action)

message = [
MessageSegment.text(f"📢 GitHub Pull Request {action_text}\n"),
MessageSegment.text(f"仓库:{repo_name}\n"),
MessageSegment.text(f"PR #{pull_request.get('number')}: {pull_request.get('title')}\n"),
MessageSegment.text(f"用户:{user}\n"),
MessageSegment.text(f"状态:{pull_request.get('state')}\n")
]

# 添加分支信息
base = pull_request.get("base", {}).get("ref", "")
head = pull_request.get("head", {}).get("ref", "")
if base and head:
message.append(MessageSegment.text(f"目标分支:{base} ← {head}\n"))

# 添加链接
if pull_request.get("html_url"):
message.append(MessageSegment.text(f"链接:{pull_request.get('html_url')}\n"))

return message

def format_github_issue_message(repo_name, action, issue, user):
"""格式化 GitHub Issue 消息"""

# 针对不同动作定制消息内容
action_text = {
"opened": "创建了",
"closed": "关闭了",
"reopened": "重新打开了",
"assigned": "被分配了",
"unassigned": "被取消分配了",
"labeled": "被添加了标签",
"unlabeled": "被移除了标签",
}.get(action, action)

message = [
MessageSegment.text(f"📢 GitHub Issue {action_text}\n"),
MessageSegment.text(f"仓库:{repo_name}\n"),
MessageSegment.text(f"Issue #{issue.get('number')}: {issue.get('title')}\n"),
MessageSegment.text(f"用户:{user}\n"),
MessageSegment.text(f"状态:{issue.get('state')}\n")
]

# 添加标签信息
labels = issue.get("labels", [])
if labels:
label_names = [label.get("name", "") for label in labels]
message.append(MessageSegment.text(f"标签:{', '.join(label_names)}\n"))

# 添加链接
if issue.get("html_url"):
message.append(MessageSegment.text(f"链接:{issue.get('html_url')}\n"))

return message

def format_github_release_message(repo_name, action, release, user):
"""格式化 GitHub Release 消息"""

# 针对不同动作定制消息内容
action_text = {
"published": "发布了",
"created": "创建了",
"edited": "编辑了",
"deleted": "删除了",
"prereleased": "预发布了",
"released": "正式发布了",
}.get(action, action)

tag_name = release.get("tag_name", "")
name = release.get("name", tag_name) if release.get("name") else tag_name

message = [
MessageSegment.text(f"📢 GitHub Release {action_text}\n"),
MessageSegment.text(f"仓库:{repo_name}\n"),
MessageSegment.text(f"版本:{name} ({tag_name})\n"),
MessageSegment.text(f"发布者:{user}\n")
]

# 添加预发布信息
if release.get("prerelease"):
message.append(MessageSegment.text("类型:预发布\n"))

# 添加发布时间
if release.get("published_at"):
published_time = release.get("published_at")
message.append(MessageSegment.text(f"发布时间:{published_time}\n"))

# 添加链接
if release.get("html_url"):
message.append(MessageSegment.text(f"链接:{release.get('html_url')}\n"))

return message

def format_github_issue_comment_message(repo_name, action, comment, issue_number, user):
"""格式化 GitHub Issue/PR 评论消息"""

# 针对不同动作定制消息内容
action_text = {
"created": "发表了",
"edited": "编辑了",
"deleted": "删除了",
}.get(action, action)

# 判断是PR还是Issue
issue_type = "PR" if comment.get("pull_request") else "Issue"

message = [
MessageSegment.text(f"📢 GitHub {issue_type}评论 {action_text}\n"),
MessageSegment.text(f"仓库:{repo_name}\n"),
MessageSegment.text(f"{issue_type} #{issue_number}\n"),
MessageSegment.text(f"用户:{user}\n")
]

# 添加评论内容预览
body = comment.get("body", "")
if body and action != "deleted":
# 截取评论内容,最多显示100个字符
preview = body[:100] + "..." if len(body) > 100 else body
preview = preview.replace("\n", " ")
message.append(MessageSegment.text(f"内容:{preview}\n"))

# 添加链接
if comment.get("html_url"):
message.append(MessageSegment.text(f"链接:{comment.get('html_url')}\n"))

return message
Loading