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

支持飞书传图GitHub #231

Merged
merged 9 commits into from
Feb 27, 2024
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
48 changes: 24 additions & 24 deletions server/routes/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,29 +175,29 @@ def install_im_application_to_team_by_get_method(team_id, platform):
)
app.logger.info("result %r", result)
events = [
"20",
"im.message.message_read_v1",
"im.message.reaction.created_v1",
"im.message.reaction.deleted_v1",
"im.message.recalled_v1",
"im.message.receive_v1",
"20", # 用户和机器人的会话首次被创建
"im.message.message_read_v1", # 消息已读
"im.message.reaction.created_v1", # 新增消息表情回复
"im.message.reaction.deleted_v1", # 删除消息表情回复
"im.message.recalled_v1", # 撤回消息
"im.message.receive_v1", # 接收消息
]
scope_ids = [
"8002",
"100032",
"6081",
"14",
"1",
"21001",
"20001",
"20011",
"3001",
"20012",
"20010",
"3000",
"20008",
"1000",
"20009",
"8002", # 获取应用信息
"100032", # 获取通讯录基本信息
"6081", # 以应用身份读取通讯录
"14", # 获取用户基本信息
"1", # 获取用户邮箱信息
"21001", # 获取与更新群组信息
"20001", # 获取与发送单聊、群组消息
"20011", # 获取用户在群组中 @ 机器人的消息
"3001", # 接收群聊中 @ 机器人消息事件
"20012", # 获取群组中所有消息
"20010", # 获取用户发给机器人的单聊消息
"3000", # 读取用户发给机器人的单聊消息
"20008", # 获取单聊、群组消息
"1000", # 以应用的身份发消息
"20009", # 获取上传图片或文件资源
]
hook_url = f"{os.environ.get('DOMAIN')}/api/feishu/hook/{app_id}"
return redirect(
Expand Down Expand Up @@ -260,9 +260,9 @@ def get_task_result_by_id(team_id, task_id):
"data": {
"task_id": task.id,
"status": task.status,
"result": task.result
if isinstance(task.result, list)
else str(task.result),
"result": (
task.result if isinstance(task.result, list) else str(task.result)
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里改了结构,需要和前端沟通好,免得前端调用接口的时候报错

},
}
)
Expand Down
44 changes: 42 additions & 2 deletions server/routes/user.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from app import app
from flask import Blueprint, jsonify, request, session
from model.team import get_team_list_by_user_id, is_team_admin
from flask import Blueprint, Response, abort, jsonify, request, session
from model.team import (
get_application_info_by_team_id,
get_team_list_by_user_id,
is_team_admin,
)
from model.user import get_user_by_id
from tasks.lark.base import get_bot_by_application_id, get_repo_by_repo_id
from utils.auth import authenticated
from utils.utils import download_file

bp = Blueprint("user", __name__, url_prefix="/api")

Expand Down Expand Up @@ -56,4 +62,38 @@ def set_account():
return jsonify({"code": 0, "msg": "success"})


@bp.route("/<team_id>/<repo_id>/<message_id>/image/<img_key>", methods=["GET"])
def get_image(team_id, message_id, repo_id, img_key):
"""
1. 用 img_key 请求飞书接口下载 image
2. 判断请求来源,如果是 GitHub 调用,则直接返回 image
3. 用户调用 校验权限
"""

def download_and_respond():
_, im_application = get_application_info_by_team_id(team_id)
bot, _ = get_bot_by_application_id(im_application.id)
image_content = download_file(img_key, message_id, bot, "image")
return Response(image_content, mimetype="image/png")

# GitHub调用
user_agent = request.headers.get("User-Agent")
if user_agent and user_agent.startswith("github-camo"):
return download_and_respond()

# TODO 用户调用(弱需求, 通常来讲此接口不会被暴露), 需要进一步校验权限
referer = request.headers.get("Referer")
if not referer:
# 公开仓库不校验
repo = get_repo_by_repo_id(repo_id)
is_private = repo.extra.get("private", False)
app.logger.debug(f"is_private: {is_private}")

# 私有仓库校验,先登录
if is_private:
return abort(403)

return download_and_respond()


app.register_blueprint(bp)
7 changes: 6 additions & 1 deletion server/tasks/lark/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def get_chat_group_by_chat_id(chat_id):


def get_repo_name_by_repo_id(repo_id):
repo = get_repo_by_repo_id(repo_id)
return repo.name


def get_repo_by_repo_id(repo_id):
repo = (
db.session.query(Repo)
.filter(
Expand All @@ -32,7 +37,7 @@ def get_repo_name_by_repo_id(repo_id):
)
.first()
)
return repo.name
return repo


def get_bot_by_application_id(app_id):
Expand Down
49 changes: 42 additions & 7 deletions server/tasks/lark/chat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
import logging
import os
import re
from urllib.parse import urlparse

from celery_app import app, celery
Expand Down Expand Up @@ -313,13 +315,8 @@ def create_issue(
)
assignees = [code_users[openid][1] for openid in users if openid in code_users]

# 判断 content 中是否有 at
if "mentions" in data["event"]["message"]:
# 替换 content 中的 im_name 为 code_name
body = replace_im_name_to_github_name(
app_id, message_id, {"text": body}, data, team, *args, **kwargs
)
body = body.replace("\n", "\r\n")
# 处理 body
body = process_desc(app_id, message_id, repo.id, body, data, team, *args, **kwargs)

response = github_app.create_issue(
team.name, repo.name, title, body, assignees, labels
Expand All @@ -331,6 +328,44 @@ def create_issue(
return response


def process_desc(app_id, message_id, repo_id, desc, data, team, *args, **kwargs):
"""
处理发给 github 的 desc, 转换@、处理图片、换行
"""
# 1. 判断 body 中是否有 at
if "mentions" in data["event"]["message"]:
# 替换 body 中的 im_name 为 code_name
desc = replace_im_name_to_github_name(
app_id, message_id, {"text": desc}, data, team, *args, **kwargs
)

# 2. 处理 body 中的图片
desc = replace_images_keys_with_url(desc, team.id, message_id, repo_id)

# github 只支持 \r\n
return desc.replace("\n", "\r\n")


def replace_images_keys_with_url(text, team_id, message_id, repo_id):
"""
replace image_key with image URL.
![](image_key) -> ![](gitmaya.com/api/<team_id>/<repo_id>/<message_id>/image/<image_key>)
Args:
text (str): original text

Returns:
str: replaced text
"""
host = os.environ.get("DOMAIN")
replaced_text = re.sub(
r"!\[.*?\]\((.*?)\)",
lambda match: f"![]({host}/api/{team_id}/{repo_id}/{message_id}/image/{match.group(1)})",
text,
)

return replaced_text


@celery.task()
def sync_issue(
issue_id, issue_link, app_id, message_id, content, data, *args, **kwargs
Expand Down
22 changes: 11 additions & 11 deletions server/tasks/lark/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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 process_image

from .base import (
get_bot_by_application_id,
Expand Down Expand Up @@ -122,12 +122,12 @@ def gen_issue_card_by_issue(bot, issue, repo_url, team, maunal=False):
tags=tags,
)

# 处理 description 中的图片
# 处理从 github 创建 Issue 时, description 中的图片
description = replace_images_with_keys(
issue.description if issue.description else "", bot
)

# 处理 description 中的at
# 处理从 github 创建 Issue 时, description 中的 at
description = replace_code_name_to_im_name(description)

return IssueCard(
Expand Down Expand Up @@ -160,15 +160,15 @@ def replace_images_with_keys(text, bot):
markdown_pattern = r"!\[.*?\]\((.*?)\)"
replaced_text = re.sub(
markdown_pattern,
lambda match: f"![]({upload_image(match.group(1), bot)})",
lambda match: f"![]({process_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)})",
lambda match: f"![]({process_image(match.group(1), bot)})",
replaced_text,
)

Expand Down Expand Up @@ -601,12 +601,12 @@ def create_issue_comment(app_id, message_id, content, data, *args, **kwargs):
)
comment_text = content["text"]

# 判断 content 中是否有 at
if "mentions" in data["event"]["message"]:
# 替换 content 中的 im_name 为 code_name
comment_text = replace_im_name_to_github_name(
app_id, message_id, content, data, team, *args, **kwargs
)
from tasks.lark.chat import process_desc

# 处理 desc
comment_text = process_desc(
app_id, message_id, repo.id, comment_text, data, team, *args, **kwargs
)

response = github_app.create_issue_comment(
team.name, repo.name, issue.issue_number, comment_text
Expand Down
24 changes: 22 additions & 2 deletions server/utils/utils.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import logging
import os

import httpx
from utils.redis import stalecache


def process_image(url, bot):
if not url or not url.startswith("http"):
return ""

if url.startswith(f"{os.environ.get('DOMAIN')}/api"):
return url.split("/")[-1]
return upload_image(url, bot)


# 使用 stalecache 装饰器,以 url 作为缓存键
@stalecache(expire=3600, stale=600)
def upload_image(url, bot):
logging.info("upload image: %s", url)
if not url or not url.startswith("http"):
return ""
response = httpx.get(url, follow_redirects=True)
if response.status_code == 200:
# 函数返回值: iamg_key 存到缓存中
Expand All @@ -30,6 +38,18 @@ def upload_image_binary(img_bin, bot):
return response["data"]["image_key"]


@stalecache(expire=3600, stale=600)
def download_file(file_key, message_id, bot, file_type="image"):
"""
获取消息中的资源文件,包括音频,视频,图片和文件,暂不支持表情包资源下载。当前仅支持 100M 以内的资源文件的下载
"""
# open-apis/im/v1/images/{img_key} 接口只能下载机器人自己上传的图片
url = f"{bot.host}/open-apis/im/v1/messages/{message_id}/resources/{file_key}?type={file_type}"

response = bot.get(url)
return response.content


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