style: introduce ruff as linter and formatter (#1356)

* style: remove necessary imports

* style: fix F841

* style: fix F401

* style: fix F811

* style: fix E402

* style: fix E721

* style: fix E722

* style: fix E722

* style: fix F541

* style: ruff format

* style: all passed

* style: add ruff in deps

* style: more ignores in ruff.toml

* style: add pre-commit
This commit is contained in:
Junyan Qin (Chin)
2025-04-29 17:24:07 +08:00
committed by GitHub
parent 09e70d70e9
commit 209f16af76
240 changed files with 5307 additions and 4689 deletions

View File

@@ -1,10 +1,11 @@
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
"""对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2014 Tencent Inc.
"""
# ------------------------------------------------------------------------
import logging
import base64
@@ -49,7 +50,7 @@ class SHA1:
sortlist = [token, timestamp, nonce, encrypt]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist).encode())
sha.update(''.join(sortlist).encode())
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
logger = logging.getLogger()
@@ -75,7 +76,7 @@ class XMLParse:
"""
try:
xml_tree = ET.fromstring(xmltext)
encrypt = xml_tree.find("Encrypt")
encrypt = xml_tree.find('Encrypt')
return ierror.WXBizMsgCrypt_OK, encrypt.text
except Exception as e:
logger = logging.getLogger()
@@ -100,13 +101,13 @@ class XMLParse:
return resp_xml
class PKCS7Encoder():
class PKCS7Encoder:
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
"""对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
@@ -134,7 +135,6 @@ class Prpcrypt(object):
"""提供接收和推送给企业微信消息的加解密接口"""
def __init__(self, key):
# self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
@@ -147,7 +147,12 @@ class Prpcrypt(object):
"""
# 16位随机字符串添加到明文开头
text = text.encode()
text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode()
text = (
self.get_random_str()
+ struct.pack('I', socket.htonl(len(text)))
+ text
+ receiveid.encode()
)
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
@@ -183,9 +188,9 @@ class Prpcrypt(object):
# plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
xml_content = content[4: xml_len + 4]
from_receiveid = content[xml_len + 4:]
xml_len = socket.ntohl(struct.unpack('I', content[:4])[0])
xml_content = content[4 : xml_len + 4]
from_receiveid = content[xml_len + 4 :]
except Exception as e:
logger = logging.getLogger()
logger.error(e)
@@ -196,7 +201,7 @@ class Prpcrypt(object):
return 0, xml_content
def get_random_str(self):
""" 随机生成16位字符串
"""随机生成16位字符串
@return: 16位字符串
"""
return str(random.randint(1000000000000000, 9999999999999999)).encode()
@@ -206,10 +211,10 @@ class WXBizMsgCrypt(object):
# 构造函数
def __init__(self, sToken, sEncodingAESKey, sReceiveId):
try:
self.key = base64.b64decode(sEncodingAESKey + "=")
self.key = base64.b64decode(sEncodingAESKey + '=')
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
except Exception:
throw_exception('[error]: EncodingAESKey unvalid !', FormatException)
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId

View File

@@ -7,15 +7,22 @@ from quart import Quart
import xml.etree.ElementTree as ET
from typing import Callable, Dict, Any
from .wecomevent import WecomEvent
from pkg.platform.types import events as platform_events, message as platform_message
from pkg.platform.types import message as platform_message
import aiofiles
class WecomClient():
def __init__(self,corpid:str,secret:str,token:str,EncodingAESKey:str,contacts_secret:str):
class WecomClient:
def __init__(
self,
corpid: str,
secret: str,
token: str,
EncodingAESKey: str,
contacts_secret: str,
):
self.corpid = corpid
self.secret = secret
self.access_token_for_contacts =''
self.access_token_for_contacts = ''
self.token = token
self.aes = EncodingAESKey
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
@@ -23,19 +30,26 @@ class WecomClient():
self.secret_for_contacts = contacts_secret
self.app = Quart(__name__)
self.wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'])
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self._message_handlers = {
"example":[],
'example': [],
}
#access——token操作
# access——token操作
async def check_access_token(self):
return bool(self.access_token and self.access_token.strip())
async def check_access_token_for_contacts(self):
return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip())
return bool(
self.access_token_for_contacts and self.access_token_for_contacts.strip()
)
async def get_access_token(self,secret):
async def get_access_token(self, secret):
url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}'
async with httpx.AsyncClient() as client:
response = await client.get(url)
@@ -43,146 +57,163 @@ class WecomClient():
if 'access_token' in data:
return data['access_token']
else:
raise Exception(f"未获取access token: {data}")
raise Exception(f'未获取access token: {data}')
async def get_users(self):
if not self.check_access_token_for_contacts():
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
self.access_token_for_contacts = await self.get_access_token(
self.secret_for_contacts
)
url = self.base_url+'/user/list_id?access_token='+self.access_token_for_contacts
url = (
self.base_url
+ '/user/list_id?access_token='
+ self.access_token_for_contacts
)
async with httpx.AsyncClient() as client:
params = {
"cursor":"",
"limit":10000,
'cursor': '',
'limit': 10000,
}
response = await client.post(url,json=params)
response = await client.post(url, json=params)
data = response.json()
if data['errcode'] == 0:
dept_users = data['dept_user']
userid = []
for user in dept_users:
userid.append(user["userid"])
userid.append(user['userid'])
return userid
else:
raise Exception("未获取用户")
async def send_to_all(self,content:str,agent_id:int):
if not self.check_access_token_for_contacts():
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
raise Exception('未获取用户')
url = self.base_url+'/message/send?access_token='+self.access_token_for_contacts
async def send_to_all(self, content: str, agent_id: int):
if not self.check_access_token_for_contacts():
self.access_token_for_contacts = await self.get_access_token(
self.secret_for_contacts
)
url = (
self.base_url
+ '/message/send?access_token='
+ self.access_token_for_contacts
)
user_ids = await self.get_users()
user_ids_string = "|".join(user_ids)
user_ids_string = '|'.join(user_ids)
async with httpx.AsyncClient() as client:
params = {
"touser" : user_ids_string,
"msgtype" : "text",
"agentid" : agent_id,
"text" : {
"content" : content,
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
'touser': user_ids_string,
'msgtype': 'text',
'agentid': agent_id,
'text': {
'content': content,
},
'safe': 0,
'enable_id_trans': 0,
'enable_duplicate_check': 0,
'duplicate_check_interval': 1800,
}
response = await client.post(url,json=params)
response = await client.post(url, json=params)
data = response.json()
if data['errcode'] != 0:
raise Exception("Failed to send message: "+str(data))
raise Exception('Failed to send message: ' + str(data))
async def send_image(self,user_id:str,agent_id:int,media_id:str):
async def send_image(self, user_id: str, agent_id: int, media_id: str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url+'/media/upload?access_token='+self.access_token
url = self.base_url + '/media/upload?access_token=' + self.access_token
async with httpx.AsyncClient() as client:
params = {
"touser" : user_id,
"toparty" : "",
"totag":"",
"agentid" : agent_id,
"msgtype" : "image",
"image" : {
"media_id" : media_id,
'touser': user_id,
'toparty': '',
'totag': '',
'agentid': agent_id,
'msgtype': 'image',
'image': {
'media_id': media_id,
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
'safe': 0,
'enable_id_trans': 0,
'enable_duplicate_check': 0,
'duplicate_check_interval': 1800,
}
try:
response = await client.post(url,json=params)
response = await client.post(url, json=params)
data = response.json()
except Exception as e:
raise Exception("Failed to send image: "+str(e))
raise Exception('Failed to send image: ' + str(e))
# 企业微信错误码40014和42001代表accesstoken问题
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
return await self.send_image(user_id,agent_id,media_id)
return await self.send_image(user_id, agent_id, media_id)
if data['errcode'] != 0:
raise Exception("Failed to send image: "+str(data))
async def send_private_msg(self,user_id:str, agent_id:int,content:str):
raise Exception('Failed to send image: ' + str(data))
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url+'/message/send?access_token='+self.access_token
url = self.base_url + '/message/send?access_token=' + self.access_token
async with httpx.AsyncClient() as client:
params={
"touser" : user_id,
"msgtype" : "text",
"agentid" : agent_id,
"text" : {
"content" : content,
params = {
'touser': user_id,
'msgtype': 'text',
'agentid': agent_id,
'text': {
'content': content,
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
'safe': 0,
'enable_id_trans': 0,
'enable_duplicate_check': 0,
'duplicate_check_interval': 1800,
}
response = await client.post(url,json=params)
response = await client.post(url, json=params)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
return await self.send_private_msg(user_id,agent_id,content)
return await self.send_private_msg(user_id, agent_id, content)
if data['errcode'] != 0:
raise Exception("Failed to send message: "+str(data))
raise Exception('Failed to send message: ' + str(data))
async def handle_callback_request(self):
"""
处理回调请求,包括 GET 验证和 POST 消息接收。
"""
try:
msg_signature = request.args.get('msg_signature')
timestamp = request.args.get('timestamp')
nonce = request.args.get('nonce')
msg_signature = request.args.get("msg_signature")
timestamp = request.args.get("timestamp")
nonce = request.args.get("nonce")
if request.method == "GET":
echostr = request.args.get("echostr")
ret, reply_echo_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if request.method == 'GET':
echostr = request.args.get('echostr')
ret, reply_echo_str = self.wxcpt.VerifyURL(
msg_signature, timestamp, nonce, echostr
)
if ret != 0:
raise Exception(f"验证失败,错误码: {ret}")
raise Exception(f'验证失败,错误码: {ret}')
return reply_echo_str
elif request.method == "POST":
elif request.method == 'POST':
encrypt_msg = await request.data
ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
ret, xml_msg = self.wxcpt.DecryptMsg(
encrypt_msg, msg_signature, timestamp, nonce
)
if ret != 0:
raise Exception(f"消息解密失败,错误码: {ret}")
raise Exception(f'消息解密失败,错误码: {ret}')
# 解析消息并处理
message_data = await self.get_message(xml_msg)
if message_data:
event = WecomEvent.from_payload(message_data) # 转换为 WecomEvent 对象
event = WecomEvent.from_payload(
message_data
) # 转换为 WecomEvent 对象
if event:
await self._handle_message(event)
return "success"
return 'success'
except Exception as e:
return f"Error processing request: {str(e)}", 400
return f'Error processing request: {str(e)}', 400
async def run_task(self, host: str, port: int, *args, **kwargs):
"""
@@ -194,11 +225,13 @@ class WecomClient():
"""
注册消息类型处理器。
"""
def decorator(func: Callable[[WecomEvent], None]):
if msg_type not in self._message_handlers:
self._message_handlers[msg_type] = []
self._message_handlers[msg_type].append(func)
return func
return decorator
async def _handle_message(self, event: WecomEvent):
@@ -216,38 +249,47 @@ class WecomClient():
"""
root = ET.fromstring(xml_msg)
message_data = {
"ToUserName": root.find("ToUserName").text,
"FromUserName": root.find("FromUserName").text,
"CreateTime": int(root.find("CreateTime").text),
"MsgType": root.find("MsgType").text,
"Content": root.find("Content").text if root.find("Content") is not None else None,
"MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None,
"AgentID": int(root.find("AgentID").text) if root.find("AgentID") is not None else None,
'ToUserName': root.find('ToUserName').text,
'FromUserName': root.find('FromUserName').text,
'CreateTime': int(root.find('CreateTime').text),
'MsgType': root.find('MsgType').text,
'Content': root.find('Content').text
if root.find('Content') is not None
else None,
'MsgId': int(root.find('MsgId').text)
if root.find('MsgId') is not None
else None,
'AgentID': int(root.find('AgentID').text)
if root.find('AgentID') is not None
else None,
}
if message_data["MsgType"] == "image":
message_data["MediaId"] = root.find("MediaId").text if root.find("MediaId") is not None else None
message_data["PicUrl"] = root.find("PicUrl").text if root.find("PicUrl") is not None else None
if message_data['MsgType'] == 'image':
message_data['MediaId'] = (
root.find('MediaId').text if root.find('MediaId') is not None else None
)
message_data['PicUrl'] = (
root.find('PicUrl').text if root.find('PicUrl') is not None else None
)
return message_data
@staticmethod
async def get_image_type(image_bytes: bytes) -> str:
"""
通过图片的magic numbers判断图片类型
"""
magic_numbers = {
b'\xFF\xD8\xFF': 'jpg',
b'\x89\x50\x4E\x47': 'png',
b'\xff\xd8\xff': 'jpg',
b'\x89\x50\x4e\x47': 'png',
b'\x47\x49\x46': 'gif',
b'\x42\x4D': 'bmp',
b'\x00\x00\x01\x00': 'ico'
b'\x42\x4d': 'bmp',
b'\x00\x00\x01\x00': 'ico',
}
for magic, ext in magic_numbers.items():
if image_bytes.startswith(magic):
return ext
return 'jpg' # 默认返回jpg
async def upload_to_work(self, image: platform_message.Image):
"""
@@ -256,9 +298,14 @@ class WecomClient():
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file'
url = (
self.base_url
+ '/media/upload?access_token='
+ self.access_token
+ '&type=file'
)
file_bytes = None
file_name = "uploaded_file.txt"
file_name = 'uploaded_file.txt'
# 获取文件的二进制数据
if image.path:
@@ -277,20 +324,22 @@ class WecomClient():
padded_base64 = base64_data + '=' * padding
file_bytes = base64.b64decode(padded_base64)
except binascii.Error as e:
raise ValueError(f"Invalid base64 string: {str(e)}")
raise ValueError(f'Invalid base64 string: {str(e)}')
else:
raise ValueError("image对象出错")
raise ValueError('image对象出错')
# 设置 multipart/form-data 格式的文件
boundary = "-------------------------acebdf13572468"
headers = {
'Content-Type': f'multipart/form-data; boundary={boundary}'
}
boundary = '-------------------------acebdf13572468'
headers = {'Content-Type': f'multipart/form-data; boundary={boundary}'}
body = (
f"--{boundary}\r\n"
f"Content-Disposition: form-data; name=\"media\"; filename=\"{file_name}\"; filelength={len(file_bytes)}\r\n"
f"Content-Type: application/octet-stream\r\n\r\n"
).encode('utf-8') + file_bytes + f"\r\n--{boundary}--\r\n".encode('utf-8')
(
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="media"; filename="{file_name}"; filelength={len(file_bytes)}\r\n'
f'Content-Type: application/octet-stream\r\n\r\n'
).encode('utf-8')
+ file_bytes
+ f'\r\n--{boundary}--\r\n'.encode('utf-8')
)
# 上传文件
async with httpx.AsyncClient() as client:
@@ -300,19 +349,18 @@ class WecomClient():
self.access_token = await self.get_access_token(self.secret)
media_id = await self.upload_to_work(image)
if data.get('errcode', 0) != 0:
raise Exception("failed to upload file")
raise Exception('failed to upload file')
media_id = data.get('media_id')
return media_id
async def download_image_to_bytes(self,url:str) -> bytes:
async def download_image_to_bytes(self, url: str) -> bytes:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.content
#进行media_id的获取
# 进行media_id的获取
async def get_media_id(self, image: platform_message.Image):
media_id = await self.upload_to_work(image=image)
return media_id

View File

@@ -4,7 +4,7 @@
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
@@ -17,4 +17,4 @@ WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011
WXBizMsgCrypt_GenReturnXml_Error = -40011

View File

@@ -9,7 +9,7 @@ class WecomEvent(dict):
"""
@staticmethod
def from_payload(payload: Dict[str, Any]) -> Optional["WecomEvent"]:
def from_payload(payload: Dict[str, Any]) -> Optional['WecomEvent']:
"""
从企业微信事件数据构造 `WecomEvent` 对象。
@@ -34,14 +34,14 @@ class WecomEvent(dict):
Returns:
str: 事件类型。
"""
return self.get("MsgType", "")
return self.get('MsgType', '')
@property
def picurl(self) -> str:
"""
图片链接
"""
return self.get("PicUrl")
return self.get('PicUrl')
@property
def detail_type(self) -> str:
@@ -53,8 +53,8 @@ class WecomEvent(dict):
Returns:
str: 事件详细类型。
"""
if self.type == "event":
return self.get("Event", "")
if self.type == 'event':
return self.get('Event', '')
return self.type
@property
@@ -65,7 +65,7 @@ class WecomEvent(dict):
Returns:
str: 事件名。
"""
return f"{self.type}.{self.detail_type}"
return f'{self.type}.{self.detail_type}'
@property
def user_id(self) -> Optional[str]:
@@ -75,8 +75,8 @@ class WecomEvent(dict):
Returns:
Optional[str]: 用户 ID。
"""
return self.get("FromUserName")
return self.get('FromUserName')
@property
def agent_id(self) -> Optional[int]:
"""
@@ -85,7 +85,7 @@ class WecomEvent(dict):
Returns:
Optional[int]: 机器人 ID。
"""
return self.get("AgentID")
return self.get('AgentID')
@property
def receiver_id(self) -> Optional[str]:
@@ -95,7 +95,7 @@ class WecomEvent(dict):
Returns:
Optional[str]: 接收者 ID。
"""
return self.get("ToUserName")
return self.get('ToUserName')
@property
def message_id(self) -> Optional[str]:
@@ -105,7 +105,7 @@ class WecomEvent(dict):
Returns:
Optional[str]: 消息 ID。
"""
return self.get("MsgId")
return self.get('MsgId')
@property
def message(self) -> Optional[str]:
@@ -115,7 +115,7 @@ class WecomEvent(dict):
Returns:
Optional[str]: 消息内容。
"""
return self.get("Content")
return self.get('Content')
@property
def media_id(self) -> Optional[str]:
@@ -125,7 +125,7 @@ class WecomEvent(dict):
Returns:
Optional[str]: 媒体文件 ID。
"""
return self.get("MediaId")
return self.get('MediaId')
@property
def timestamp(self) -> Optional[int]:
@@ -135,7 +135,7 @@ class WecomEvent(dict):
Returns:
Optional[int]: 时间戳。
"""
return self.get("CreateTime")
return self.get('CreateTime')
@property
def event_key(self) -> Optional[str]:
@@ -145,7 +145,7 @@ class WecomEvent(dict):
Returns:
Optional[str]: 事件 Key。
"""
return self.get("EventKey")
return self.get('EventKey')
def __getattr__(self, key: str) -> Optional[Any]:
"""
@@ -176,4 +176,4 @@ class WecomEvent(dict):
Returns:
str: 字符串表示。
"""
return f"<WecomEvent {super().__repr__()}>"
return f'<WecomEvent {super().__repr__()}>'