255 lines
6.4 KiB
Python
255 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
钉钉 Webhook 通知模块
|
||
用于发送构建和部署通知到钉钉群
|
||
"""
|
||
|
||
import time
|
||
import hmac
|
||
import hashlib
|
||
import base64
|
||
import urllib.parse
|
||
import urllib.request
|
||
import json
|
||
from datetime import datetime
|
||
|
||
|
||
class DingTalkNotifier:
|
||
"""钉钉通知器"""
|
||
|
||
def __init__(self, access_token, secret=None):
|
||
"""
|
||
初始化钉钉通知器
|
||
|
||
Args:
|
||
access_token: 钉钉机器人的 access_token
|
||
secret: 钉钉机器人的加签密钥(可选)
|
||
"""
|
||
self.access_token = access_token
|
||
self.secret = secret
|
||
self.base_url = "https://oapi.dingtalk.com/robot/send"
|
||
|
||
def _generate_sign(self, timestamp):
|
||
"""
|
||
生成钉钉加签
|
||
|
||
Args:
|
||
timestamp: 当前时间戳(毫秒)
|
||
|
||
Returns:
|
||
签名字符串
|
||
"""
|
||
if not self.secret:
|
||
return None
|
||
|
||
string_to_sign = f'{timestamp}\n{self.secret}'
|
||
string_to_sign_enc = string_to_sign.encode('utf-8')
|
||
secret_enc = self.secret.encode('utf-8')
|
||
|
||
hmac_code = hmac.new(
|
||
secret_enc,
|
||
string_to_sign_enc,
|
||
digestmod=hashlib.sha256
|
||
).digest()
|
||
|
||
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||
return sign
|
||
|
||
def _build_url(self):
|
||
"""
|
||
构建完整的 webhook URL
|
||
|
||
Returns:
|
||
完整的 URL 字符串
|
||
"""
|
||
url = f"{self.base_url}?access_token={self.access_token}"
|
||
|
||
if self.secret:
|
||
timestamp = str(round(time.time() * 1000))
|
||
sign = self._generate_sign(timestamp)
|
||
url += f"×tamp={timestamp}&sign={sign}"
|
||
|
||
return url
|
||
|
||
def send_text(self, content, at_mobiles=None, at_all=False):
|
||
"""
|
||
发送文本消息
|
||
|
||
Args:
|
||
content: 消息内容
|
||
at_mobiles: @的手机号列表
|
||
at_all: 是否@所有人
|
||
|
||
Returns:
|
||
发送结果 (True/False)
|
||
"""
|
||
data = {
|
||
"msgtype": "text",
|
||
"text": {
|
||
"content": content
|
||
},
|
||
"at": {
|
||
"atMobiles": at_mobiles or [],
|
||
"isAtAll": at_all
|
||
}
|
||
}
|
||
|
||
return self._send_request(data)
|
||
|
||
def send_build_success(self, repo_name, branch, commit_hash, duration):
|
||
"""
|
||
发送构建成功通知
|
||
|
||
Args:
|
||
repo_name: 仓库名称
|
||
branch: 分支名称
|
||
commit_hash: 提交哈希
|
||
duration: 构建耗时(秒)
|
||
|
||
Returns:
|
||
发送结果 (True/False)
|
||
"""
|
||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
title = f"✅ 构建成功 - {repo_name}"
|
||
text = f"""### ✅ 构建成功
|
||
|
||
**仓库**: {repo_name}
|
||
**分支**: {branch}
|
||
**提交**: {commit_hash[:8]}
|
||
**耗时**: {duration:.1f} 秒
|
||
**时间**: {now}
|
||
|
||
---
|
||
构建和部署已完成!
|
||
"""
|
||
return self.send_markdown(title, text)
|
||
|
||
def send_build_failure(self, repo_name, branch, commit_hash, error_msg, at_all=False):
|
||
"""
|
||
发送构建失败通知
|
||
|
||
Args:
|
||
repo_name: 仓库名称
|
||
branch: 分支名称
|
||
commit_hash: 提交哈希
|
||
error_msg: 错误信息
|
||
at_all: 是否@所有人
|
||
|
||
Returns:
|
||
发送结果 (True/False)
|
||
"""
|
||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
title = f"❌ 构建失败 - {repo_name}"
|
||
# 限制错误信息长度
|
||
error_display = error_msg[:500] if len(error_msg) > 500 else error_msg
|
||
text = f"""### ❌ 构建失败
|
||
|
||
**仓库**: {repo_name}
|
||
**分支**: {branch}
|
||
**提交**: {commit_hash[:8]}
|
||
**时间**: {now}
|
||
|
||
**错误信息**:
|
||
```
|
||
{error_display}
|
||
```
|
||
|
||
---
|
||
请检查日志并修复问题!
|
||
"""
|
||
return self.send_markdown(title, text, at_all=at_all)
|
||
|
||
def send_build_start(self, repo_name, branch, commit_hash, commit_message=None, server_ip=None):
|
||
"""
|
||
发送构建开始通知
|
||
|
||
Args:
|
||
repo_name: 仓库名称
|
||
branch: 分支名称
|
||
commit_hash: 提交哈希
|
||
commit_message: 提交消息(可选)
|
||
server_ip: 服务器IP(可选)
|
||
|
||
Returns:
|
||
发送结果 (True/False)
|
||
"""
|
||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
title = f"🚀 构建开始 - {repo_name}"
|
||
|
||
# 构建消息内容
|
||
text = f"""### 🚀 构建开始
|
||
|
||
**仓库**: {repo_name}
|
||
**分支**: {branch}
|
||
**提交**: {commit_hash[:8]}"""
|
||
|
||
if commit_message:
|
||
text += f"\n**消息**: {commit_message}"
|
||
|
||
if server_ip:
|
||
text += f"\n**服务器**: {server_ip}"
|
||
|
||
text += f"""
|
||
**时间**: {now}
|
||
|
||
---
|
||
构建任务已启动,请稍候...
|
||
"""
|
||
return self.send_markdown(title, text)
|
||
|
||
def _send_request(self, data):
|
||
"""
|
||
发送 HTTP 请求到钉钉 webhook
|
||
|
||
Args:
|
||
data: 请求数据字典
|
||
|
||
Returns:
|
||
发送结果 (True/False)
|
||
"""
|
||
try:
|
||
url = self._build_url()
|
||
headers = {'Content-Type': 'application/json'}
|
||
json_data = json.dumps(data).encode('utf-8')
|
||
|
||
req = urllib.request.Request(url, data=json_data, headers=headers)
|
||
response = urllib.request.urlopen(req, timeout=10)
|
||
result = json.loads(response.read().decode('utf-8'))
|
||
|
||
if result.get('errcode') == 0:
|
||
return True
|
||
else:
|
||
print(f"钉钉通知发送失败: {result.get('errmsg')}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"钉钉通知发送异常: {e}")
|
||
return False
|
||
|
||
def send_markdown(self, title, text, at_mobiles=None, at_all=False):
|
||
"""
|
||
发送 Markdown 消息
|
||
|
||
Args:
|
||
title: 消息标题
|
||
text: Markdown 格式的消息内容
|
||
at_mobiles: @的手机号列表
|
||
at_all: 是否@所有人
|
||
|
||
Returns:
|
||
发送结果 (True/False)
|
||
"""
|
||
data = {
|
||
"msgtype": "markdown",
|
||
"markdown": {
|
||
"title": title,
|
||
"text": text
|
||
},
|
||
"at": {
|
||
"atMobiles": at_mobiles or [],
|
||
"isAtAll": at_all
|
||
}
|
||
}
|
||
|
||
return self._send_request(data) |