This commit is contained in:
aaron 2025-02-26 21:40:34 +08:00
parent ea29a879e4
commit d7ffde37bc

137
app/api/endpoints/wecom.py Normal file
View File

@ -0,0 +1,137 @@
from fastapi import APIRouter, Request, Response, Query
from app.core.wecombot import WecomBot
from app.models.database import get_db
from app.models.community import CommunityDB
from app.core.config import settings
import xml.etree.ElementTree as ET
import logging
import hashlib
import base64
from Crypto.Cipher import AES
import typing
import struct
router = APIRouter()
def decrypt_msg(msg_encrypt: str, signature: str, timestamp: str, nonce: str) -> typing.Optional[str]:
"""解密企业微信消息"""
try:
token = settings.WECHAT_CORP_TOKEN
encoding_aes_key = settings.WECHAT_CORP_ENCODING_AES_KEY
corpid = settings.WECHAT_CORP_ID
# 1. 验证签名
sort_list = [token, timestamp, nonce, msg_encrypt]
sort_list.sort()
sha1 = hashlib.sha1()
sha1.update("".join(sort_list).encode())
calc_signature = sha1.hexdigest()
if calc_signature != signature:
return None
# 2. 解密消息
aes_key = base64.b64decode(encoding_aes_key + "=")
aes = AES.new(aes_key, AES.MODE_CBC, aes_key[:16])
# 解密
text = base64.b64decode(msg_encrypt)
decrypted_text = aes.decrypt(text)
# 去除补位
pad = decrypted_text[-1]
if not isinstance(pad, int):
pad = ord(pad)
content = decrypted_text[:-pad]
# 验证corpid
xml_len = struct.unpack('!I', content[16:20])[0]
xml_content = content[20 : 20 + xml_len]
from_corpid = content[20 + xml_len:]
if from_corpid.decode() != corpid:
return None
return xml_content.decode()
except Exception as e:
logging.exception("解密企业微信消息失败")
return None
@router.get("/callback")
async def verify_callback(
msg_signature: str = Query(..., description="签名"),
timestamp: str = Query(..., description="时间戳"),
nonce: str = Query(..., description="随机数"),
echostr: str = Query(..., description="随机字符串")
):
"""验证回调配置"""
try:
# 解密echostr
decrypted_str = decrypt_msg(echostr, msg_signature, timestamp, nonce)
if not decrypted_str:
return Response(status_code=403)
return Response(content=decrypted_str, media_type="text/plain")
except Exception as e:
logging.exception("验证回调配置失败")
return Response(status_code=403)
@router.post("/callback")
async def wechat_corp_callback(
request: Request,
msg_signature: str = Query(..., description="签名"),
timestamp: str = Query(..., description="时间戳"),
nonce: str = Query(..., description="随机数")
):
"""处理企业微信回调消息"""
try:
# 读取原始XML数据
body = await request.body()
body_str = body.decode()
# 解析XML获取加密消息
root = ET.fromstring(body_str)
encrypt_msg = root.find('Encrypt').text
# 解密消息
decrypted_msg = decrypt_msg(encrypt_msg, msg_signature, timestamp, nonce)
if not decrypted_msg:
return Response(content="success", media_type="text/plain")
# 解析解密后的XML
msg_root = ET.fromstring(decrypted_msg)
print(f"企业微信回调消息:{decrypted_msg}")
# 解析基本信息
msg_type = msg_root.find('MsgType').text
# 处理事件消息
if msg_type == 'event':
event = msg_root.find('Event').text
# 处理进群事件
if event == 'change_external_chat':
chat_id = msg_root.find('ChatId').text
change_type = msg_root.find('ChangeType').text
if change_type == 'add_member':
# 获取数据库会话
db = next(get_db())
# 查找对应的小区
community = db.query(CommunityDB).filter(
CommunityDB.webot_webhook.isnot(None)
).first()
if community and community.webot_webhook:
# 发送欢迎消息
wecom_bot = WecomBot()
await wecom_bot.send_welcome_message(community.webot_webhook)
return Response(content="success", media_type="text/plain")
except Exception as e:
logging.exception("处理企业微信回调消息异常")
return Response(content="success", media_type="text/plain")