Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 77 additions & 0 deletions formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,80 @@ def format_pr_details(repo: str, pr_data: dict[str, Any]) -> str:

result += f"\n链接: {pr_data['html_url']}"
return result


def format_webhook_push_message(
repo: str,
payload: dict[str, Any],
sender: dict[str, Any] | None,
) -> str | None:
ref = payload.get("ref", "")
branch_or_tag = ref.split("/")[-1] if "/" in ref else ref

commits = payload.get("commits", [])
if not commits:
return None

actor = (sender or {}).get("login") or "未知"

message_lines = [
f"[GitHub Webhook] 仓库 {repo} 代码推送",
f"分支: {branch_or_tag}",
f"触发人: {actor}",
f"新增 {len(commits)} 个提交:"
]

for commit in commits[:3]:
msg = commit.get("message", "").split("\n")[0]
sha = commit.get("id", "")[:7]
message_lines.append(f"- {sha} {msg}")

if len(commits) > 3:
message_lines.append(f"... 等共 {len(commits)} 个提交")

compare_url = payload.get("compare")
if compare_url:
message_lines.append(f"链接: {compare_url}")

return "\n".join(message_lines)


def format_webhook_release_message(
repo: str,
action: str,
release: dict[str, Any],
sender: dict[str, Any] | None,
) -> str | None:
action_labels = {
"published": "发布了新版本",
"created": "创建了版本",
"edited": "更新了版本",
"deleted": "删除了版本",
"prereleased": "发布了预发布版本",
"released": "发布了正式版本",
}

label = action_labels.get(action)
if not label:
return None

actor = (sender or {}).get("login") or release.get("author", {}).get("login") or "未知"
tag_name = release.get("tag_name", "未知版本")
name = release.get("name") or tag_name

message_lines = [
f"[GitHub Webhook] 仓库 {repo} Release 更新",
f"版本: {name} ({tag_name})",
f"事件: {label}",
f"触发人: {actor}",
]

body = release.get("body", "")
if body and action in ["published", "released", "prereleased"]:
message_lines.append("发布说明:")
message_lines.append(truncate_text(body, limit=200))

if release.get("html_url"):
message_lines.append(f"链接: {release['html_url']}")

return "\n".join(message_lines)
153 changes: 118 additions & 35 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"https://api.github.com/repos/{repo}/readme" # 新增 README API URL
)
GITHUB_ISSUES_API_URL = "https://api.github.com/repos/{repo}/issues"
GITHUB_COMMITS_API_URL = "https://api.github.com/repos/{repo}/commits"
GITHUB_RELEASES_API_URL = "https://api.github.com/repos/{repo}/releases"
GITHUB_ISSUE_API_URL = "https://api.github.com/repos/{repo}/issues/{issue_number}"
GITHUB_PR_API_URL = "https://api.github.com/repos/{repo}/pulls/{pr_number}"
GITHUB_RATE_LIMIT_URL = "https://api.github.com/rate_limit"
Expand Down Expand Up @@ -409,7 +411,7 @@ async def _check_all_repos(self):
logger.error(f"检查仓库 {repo} 更新时出错: {e}")

async def _fetch_new_items(self, repo: str, last_check: str | None):
"""Fetch new issues and PRs from a repository since last check"""
"""Fetch new issues, PRs, commits, and releases from a repository since last check"""
if not last_check:
# If first time checking, just record current time and return empty list
# Store as UTC timestamp without timezone info to avoid comparison issues
Expand All @@ -431,54 +433,100 @@ async def _fetch_new_items(self, repo: str, last_check: str | None):
logger.debug(f"仓库 {repo} 的上次检查时间: {last_check_dt.isoformat()}")
new_items = []

# GitHub API returns both issues and PRs in the issues endpoint
async with aiohttp.ClientSession() as session:
# 1. Fetch Issues / PRs
try:
params = {
params_issues = {
"sort": "created",
"direction": "desc",
"state": "all",
"per_page": 10,
"since": last_check_dt.isoformat() + "Z",
}
Comment on lines +439 to 445
async with session.get(
GITHUB_ISSUES_API_URL.format(repo=repo),
params=params,
params=params_issues,
headers=self._get_github_headers(),
) as resp:
if resp.status == 200:
items = await resp.json()

for item in items:
# Convert GitHub's timestamp to naive UTC datetime for consistent comparison
github_timestamp = item["created_at"].replace("Z", "")
created_at = datetime.fromisoformat(github_timestamp)

# Always remove timezone info for comparison
created_at = created_at.replace(tzinfo=None)

logger.debug(
f"比较: 仓库 {repo} 的 item #{item['number']} 创建于 {created_at.isoformat()}, 上次检查: {last_check_dt.isoformat()}"
)

created_at = datetime.fromisoformat(github_timestamp).replace(tzinfo=None)
if created_at > last_check_dt:
logger.info(
f"发现新的 item #{item['number']} in {repo}"
)
logger.info(f"发现新的 item #{item.get('number')} in {repo}")
new_items.append(item)
else:
# Since items are sorted by creation time, we can break early
logger.debug(f"没有更多新 items in {repo}")
break
else:
text = await resp.text()
if len(text) > 100:
text = text[:100] + "..."
logger.error(
f"获取仓库 {repo} 的 Issue/PR 失败: {resp.status}: {text}"
)
logger.error(f"获取仓库 {repo} 的 Issue/PR 失败: {resp.status}: {text[:100]}")
except Exception as e:
logger.error(f"获取仓库 {repo} 的 Issue/PR 时出错: {e}")

# 2. Fetch Commits
try:
params_commits = {
"per_page": 100,
"since": last_check_dt.isoformat() + "Z",
}
async with session.get(
GITHUB_COMMITS_API_URL.format(repo=repo),
params=params_commits,
headers=self._get_github_headers(),
) as resp:
if resp.status == 200:
commits = await resp.json()
if isinstance(commits, list):
for commit in commits:
commit_date_str = commit.get("commit", {}).get("committer", {}).get("date", "")
if not commit_date_str:
continue
github_timestamp = commit_date_str.replace("Z", "")
created_at = datetime.fromisoformat(github_timestamp).replace(tzinfo=None)
if created_at > last_check_dt:
logger.info(f"发现新的 commit {commit.get('sha')[:7]} in {repo}")
commit["_astrbot_type"] = "commit"
# To pass branch info in notifier we can't easily get it here, but we can just say "代码推送"
new_items.append(commit)
else:
break
else:
text = await resp.text()
logger.error(f"获取仓库 {repo} 的 Commits 失败: {resp.status}: {text[:100]}")
except Exception as e:
logger.error(f"获取仓库 {repo} 的 Commits 时出错: {e}")

# 3. Fetch Releases
try:
params_releases = {"per_page": 5}
# releases API doesn't support 'since', we rely on sorting (default is by created_at desc)
async with session.get(
GITHUB_RELEASES_API_URL.format(repo=repo),
params=params_releases,
headers=self._get_github_headers(),
) as resp:
if resp.status == 200:
releases = await resp.json()
if isinstance(releases, list):
for release in releases:
release_date_str = release.get("published_at") or release.get("created_at") or ""
if not release_date_str:
continue
github_timestamp = release_date_str.replace("Z", "")
created_at = datetime.fromisoformat(github_timestamp).replace(tzinfo=None)
if created_at > last_check_dt:
logger.info(f"发现新的 release {release.get('tag_name')} in {repo}")
release["_astrbot_type"] = "release"
new_items.append(release)
else:
break
else:
text = await resp.text()
logger.error(f"获取仓库 {repo} 的 Releases 失败: {resp.status}: {text[:100]}")
except Exception as e:
logger.error(f"获取仓库 {repo} 的 Releases 时出错: {e}")

# Update the last check time to now (UTC without timezone info)
if new_items:
logger.info(f"找到 {len(new_items)} 个新的 items 在 {repo}")
Expand All @@ -493,9 +541,7 @@ async def _fetch_new_items(self, repo: str, last_check: str | None):

return new_items
except Exception as e:
logger.error(f"解析时间时出错: {e}")
# If we can't parse the time correctly, just return an empty list
# and update the last check time to prevent continuous errors
logger.error(f"解析时间/获取数据时出错: {e}", exc_info=True)
self.last_check_time[repo] = (
datetime.utcnow().replace(microsecond=0).isoformat()
)
Expand All @@ -515,13 +561,40 @@ async def _notify_subscribers(self, repo: str, new_items: list[dict[str, Any]]):
try:
# Create notification message
for item in new_items:
item_type = "PR" if "pull_request" in item else "Issue"
message = (
f"[GitHub 更新] 仓库 {repo} 有新的{item_type}:\n"
f"#{item['number']} {item['title']}\n"
f"作者: {item['user']['login']}\n"
f"链接: {item['html_url']}"
)
if "_astrbot_type" in item:
if item["_astrbot_type"] == "commit":
sha = item.get("sha", "")[:7]
msg = item.get("commit", {}).get("message", "").split("\n")[0]
author = item.get("commit", {}).get("author", {}).get("name", "未知")
url = item.get("html_url", "")
message = (
f"[GitHub 更新] 仓库 {repo} 有新的代码推送:\n"
f"- {sha} {msg}\n"
f"作者: {author}\n"
f"链接: {url}"
)
elif item["_astrbot_type"] == "release":
tag_name = item.get("tag_name", "未知版本")
name = item.get("name") or tag_name
author = item.get("author", {}).get("login", "未知")
url = item.get("html_url", "")
message = (
f"[GitHub 更新] 仓库 {repo} 发布了新版本:\n"
f"版本: {name} ({tag_name})\n"
f"发布者: {author}\n"
f"链接: {url}"
)
else:
# Fallback if unknown type
continue
else:
item_type = "PR" if "pull_request" in item else "Issue"
message = (
f"[GitHub 更新] 仓库 {repo} 有新的{item_type}:\n"
f"#{item['number']} {item['title']}\n"
f"作者: {item['user']['login']}\n"
f"链接: {item['html_url']}"
)

# Send message to subscriber
await self.context.send_message(
Expand Down Expand Up @@ -640,6 +713,16 @@ async def handle_webhook_event(
message = formatters.format_webhook_create_message(
repo_full_name, payload, sender
)
elif event_type == "push":
message = formatters.format_webhook_push_message(
repo_full_name, payload, sender
)
elif event_type == "release":
release = payload.get("release")
if isinstance(release, dict):
message = formatters.format_webhook_release_message(
repo_full_name, action, release, sender
)
else:
logger.debug(f"暂不处理的 GitHub Webhook 事件类型: {event_type}")
return
Expand Down
2 changes: 1 addition & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: astrbot_plugin_github_cards # 插件名称
desc: GitHub 相关链接自动发送 GitHub 仓库简介/issue/pr卡片、订阅 GitHub 仓库事件 # 插件描述
help:
version: 1.1.0 # 插件版本
version: 1.1.1 # 插件版本
author: Soulter # 插件作者
repo: https://github.com/Soulter/astrbot_plugin_github_cards
14 changes: 13 additions & 1 deletion webhook_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import hashlib
import hmac
import json
from typing import Any

from quart import Quart, Response, request
Expand Down Expand Up @@ -52,7 +53,18 @@ async def github_webhook():
return Response("missing event", status=400)

try:
data = await request.get_json()
if request.is_json:
data = await request.get_json()
else:
form_data = await request.form
if "payload" in form_data:
data = json.loads(form_data["payload"])
else:
data = None

if data is None:
logger.warning("GitHub Webhook 缺少有效的 payload 数据")
return Response("invalid payload", status=400)
except Exception:
logger.warning("GitHub Webhook JSON 解析失败")
return Response("invalid payload", status=400)
Expand Down