Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

添加私有仓库传图 #204

Closed
wants to merge 5 commits into from
Closed
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
2 changes: 1 addition & 1 deletion server/tasks/github/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def on_issue_opened(event_dict: dict | None) -> list:
db.session.add(new_issue)
db.session.commit()

task = send_issue_card.delay(issue_id=new_issue.id)
task = send_issue_card.delay(issue_id=new_issue.id, user_name=event.sender.login)

# 新建issue之后也要更新 repo info
on_repository_updated(event.model_dump())
Expand Down
189 changes: 158 additions & 31 deletions server/tasks/lark/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ChatGroup,
CodeApplication,
CodeUser,
IMApplication,
IMUser,
Issue,
Repo,
Expand All @@ -16,12 +17,13 @@
db,
)
from model.team import get_assignees_by_openid
from utils.github.bot import BaseGitHubApp
from utils.github.repo import GitHubAppRepo
from utils.lark.issue_card import IssueCard
from utils.lark.issue_manual_help import IssueManualHelp, IssueView
from utils.lark.issue_tip_failed import IssueTipFailed
from utils.lark.issue_tip_success import IssueTipSuccess
from utils.utils import upload_image
from utils.utils import upload_image, upload_private_image

from .base import (
get_bot_by_application_id,
Expand Down Expand Up @@ -80,7 +82,9 @@ def get_assignees_by_issue(issue, team):
return assignees


def gen_issue_card_by_issue(bot, issue, repo_url, team, maunal=False):
def gen_issue_card_by_issue(
bot, issue, repo_url, team, maunal=False, from_github=False, user_name=None
):
assignees = get_assignees_by_issue(issue, team)
tags = [i["name"] for i in issue.extra.get("labels", [])]
status = issue.extra.get("state", "opened")
Expand All @@ -100,10 +104,32 @@ def gen_issue_card_by_issue(bot, issue, repo_url, team, maunal=False):
tags=tags,
)

# 处理 description
description = replace_images_with_keys(
issue.description if issue.description else "", bot
)
description = issue.description

if from_github:
# 处理图片, 私有仓库先下载再上传飞书
extra = (
db.session.query(Repo.extra)
.filter(
Repo.id == issue.repo_id,
)
.scalar()
)
is_private = extra.get("private", True)

# 处理 description
if is_private:
access_token = get_code_access_token_by_name(user_name)
description = replace_images_with_keys(
issue.description if issue.description else "",
bot,
access_token=access_token,
)
else:
description = replace_images_with_keys(
issue.description if issue.description else "", bot
)

return IssueCard(
repo_url=repo_url,
id=issue.issue_number,
Expand All @@ -116,7 +142,7 @@ def gen_issue_card_by_issue(bot, issue, repo_url, team, maunal=False):
)


def replace_images_with_keys(text, bot):
def replace_images_with_keys(text, bot, access_token=None):
"""
replace image URL to image_key.
Markdown: ![](url) -> ![](image_key)
Expand All @@ -128,21 +154,31 @@ def replace_images_with_keys(text, bot):
Returns:
str: replaced text
"""
# Replace Markdown image syntax
markdown_pattern = r"!\[.*?\]\((.*?)\)"
replaced_text = re.sub(
markdown_pattern,
lambda match: f"![]({upload_image(match.group(1), bot)})",
text,
)

# Replace HTML image syntax
html_pattern = r"<img.*?src=\"(.*?)\".*?>"
replaced_text = re.sub(
html_pattern,
lambda match: f"![]({upload_image(match.group(1), bot)})",
replaced_text,
)

if access_token:
replaced_text = re.sub(
markdown_pattern,
lambda match: f"![]({upload_private_image(match.group(1),access_token, bot)})",
text,
)
replaced_text = re.sub(
html_pattern,
lambda match: f"![]({upload_private_image(match.group(1), bot)})",
replaced_text,
)
else:
replaced_text = re.sub(
markdown_pattern,
lambda match: f"![]({upload_image(match.group(1), bot)})",
text,
)
replaced_text = re.sub(
html_pattern,
lambda match: f"![]({upload_image(match.group(1), bot)})",
replaced_text,
)

return replaced_text.replace("![]()", "(请确认图片是否上传成功)")

Expand Down Expand Up @@ -171,7 +207,14 @@ def send_issue_url_message(
bot, application = get_bot_by_application_id(app_id)
if not application:
return send_issue_failed_tip(
"找不到对应的应用", app_id, message_id, content, data, *args, bot=bot, **kwargs
"找不到对应的应用",
app_id,
message_id,
content,
data,
*args,
bot=bot,
**kwargs,
)

team = (
Expand All @@ -183,7 +226,14 @@ def send_issue_url_message(
)
if not team:
return send_issue_failed_tip(
"找不到对应的项目", app_id, message_id, content, data, *args, bot=bot, **kwargs
"找不到对应的项目",
app_id,
message_id,
content,
data,
*args,
bot=bot,
**kwargs,
)

repo_url = f"https://github.com/{team.name}/{repo.name}"
Expand All @@ -194,7 +244,14 @@ def send_issue_url_message(
)
else:
return send_issue_failed_tip(
"找不到对应的项目", app_id, message_id, content, data, *args, bot=bot, **kwargs
"找不到对应的项目",
app_id,
message_id,
content,
data,
*args,
bot=bot,
**kwargs,
)
# 回复到话题内部
return bot.reply(message_id, message).json()
Expand Down Expand Up @@ -230,7 +287,14 @@ def send_issue_manual(app_id, message_id, content, data, *args, **kwargs):
bot, application = get_bot_by_application_id(app_id)
if not application:
return send_issue_failed_tip(
"找不到对应的应用", app_id, message_id, content, data, *args, bot=bot, **kwargs
"找不到对应的应用",
app_id,
message_id,
content,
data,
*args,
bot=bot,
**kwargs,
)

team = (
Expand All @@ -242,17 +306,24 @@ def send_issue_manual(app_id, message_id, content, data, *args, **kwargs):
)
if not team:
return send_issue_failed_tip(
"找不到对应的项目", app_id, message_id, content, data, *args, bot=bot, **kwargs
"找不到对应的项目",
app_id,
message_id,
content,
data,
*args,
bot=bot,
**kwargs,
)

repo_url = f"https://github.com/{team.name}/{repo.name}"
message = gen_issue_card_by_issue(bot, issue, repo_url, team, True)
message = gen_issue_card_by_issue(bot, issue, repo_url, team, maunal=True)
# 回复到话题内部
return bot.reply(message_id, message).json()


@celery.task()
def send_issue_card(issue_id):
def send_issue_card(issue_id, user_name):
"""send new issue card message to user.

Args:
Expand All @@ -275,7 +346,9 @@ def send_issue_card(issue_id):
team = db.session.query(Team).filter(Team.id == application.team_id).first()
if application and team:
repo_url = f"https://github.com/{team.name}/{repo.name}"
message = gen_issue_card_by_issue(bot, issue, repo_url, team)
message = gen_issue_card_by_issue(
bot, issue, repo_url, team, from_github=True, user_name=user_name
)
result = bot.send(
chat_group.chat_id, message, receive_id_type="chat_id"
).json()
Expand Down Expand Up @@ -308,7 +381,7 @@ def send_issue_card(issue_id):

@celery.task()
def send_issue_comment(issue_id, comment, user_name: str):
"""send new issue comment message to user.
"""send new issue comment message to feishu.

Args:
issue_id: Issue.id.
Expand All @@ -328,8 +401,19 @@ def send_issue_comment(issue_id, comment, user_name: str):
)
if chat_group and issue.message_id:
bot, _ = get_bot_by_application_id(chat_group.im_application_id)

# 处理图片, 私有仓库先下载再上传飞书
is_private = repo.extra.get("private", True)

# 替换 comment 中的图片 url 为 image_key
comment = replace_images_with_keys(comment, bot)
if is_private:
access_token = get_installation_token_by_issue(issue_id)
comment = replace_images_with_keys(
comment, bot, access_token=access_token
)
else:
comment = replace_images_with_keys(comment, bot)

# 统一用富文本回答, 支持图片、at
content = gen_comment_post_message(user_name, comment)
result = bot.reply(
Expand All @@ -340,6 +424,43 @@ def send_issue_comment(issue_id, comment, user_name: str):
return False


def get_installation_token_by_issue(issue_id):
# 1. 通过 issue_id 查询 im_application_id
code_application_id = (
db.session.query(Repo.application_id)
.join(
Issue,
Issue.repo_id == Repo.id,
)
.filter(
Issue.id == issue_id,
)
.limit(1)
.scalar()
)
installation_id = (
db.session.query(CodeApplication.installation_id)
.filter(
CodeApplication.id == code_application_id,
)
.limit(1)
.scalar()
)

github_app = BaseGitHubApp(installation_id)
return github_app.installation_token


def get_code_access_token_by_name(name):
access_token = (
(db.session.query(CodeUser.access_token).filter(CodeUser.name == name))
.limit(1)
.scalar()
)
# TODO 校验Token
return access_token


def gen_comment_post_message(user_name, comment):
comment = comment.replace("\r\n", "\n")
comment = re.sub(r"!\[.*?\]\((.*?)\)", r"\n\1\n", comment)
Expand Down Expand Up @@ -631,7 +752,13 @@ def get_github_name_by_openid(

if not code_user_id:
return send_issue_failed_tip(
"找不到对应的 GitHub 用户", app_id, message_id, content, data, *args, **kwargs
"找不到对应的 GitHub 用户",
app_id,
message_id,
content,
data,
*args,
**kwargs,
)

# 第三步:如果找到了 code_user_id,使用它在 bind_user 表中查询 name
Expand Down
38 changes: 38 additions & 0 deletions server/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ def upload_image(url, bot):
return ""


@stalecache(expire=86400, stale=600)
def upload_private_image(url, access_token, bot):
img_bin = download_image_with_token(access_token, url)
if img_bin:
img_key = upload_image_binary(img_bin, bot)
return img_key
else:
return ""


def upload_image_binary(img_bin, bot):
url = f"{bot.host}/open-apis/im/v1/images"

Expand All @@ -30,6 +40,34 @@ def upload_image_binary(img_bin, bot):
return response["data"]["image_key"]


def download_image_with_token(access_token: str, url: str) -> str | None:
"""Download image by access token.

Args:
access_token (str): The user access token.
url (str): The image url.

Returns:
str: image.
"""

response = httpx.get(
url,
headers={
"Accept": "application/vnd.github.v3+json",
"Authorization": f"Bearer {access_token}",
"X-GitHub-Api-Version": "2022-11-28",
},
follow_redirects=True,
)

if response.status_code != 200:
logging.debug(f"Failed to get image. {response.text}")
return None

return response.content


def query_one_page(query, page, size):
offset = (page - 1) * int(size)
return (
Expand Down
Loading