Skip to content

Commit 6e19a37

Browse files
committed
feat(reply): add reply module, implement reply factory and add Gitlab and Dingtalk reply handlers
1 parent dca5a9e commit 6e19a37

File tree

7 files changed

+229
-0
lines changed

7 files changed

+229
-0
lines changed

reply_module/__init__.py

Whitespace-only changes.

reply_module/reply.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import threading
2+
3+
from reply_module.reply_target.reply_factory import ReplyFactory
4+
5+
6+
class Reply:
7+
def __init__(self, project_id, merge_request_id):
8+
self.replies = []
9+
self.lock = threading.Lock()
10+
self.project_id = project_id
11+
self.merge_request_id = merge_request_id
12+
13+
def add_reply(self, reply):
14+
# reply 格式检查:title, content 必选
15+
if 'title' not in reply or 'content' not in reply:
16+
raise Exception('Reply format error, title and content are required.')
17+
if 'priority' in reply:
18+
if not isinstance(reply['priority'], int):
19+
raise Exception('Reply format error, priority should be an integer.')
20+
elif reply['priority'] == 0:
21+
self.send_single_message(reply)
22+
return
23+
24+
with self.lock: # 加锁
25+
self.replies.append(reply)
26+
27+
def send(self):
28+
markdown_message = ""
29+
with self.lock: # 加锁
30+
# 发送所有消息的逻辑
31+
for reply in self.replies:
32+
markdown_message += f"## {reply['title']}\n\n{reply['content']}\n\n"
33+
self.replies = [] # 清空已发送的消息
34+
reply_target = ReplyFactory.get_reply_instance(reply['target'], self.project_id, self.merge_request_id)
35+
return reply_target.send(markdown_message)
36+
37+
def send_single_message(self, reply):
38+
"""
39+
实时发送消息
40+
"""
41+
reply_target = ReplyFactory.get_reply_instance(reply['target'], self.project_id, self.merge_request_id)
42+
return reply_target.send(reply['content'])
43+
44+
45+
if __name__ == '__main__':
46+
reply = Reply(9885, 18)
47+
threads = []
48+
for i in range(10):
49+
threads.append(threading.Thread(target=reply.add_reply, args=(
50+
{'title': f'title{i}', 'content': f'content{i}', 'target': 'gitlab', 'priority': i % 3},)))
51+
for thread in threads:
52+
thread.start()
53+
for thread in threads:
54+
thread.join()
55+
reply.send()

reply_module/reply_target/__init__.py

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from abc import ABC, abstractmethod
2+
3+
class AbstractReply(ABC):
4+
@abstractmethod
5+
def __init__(self, project_id, merge_request_id):
6+
self.project_id = project_id
7+
self.merge_request_id = merge_request_id
8+
9+
@abstractmethod
10+
def send(self, message):
11+
pass
12+
13+
# # 发送失败调用
14+
# @abstractmethod
15+
# def send_failed(self, message):
16+
# pass
17+
#
18+
# # 发送成功调用
19+
# @abstractmethod
20+
# def send_success(self, message):
21+
# pass
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import base64
2+
import hashlib
3+
import hmac
4+
import time
5+
import urllib
6+
import requests
7+
import json
8+
from config.config import *
9+
from utils.logger import *
10+
from reply_module.reply_target.abstract_reply import AbstractReply
11+
12+
13+
class DingtalkReply(AbstractReply):
14+
def __init__(self, project_id, merge_request_id):
15+
super().__init__(project_id, merge_request_id)
16+
17+
def send(self, message):
18+
return self.send_dingtalk_message_by_sign(message)
19+
20+
def send_dingtalk_message_by_sign(self, message_text):
21+
"""
22+
使用签名方式发送消息通知到钉钉群
23+
24+
Args:
25+
message_text (str): 消息文本内容
26+
27+
Returns:
28+
bool: 消息是否发送成功
29+
"""
30+
timestamp = str(round(time.time() * 1000))
31+
sign = self.__get_sign(timestamp)
32+
webhookurl = f"{dingding_bot_webhook}&timestamp={timestamp}&sign={sign}"
33+
# 构建请求头
34+
headers = {
35+
"Content-Type": "application/json",
36+
}
37+
38+
# 构建请求体
39+
message = {
40+
"msgtype": "markdown",
41+
"markdown": {
42+
"title": "Gitlab 通知",
43+
"text": message_text
44+
},
45+
"timestamp": timestamp,
46+
"sign": sign
47+
}
48+
49+
# 发送HTTP POST请求
50+
response = requests.post(
51+
webhookurl,
52+
headers=headers,
53+
data=json.dumps(message)
54+
)
55+
56+
# 检查响应
57+
if response.status_code == 200 and response.json()["errcode"] == 0:
58+
log.info(f"评论信息发送成功:project_id:{self.project_id} merge_request_id:{self.merge_request_id}")
59+
return True
60+
else:
61+
log.error(
62+
f"评论信息发送失败:project_id:{self.project_id} merge_request_id:{self.merge_request_id} response:{response}")
63+
return False
64+
65+
def send_dingtalk_message_by_key_word(self, project_url):
66+
"""
67+
通过关键词发送
68+
69+
"""
70+
# 设置钉钉机器人的 Webhook URL
71+
webhook_url = dingding_bot_webhook
72+
73+
# 要发送的消息内容
74+
message = f"新工程接入\nurl:{project_url}"
75+
76+
headers = {"Content-Type": "application/json"}
77+
payload = {
78+
"msgtype": "text",
79+
"text": {
80+
"content": message
81+
}
82+
}
83+
response = requests.post(webhook_url, headers=headers, data=json.dumps(payload))
84+
return response.json()
85+
86+
def __get_sign(self, timestamp):
87+
'''
88+
计算签名
89+
:param timestamp: 时间戳
90+
:return: 签名
91+
'''
92+
93+
secret = dingding_secret
94+
secret_enc = secret.encode('utf-8')
95+
string_to_sign = '{}\n{}'.format(timestamp, secret)
96+
string_to_sign_enc = string_to_sign.encode('utf-8')
97+
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
98+
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
99+
return sign
100+
101+
if __name__ == '__main__':
102+
dingtalk = DingtalkReply(1, 1)
103+
dingtalk.send("test message")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import requests
2+
from retrying import retry
3+
from config.config import *
4+
from reply_module.reply_target.abstract_reply import AbstractReply
5+
from utils.logger import log
6+
7+
# 继承AbstractReply类,实现send方法
8+
class GitlabReply(AbstractReply):
9+
def __init__(self, project_id, merge_request_id):
10+
super().__init__(project_id, merge_request_id)
11+
12+
@retry(stop_max_attempt_number=3, wait_fixed=2000)
13+
def send(self, message):
14+
headers = {
15+
"Private-Token": gitlab_private_token,
16+
"Content-Type": "application/json"
17+
}
18+
project_id = self.project_id
19+
merge_request_id = self.merge_request_id
20+
url = f"{gitlab_server_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/notes"
21+
data = {
22+
"body": message
23+
}
24+
25+
response = requests.post(url, headers=headers, json=data)
26+
27+
if response.status_code == 201:
28+
log.info(f"评论信息发送成功:project_id:{project_id} merge_request_id:{merge_request_id}")
29+
return True
30+
else:
31+
log.error(
32+
f"评论信息发送成功:project_id:{project_id} merge_request_id:{merge_request_id} response:{response}")
33+
return False
34+
35+
if __name__ == '__main__':
36+
gitlab_reply = GitlabReply(9885, 18)
37+
gitlab_reply.send("test")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from reply_module.reply_target.dingtalk_reply import DingtalkReply
2+
from reply_module.reply_target.gitlab_reply import GitlabReply
3+
4+
5+
class ReplyFactory:
6+
@staticmethod
7+
def get_reply_instance(target, project_id, merge_request_id):
8+
if target == 'gitlab':
9+
return GitlabReply(project_id, merge_request_id)
10+
elif target == 'dingtalk':
11+
return DingtalkReply(project_id, merge_request_id)
12+
else:
13+
raise ValueError(f"Unknown target: {target}")

0 commit comments

Comments
 (0)