Update main.py
This commit is contained in:
parent
6dc75cd4ed
commit
3a6aef8ed7
222
main.py
222
main.py
@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -16,15 +17,17 @@ SUBSCRIPTION_FILE = "data/astrbot_plugin_github_sub_subscriptions.json"
|
|||||||
DEFAULT_REPO_FILE = "data/astrbot_plugin_github_sub_default_repos.json"
|
DEFAULT_REPO_FILE = "data/astrbot_plugin_github_sub_default_repos.json"
|
||||||
|
|
||||||
GITHUB_URL_PATTERN = r"https://github\.com/[\w\-]+/[\w\-]+(?:/(pull|issues)/\d+)?"
|
GITHUB_URL_PATTERN = r"https://github\.com/[\w\-]+/[\w\-]+(?:/(pull|issues)/\d+)?"
|
||||||
|
GITHUB_REPO_OPENGRAPH = "https://opengraph.githubassets.com/{hash}/{appendix}"
|
||||||
GITHUB_API_URL = "https://api.github.com/repos/{repo}"
|
GITHUB_API_URL = "https://api.github.com/repos/{repo}"
|
||||||
GITHUB_ISSUES_API_URL = "https://api.github.com/repos/{repo}/issues"
|
GITHUB_ISSUES_API_URL = "https://api.github.com/repos/{repo}/issues"
|
||||||
|
GITHUB_RELEASES_API_URL = "https://api.github.com/repos/{repo}/releases" # 新增Release API
|
||||||
|
|
||||||
|
|
||||||
@register(
|
@register(
|
||||||
"astrbot_plugin_github_sub",
|
"astrbot_plugin_github_sub",
|
||||||
"XieMu",
|
"XieMu",
|
||||||
"GitHub仓库订阅插件",
|
"GitHub仓库订阅插件",
|
||||||
"1.0.0",
|
"1.1.0", # 版本号升级
|
||||||
"https://github.com/xiemu-c/astrbot_plugin_github_sub",
|
"https://github.com/xiemu-c/astrbot_plugin_github_sub",
|
||||||
)
|
)
|
||||||
class MyPlugin(Star):
|
class MyPlugin(Star):
|
||||||
@ -33,15 +36,22 @@ class MyPlugin(Star):
|
|||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
self.subscriptions = self._load_subscriptions()
|
self.subscriptions = self._load_subscriptions()
|
||||||
self.default_repos = self._load_default_repos()
|
self.default_repos = self._load_default_repos()
|
||||||
self.last_check_time = {} # 存储每个仓库的最后检查时间
|
# 修改:区分issues和releases的检查时间
|
||||||
|
self.last_check_time = {
|
||||||
|
"issues": {}, # 记录Issue/PR的最后检查时间
|
||||||
|
"releases": {} # 记录Release的最后检查时间
|
||||||
|
}
|
||||||
self.use_lowercase = self.config.get("use_lowercase_repo", True)
|
self.use_lowercase = self.config.get("use_lowercase_repo", True)
|
||||||
self.github_token = self.config.get("github_token", "")
|
self.github_token = self.config.get("github_token", "")
|
||||||
self.check_interval = self.config.get("check_interval", 30)
|
self.check_interval = self.config.get("check_interval", 30)
|
||||||
|
# 新增:是否包含预发布版本的配置
|
||||||
|
self.include_prereleases = self.config.get("include_prereleases", False)
|
||||||
|
|
||||||
# 启动后台检查更新任务
|
# 启动后台检查更新任务
|
||||||
self.task = asyncio.create_task(self._check_updates_periodically())
|
self.task = asyncio.create_task(self._check_updates_periodically())
|
||||||
logger.info(
|
logger.info(
|
||||||
f"GitHub 订阅插件初始化完成,检查间隔: {self.check_interval}分钟"
|
f"GitHub 订阅插件初始化完成,检查间隔: {self.check_interval}分钟,"
|
||||||
|
f"是否包含预发布版本: {self.include_prereleases}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_subscriptions(self) -> Dict[str, List[str]]:
|
def _load_subscriptions(self) -> Dict[str, List[str]]:
|
||||||
@ -113,7 +123,7 @@ class MyPlugin(Star):
|
|||||||
|
|
||||||
@filter.command("ghsub")
|
@filter.command("ghsub")
|
||||||
async def subscribe_repo(self, event: AstrMessageEvent, repo: str):
|
async def subscribe_repo(self, event: AstrMessageEvent, repo: str):
|
||||||
"""订阅GitHub仓库的Issue和PR。例如: /ghsub Soulter/AstrBot"""
|
"""订阅GitHub仓库的Issue、PR和Release。例如: /ghsub Soulter/AstrBot"""
|
||||||
if not self._is_valid_repo(repo):
|
if not self._is_valid_repo(repo):
|
||||||
yield event.plain_result("请提供有效的仓库名,格式为: 用户名/仓库名")
|
yield event.plain_result("请提供有效的仓库名,格式为: 用户名/仓库名")
|
||||||
return
|
return
|
||||||
@ -150,9 +160,10 @@ class MyPlugin(Star):
|
|||||||
self._save_subscriptions()
|
self._save_subscriptions()
|
||||||
|
|
||||||
# 为新订阅获取初始状态
|
# 为新订阅获取初始状态
|
||||||
await self._fetch_new_items(normalized_repo, None)
|
await self._fetch_new_items(repo, None)
|
||||||
|
await self._fetch_new_releases(repo, None) # 初始化Release检查时间
|
||||||
|
|
||||||
yield event.plain_result(f"成功订阅仓库 {display_name} 的Issue和PR更新")
|
yield event.plain_result(f"成功订阅仓库 {display_name} 的Issue、PR和Release更新")
|
||||||
else:
|
else:
|
||||||
yield event.plain_result(f"你已经订阅了仓库 {display_name}")
|
yield event.plain_result(f"你已经订阅了仓库 {display_name}")
|
||||||
|
|
||||||
@ -174,6 +185,11 @@ class MyPlugin(Star):
|
|||||||
unsubscribed.append(repo_name)
|
unsubscribed.append(repo_name)
|
||||||
if not subscribers:
|
if not subscribers:
|
||||||
del self.subscriptions[repo_name]
|
del self.subscriptions[repo_name]
|
||||||
|
# 移除检查时间记录
|
||||||
|
if repo_name in self.last_check_time["issues"]:
|
||||||
|
del self.last_check_time["issues"][repo_name]
|
||||||
|
if repo_name in self.last_check_time["releases"]:
|
||||||
|
del self.last_check_time["releases"][repo_name]
|
||||||
|
|
||||||
if unsubscribed:
|
if unsubscribed:
|
||||||
self._save_subscriptions()
|
self._save_subscriptions()
|
||||||
@ -206,6 +222,10 @@ class MyPlugin(Star):
|
|||||||
self.subscriptions[normalized_repo].remove(subscriber_id)
|
self.subscriptions[normalized_repo].remove(subscriber_id)
|
||||||
if not self.subscriptions[normalized_repo]:
|
if not self.subscriptions[normalized_repo]:
|
||||||
del self.subscriptions[normalized_repo]
|
del self.subscriptions[normalized_repo]
|
||||||
|
if normalized_repo in self.last_check_time["issues"]:
|
||||||
|
del self.last_check_time["issues"][normalized_repo]
|
||||||
|
if normalized_repo in self.last_check_time["releases"]:
|
||||||
|
del self.last_check_time["releases"][normalized_repo]
|
||||||
self._save_subscriptions()
|
self._save_subscriptions()
|
||||||
yield event.plain_result(f"已取消订阅仓库 {repo}")
|
yield event.plain_result(f"已取消订阅仓库 {repo}")
|
||||||
else:
|
else:
|
||||||
@ -256,18 +276,20 @@ class MyPlugin(Star):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取该仓库的最后检查时间
|
# 检查新的issues和PRs
|
||||||
last_check = self.last_check_time.get(repo, None)
|
issue_last_check = self.last_check_time["issues"].get(repo, None)
|
||||||
|
new_items = await self._fetch_new_items(repo, issue_last_check)
|
||||||
# 获取新的issues和PRs
|
|
||||||
new_items = await self._fetch_new_items(repo, last_check)
|
|
||||||
|
|
||||||
if new_items:
|
if new_items:
|
||||||
# 更新最后检查时间
|
self.last_check_time["issues"][repo] = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
self.last_check_time[repo] = datetime.now().isoformat()
|
|
||||||
|
|
||||||
# 通知订阅者有关新内容
|
|
||||||
await self._notify_subscribers(repo, new_items)
|
await self._notify_subscribers(repo, new_items)
|
||||||
|
|
||||||
|
# 检查新的Releases
|
||||||
|
release_last_check = self.last_check_time["releases"].get(repo, None)
|
||||||
|
new_releases = await self._fetch_new_releases(repo, release_last_check)
|
||||||
|
if new_releases:
|
||||||
|
self.last_check_time["releases"][repo] = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
|
await self._notify_subscribers_releases(repo, new_releases)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"检查仓库 {repo} 更新时出错: {e}")
|
logger.error(f"检查仓库 {repo} 更新时出错: {e}")
|
||||||
|
|
||||||
@ -275,26 +297,20 @@ class MyPlugin(Star):
|
|||||||
"""从上次检查以来获取仓库的新issues和PRs"""
|
"""从上次检查以来获取仓库的新issues和PRs"""
|
||||||
if not last_check:
|
if not last_check:
|
||||||
# 如果是第一次检查,只记录当前时间并返回空列表
|
# 如果是第一次检查,只记录当前时间并返回空列表
|
||||||
# 存储为UTC时间戳,不带时区信息以避免比较问题
|
self.last_check_time["issues"][repo] = (
|
||||||
self.last_check_time[repo] = (
|
|
||||||
datetime.utcnow().replace(microsecond=0).isoformat()
|
datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
)
|
)
|
||||||
logger.info(f"初始化仓库 {repo} 的时间戳: {self.last_check_time[repo]}")
|
logger.info(f"初始化仓库 {repo} 的Issue/PR时间戳: {self.last_check_time['issues'][repo]}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 始终将存储的时间戳视为不带时区信息的UTC时间
|
|
||||||
last_check_dt = datetime.fromisoformat(last_check)
|
last_check_dt = datetime.fromisoformat(last_check)
|
||||||
|
|
||||||
# 确保它被视为简单的datetime
|
|
||||||
if hasattr(last_check_dt, "tzinfo") and last_check_dt.tzinfo is not None:
|
if hasattr(last_check_dt, "tzinfo") and last_check_dt.tzinfo is not None:
|
||||||
# 如果它以某种方式具有时区信息,转换为简单的UTC
|
|
||||||
last_check_dt = last_check_dt.replace(tzinfo=None)
|
last_check_dt = last_check_dt.replace(tzinfo=None)
|
||||||
|
|
||||||
logger.info(f"仓库 {repo} 的上次检查时间: {last_check_dt.isoformat()}")
|
logger.info(f"仓库 {repo} 的Issue/PR上次检查时间: {last_check_dt.isoformat()}")
|
||||||
new_items = []
|
new_items = []
|
||||||
|
|
||||||
# GitHub API在issues端点中同时返回issues和PRs
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
try:
|
try:
|
||||||
params = {
|
params = {
|
||||||
@ -312,56 +328,107 @@ class MyPlugin(Star):
|
|||||||
items = await resp.json()
|
items = await resp.json()
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
# 将GitHub的时间戳转换为简单的UTC datetime以进行一致的比较
|
|
||||||
github_timestamp = item["created_at"].replace("Z", "")
|
github_timestamp = item["created_at"].replace("Z", "")
|
||||||
created_at = datetime.fromisoformat(github_timestamp)
|
created_at = datetime.fromisoformat(github_timestamp)
|
||||||
|
|
||||||
# 始终移除时区信息以进行比较
|
|
||||||
created_at = created_at.replace(tzinfo=None)
|
created_at = created_at.replace(tzinfo=None)
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"比较: 仓库 {repo} 的item #{item['number']} 创建于 {created_at.isoformat()}, 上次检查: {last_check_dt.isoformat()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if created_at > last_check_dt:
|
if created_at > last_check_dt:
|
||||||
logger.info(
|
logger.info(f"发现新的item #{item['number']} in {repo}")
|
||||||
f"发现新的item #{item['number']} in {repo}"
|
|
||||||
)
|
|
||||||
new_items.append(item)
|
new_items.append(item)
|
||||||
else:
|
else:
|
||||||
# 由于项目按创建时间排序,我们可以提前中断
|
|
||||||
logger.info(f"没有更多新items in {repo}")
|
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(f"获取仓库 {repo} 的Issue/PR失败: {resp.status}: {await resp.text()}")
|
||||||
f"获取仓库 {repo} 的Issue/PR失败: {resp.status}: {await resp.text()}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取仓库 {repo} 的Issue/PR时出错: {e}")
|
logger.error(f"获取仓库 {repo} 的Issue/PR时出错: {e}")
|
||||||
|
|
||||||
# 将最后检查时间更新为现在(不带时区信息的UTC)
|
self.last_check_time["issues"][repo] = (
|
||||||
if new_items:
|
|
||||||
logger.info(f"找到 {len(new_items)} 个新的items在 {repo}")
|
|
||||||
else:
|
|
||||||
logger.info(f"没有找到新的items在 {repo}")
|
|
||||||
|
|
||||||
# 检查后始终更新时间戳,无论是否找到项目
|
|
||||||
self.last_check_time[repo] = (
|
|
||||||
datetime.utcnow().replace(microsecond=0).isoformat()
|
datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
)
|
)
|
||||||
logger.info(f"更新仓库 {repo} 的时间戳为: {self.last_check_time[repo]}")
|
logger.info(f"更新仓库 {repo} 的Issue/PR时间戳为: {self.last_check_time['issues'][repo]}")
|
||||||
|
|
||||||
return new_items
|
return new_items
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"解析时间时出错: {e}")
|
logger.error(f"解析Issue/PR时间时出错: {e}")
|
||||||
# 如果无法正确解析时间,只需返回空列表
|
self.last_check_time["issues"][repo] = (
|
||||||
# 并更新最后检查时间以防止连续错误
|
|
||||||
self.last_check_time[repo] = (
|
|
||||||
datetime.utcnow().replace(microsecond=0).isoformat()
|
datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(f"出错后更新仓库 {repo} 的Issue/PR时间戳为: {self.last_check_time['issues'][repo]}")
|
||||||
f"出错后更新仓库 {repo} 的时间戳为: {self.last_check_time[repo]}"
|
return []
|
||||||
|
|
||||||
|
# 新增:获取新的Releases
|
||||||
|
async def _fetch_new_releases(self, repo: str, last_check: str):
|
||||||
|
"""从上次检查以来获取仓库的新Releases"""
|
||||||
|
if not last_check:
|
||||||
|
# 第一次检查,初始化时间戳
|
||||||
|
self.last_check_time["releases"][repo] = (
|
||||||
|
datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
)
|
)
|
||||||
|
logger.info(f"初始化仓库 {repo} 的Release时间戳: {self.last_check_time['releases'][repo]}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
last_check_dt = datetime.fromisoformat(last_check)
|
||||||
|
if hasattr(last_check_dt, "tzinfo") and last_check_dt.tzinfo is not None:
|
||||||
|
last_check_dt = last_check_dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
logger.info(f"仓库 {repo} 的Release上次检查时间: {last_check_dt.isoformat()}")
|
||||||
|
new_releases = []
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
"sort": "published",
|
||||||
|
"direction": "desc",
|
||||||
|
"per_page": 10,
|
||||||
|
}
|
||||||
|
async with session.get(
|
||||||
|
GITHUB_RELEASES_API_URL.format(repo=repo),
|
||||||
|
params=params,
|
||||||
|
headers=self._get_github_headers(),
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
releases = await resp.json()
|
||||||
|
|
||||||
|
for release in releases:
|
||||||
|
# 跳过草稿版本
|
||||||
|
if release["draft"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 根据配置决定是否跳过预发布版本
|
||||||
|
if not self.include_prereleases and release["prerelease"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 解析发布时间
|
||||||
|
publish_timestamp = release["published_at"].replace("Z", "") if release["published_at"] else None
|
||||||
|
if not publish_timestamp:
|
||||||
|
continue # 跳过未发布的版本
|
||||||
|
|
||||||
|
published_at = datetime.fromisoformat(publish_timestamp)
|
||||||
|
published_at = published_at.replace(tzinfo=None)
|
||||||
|
|
||||||
|
if published_at > last_check_dt:
|
||||||
|
logger.info(f"发现新的Release {release['tag_name']} in {repo}")
|
||||||
|
new_releases.append(release)
|
||||||
|
else:
|
||||||
|
break # 按时间排序,可提前中断
|
||||||
|
else:
|
||||||
|
logger.error(f"获取仓库 {repo} 的Release失败: {resp.status}: {await resp.text()}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取仓库 {repo} 的Release时出错: {e}")
|
||||||
|
|
||||||
|
self.last_check_time["releases"][repo] = (
|
||||||
|
datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
|
)
|
||||||
|
logger.info(f"更新仓库 {repo} 的Release时间戳为: {self.last_check_time['releases'][repo]}")
|
||||||
|
|
||||||
|
return new_releases
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"解析Release时间时出错: {e}")
|
||||||
|
self.last_check_time["releases"][repo] = (
|
||||||
|
datetime.utcnow().replace(microsecond=0).isoformat()
|
||||||
|
)
|
||||||
|
logger.info(f"出错后更新仓库 {repo} 的Release时间戳为: {self.last_check_time['releases'][repo]}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def _notify_subscribers(self, repo: str, new_items: List[Dict]):
|
async def _notify_subscribers(self, repo: str, new_items: List[Dict]):
|
||||||
@ -371,7 +438,6 @@ class MyPlugin(Star):
|
|||||||
|
|
||||||
for subscriber_id in self.subscriptions.get(repo, []):
|
for subscriber_id in self.subscriptions.get(repo, []):
|
||||||
try:
|
try:
|
||||||
# 创建通知消息
|
|
||||||
for item in new_items:
|
for item in new_items:
|
||||||
item_type = "PR" if "pull_request" in item else "Issue"
|
item_type = "PR" if "pull_request" in item else "Issue"
|
||||||
message = (
|
message = (
|
||||||
@ -381,16 +447,54 @@ class MyPlugin(Star):
|
|||||||
f"链接: {item['html_url']}"
|
f"链接: {item['html_url']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 向订阅者发送消息
|
|
||||||
await self.context.send_message(
|
await self.context.send_message(
|
||||||
subscriber_id, Comp.Plain(message)
|
subscriber_id, Comp.Plain(message)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 消息之间添加小延迟以避免速率限制
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"向订阅者 {subscriber_id} 发送通知时出错: {e}")
|
logger.error(f"向订阅者 {subscriber_id} 发送通知时出错: {e}")
|
||||||
|
|
||||||
|
# 新增:通知订阅者有关新的Releases
|
||||||
|
async def _notify_subscribers_releases(self, repo: str, new_releases: List[Dict]):
|
||||||
|
"""通知订阅者有关新的Release"""
|
||||||
|
if not new_releases:
|
||||||
|
return
|
||||||
|
|
||||||
|
for subscriber_id in self.subscriptions.get(repo, []):
|
||||||
|
try:
|
||||||
|
for release in new_releases:
|
||||||
|
# 处理发布说明(过长时截断)
|
||||||
|
body = release.get("body", "无发布说明")
|
||||||
|
if len(body) > 200:
|
||||||
|
body = body[:200] + "..."
|
||||||
|
|
||||||
|
# 构建通知消息
|
||||||
|
message_parts = [
|
||||||
|
f"[GitHub Release更新] 仓库 {repo} 发布了新版本:\n",
|
||||||
|
f"版本: {release['tag_name']}"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 如果是预发布版本,添加标记
|
||||||
|
if release["prerelease"]:
|
||||||
|
message_parts.append(" 🧪 预发布")
|
||||||
|
|
||||||
|
message_parts.extend([
|
||||||
|
f"\n标题: {release['name'] or '无标题'}\n",
|
||||||
|
f"发布时间: {release['published_at'].replace('T', ' ').replace('Z', '')}\n",
|
||||||
|
f"说明: {body}\n",
|
||||||
|
f"下载: {release['html_url']}"
|
||||||
|
])
|
||||||
|
|
||||||
|
message = ''.join(message_parts)
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
await self.context.send_message(
|
||||||
|
subscriber_id, Comp.Plain(message)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"向订阅者 {subscriber_id} 发送Release通知时出错: {e}")
|
||||||
|
|
||||||
async def terminate(self):
|
async def terminate(self):
|
||||||
"""终止前清理并保存数据"""
|
"""终止前清理并保存数据"""
|
||||||
self._save_subscriptions()
|
self._save_subscriptions()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user