Commit 02e3495

benny-dou <60535774+benny-dou@users.noreply.github.com>
2025-09-27 03:16:26
feat(github): support issue and pull request preview
1 parent d7c8cb5
Changed files (4)
src
src/messages/utils.py
@@ -233,3 +233,11 @@ async def delete_message(message: Message | None):
         return
     with contextlib.suppress(Exception):
         await message.delete()
+
+
+def remove_img_tag(markdown: str) -> tuple[str, list[str]]:
+    """Removes all image tags from a markdown string."""
+    image_pattern = r"!\[.*?\]\((.*?)\)"  # Matches both with and without alt text
+    clean = re.sub(image_pattern, "", markdown)
+    urls = re.findall(image_pattern, markdown)
+    return clean, urls
src/podcast/utils.py
@@ -1,7 +1,6 @@
 #!/venv/bin/python
 # -*- coding: utf-8 -*-
 import base64
-import re
 import string
 from datetime import UTC, datetime
 from zoneinfo import ZoneInfo
@@ -18,12 +17,6 @@ HEADERS = {
 }
 
 
-def remove_img_tag(markdown: str):
-    """Removes all image tags from a markdown string."""
-    image_pattern = r"!\[.*?\]\((.*?)\)"  # Matches both with and without alt text
-    return re.sub(image_pattern, "", markdown)
-
-
 def clean_feed_url(url: str) -> str:
     if not url:
         return ""
src/preview/github.py
@@ -10,25 +10,47 @@ from pyrogram.types import Message
 from config import PROXY, TOKEN, TZ
 from messages.progress import modify_progress
 from messages.sender import send2tg
-from networking import download_file, hx_req
+from messages.utils import remove_img_tag
+from networking import download_file, download_media, hx_req
 from utils import nowdt
 
+HEADERS = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
+if TOKEN.GITHUB:
+    HEADERS["Authorization"] = f"Bearer {TOKEN.GITHUB}"
 
-async def preview_github(client: Client, message: Message, url: str, gh_user: str = "", gh_repo: str = "", **kwargs):
+
+async def preview_github(client: Client, message: Message, url: str, gh_user: str = "", gh_repo: str = "", query: str = "", **kwargs):
     """Preview github info in the message."""
     if kwargs.get("show_progress") and "progress" not in kwargs:
         res = await send2tg(client, message, texts=f"🔗正在解析GitHub链接\n{url}", **kwargs)
         kwargs["progress"] = res[0]
-    headers = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
-    if TOKEN.GITHUB:
-        headers["Authorization"] = f"Bearer {TOKEN.GITHUB}"
+    kwargs["send_from_user"] = ""  # disable @send_user
+    if not query:
+        resp = await preview_readme(gh_user, gh_repo)
+    elif query.startswith(("issues", "pull")):
+        resp = await preview_issue(gh_user, gh_repo, query)
+    else:
+        resp = {"error": "未知的GitHub查询类型"}
+
+    if error := resp.get("error"):
+        await modify_progress(text=f"❌GitHub解析失败: {error}", force_update=True, **kwargs)
+    await send2tg(client, message, **resp, **kwargs)
+    await modify_progress(del_status=True, **kwargs)
+
+
+async def preview_readme(gh_user: str, gh_repo: str) -> dict:
+    """Preview github readme.
+
+    https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#get-a-repository
+
+    Returns:
+        {"texts": str, "media": list[dict]}
+    """
     api = f"https://api.github.com/repos/{gh_user}/{gh_repo}"
-    resp = await hx_req(api, headers=headers, proxy=PROXY.GITHUB, check_keys=["full_name"])
+    resp = await hx_req(api, headers=HEADERS, proxy=PROXY.GITHUB, check_keys=["full_name"])
     if error := resp.get("hx_error"):
-        await modify_progress(text=f"❌GitHub解析失败: {error}", force_update=True, **kwargs)
-        return
+        return {"error": error}
     full_repo = resp["full_name"]
-    url = f"https://github.com/{full_repo}"
     gh_user, gh_repo = full_repo.split("/")  # correct uppercase / lowercase
     msg = ""
     if desc := resp["description"]:
@@ -52,20 +74,61 @@ async def preview_github(client: Client, message: Message, url: str, gh_user: st
         for tag in tags:
             msg += f" #{tag}"
     media = []
-    if readme := await github_readme(gh_user, gh_repo, headers=headers):
+    if readme := await download_readme(gh_user, gh_repo):
         media = [{"document": readme}]
-    kwargs["send_from_user"] = ""  # disable @send_user
-    await send2tg(client, message, texts=msg.strip(), media=media, **kwargs)
-    await modify_progress(del_status=True, **kwargs)
+    return {"texts": msg.strip(), "media": media}
+
+
+async def preview_issue(gh_user: str, gh_repo: str, query: str) -> dict:
+    """Preview github issue.
+
+    https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue
+
+    Args:
+        query:
+            issues/123
+            issues/123#issuecomment-4567
+            pull/123
+            pull/123#issuecomment-4567
+
+    Returns:
+        {"texts": str, "media": list[dict]}
+    """
+    issue_number = query.split("#")[0].split("/")[1]
+    api = f"https://api.github.com/repos/{gh_user}/{gh_repo}/issues/{issue_number}"
+    resp = await hx_req(api, headers=HEADERS, proxy=PROXY.GITHUB, check_kv={"number": issue_number})
+    if error := resp.get("hx_error"):
+        return {"error": error}
+    msg = f"📦[{gh_user}/{gh_repo}/{query.split('#')[0]}](https://github.com/{gh_user}/{gh_repo}/{query})\n"
+    emoji = "🟢" if resp["state"] == "open" else "🟣"
+    msg += f"{emoji}**{resp['title']}**\n"
+    if "issuecomment" in query:
+        comment_id = query.split("#issuecomment-")[-1]
+        api = f"https://api.github.com/repos/{gh_user}/{gh_repo}/issues/comments/{comment_id}"
+        resp = await hx_req(api, headers=HEADERS, proxy=PROXY.GITHUB, check_kv={"id": comment_id})
+    issue_user = glom(resp, "user.login", default="user")
+    msg += f"👤[{issue_user}](https://github.com/{issue_user})\n"
+    msg += f"🕒{convert_dt(resp['created_at']):%Y-%m-%d %H:%M:%S}创建\n"
+    if resp.get("closed_at"):
+        msg += f"🕒{convert_dt(resp['closed_at']):%Y-%m-%d %H:%M:%S}关闭\n"
+
+    media = []
+    if desc := resp["body"]:
+        cleaned, urls = remove_img_tag(desc)
+        media = [{"photo": download_file(url, proxy=PROXY.GITHUB)} for url in urls]
+        media = await download_media(media)
+        msg += f"{cleaned}\n"
+
+    return {"texts": msg.strip(), "media": media}
 
 
-async def github_readme(user: str, repo: str, headers: dict) -> str:
+async def download_readme(user: str, repo: str) -> str:
     """Returns downloaded README path."""
     api = f"https://api.github.com/repos/{user}/{repo}/readme"
-    resp = await hx_req(api, headers=headers, proxy=PROXY.GITHUB, check_kv={"type": "file"})
+    resp = await hx_req(api, headers=HEADERS, proxy=PROXY.GITHUB, check_kv={"type": "file"})
     if not resp.get("download_url"):
         return ""
-    return await download_file(resp["download_url"], headers=headers, skip_exist=False, proxy=PROXY.GITHUB)
+    return await download_file(resp["download_url"], headers=HEADERS, skip_exist=False, proxy=PROXY.GITHUB)
 
 
 def convert_dt(dt: str) -> datetime:
@@ -85,7 +148,7 @@ def delta_time(dt: str) -> str:
     res = ""
     if delta.days:
         res += f"{delta.days}天"
-    minutes, seconds = divmod(delta.seconds, 60)
+    minutes, _ = divmod(delta.seconds, 60)
     hours, minutes = divmod(minutes, 60)
     if hours:
         res += f"{hours}小时"
src/networking.py
@@ -217,6 +217,8 @@ async def download_first_success_urls(links: list[str], **kwargs) -> str:
 
 
 async def download_media(media: list[dict], **kwargs) -> list[dict]:
+    if not media:
+        return []
     tasks = []
     for item in media:
         if task := item.get("photo"):  # async function
@@ -362,11 +364,16 @@ async def match_social_media_link(text: str, *, flatten_first: bool = True) -> d
         return {"url": url, "db_key": url, "post_id": post_id, "platform": "bilibili-opus"}
 
     # https://github.com/user-name/repo
-    if matched := re.search(r"(https?://)?github\.com/([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)/([a-zA-Z0-9_-]+)", text):
+    # https://github.com/user-name/repo/issues/123
+    # https://github.com/user-name/repo/issues/123#issuecomment-45678
+    # https://github.com/user-name/repo/pull/123
+    # https://github.com/user-name/repo/pull/123#issuecomment-45678
+    if matched := re.search(r"(https?://)?github\.com/([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)/([a-zA-Z0-9_-]+)/?([#/a-zA-Z0-9_-]+)?", text):
         gh_user = matched.group(2)
         gh_repo = matched.group(4)
-        url = f"https://github.com/{gh_user}/{gh_repo}"
-        return {"url": url, "db_key": bare_url(url), "gh_user": gh_user, "gh_repo": gh_repo, "platform": "github"}
+        query = matched.group(5) or ""
+        url = matched.group(0)
+        return {"url": https_url(url), "db_key": bare_url(url), "gh_user": gh_user, "gh_repo": gh_repo, "query": query, "platform": "github"}
 
     # https://www.v2ex.com/t/1153086
     if matched := re.search(r"(https?://)?(www\.)?v2ex\.com/t/(\d+)", text):
@@ -522,8 +529,15 @@ if __name__ == "__main__":
     # asyncio.run(flatten_rediercts("https://v.douyin.com/CeiJfJMQG/"))
     # asyncio.run(flatten_rediercts("https://www.tiktok.com/t/ZT2mcMA7f/"))
     # asyncio.run(flatten_rediercts("https://t.co/Wwo3x69CQz"))
-    print(asyncio.run(match_social_media_link("https://www.bilibili.com/video/BV1TC411J7PK")))
-    print(asyncio.run(match_social_media_link("https://www.bilibili.com/BV1TC411J7PK")))
+    print(asyncio.run(match_social_media_link("https://github.com/yt-dlp/yt-dlp/issues/14463")))
+    print(asyncio.run(match_social_media_link("https://github.com/yt-dlp/yt-dlp/")))
+    print(asyncio.run(match_social_media_link("https://github.com/yt-dlp/yt-dlp")))
+    print(asyncio.run(match_social_media_link("https://github.com/yt-dlp/yt-dlp/issues/14404#issuecomment-3323873708")))
+    print(asyncio.run(match_social_media_link("https://github.com/yt-dlp/yt-dlp/pull/14467")))
+    print(asyncio.run(match_social_media_link("https://github.com/yt-dlp/yt-dlp/pull/14417#issuecomment-3327344721")))
+
+    # print(asyncio.run(match_social_media_link("https://www.bilibili.com/video/BV1TC411J7PK")))
+    # print(asyncio.run(match_social_media_link("https://www.bilibili.com/BV1TC411J7PK")))
     # print(asyncio.run(match_social_media_link("https://www.instagram.com/miyoshi.aa/p/DN5hFcUE8rS/")))
     # print(asyncio.run(match_social_media_link("https://www.youtube.com/watch?v=D6aE2E0RHTc")))
     # print(asyncio.run(match_social_media_link("https://youtube.com/shorts/lFKHbluAlJw")))