#!/usr/bin/env python3 """ 消息服务 提供加载会话存档、解密、保存到数据库的整合功能 """ import asyncio import json import os import logging import base64 from pathlib import Path from datetime import datetime from typing import List, Dict, Any, Optional import sys # 添加scripts目录到路径 sys.path.insert(0, str(Path(__file__).parent.parent.parent / "scripts")) from wework_ctypes import WeWorkFinanceSDK, WeWorkSDKError from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.backends import default_backend logger = logging.getLogger(__name__) class MessageService: """消息服务,提供加载会话存档的整合功能""" def __init__(self, corpid: Optional[str] = None, secret: Optional[str] = None): """ 初始化消息服务 Args: corpid: 企业ID,如果为None则从环境变量CORPID读取 secret: 聊天内容存档的Secret,如果为None则从环境变量WEWORK_SECRET读取 """ self.corpid = corpid or os.getenv("CORPID", "wwdf65802ca25ec195") self.secret = secret or os.getenv( "WEWORK_SECRET", "-Ta6WMWxBhfGolWnnlO15nQckj3DRKAowUOdX2fwvzE" ) # SDK实例(延迟初始化) self._sdk: Optional[WeWorkFinanceSDK] = None # RSA私钥路径 self.rsa_private_key_path = Path("cert/1_rsa.key") logger.info(f"消息服务初始化完成,企业ID: {self.corpid}") @property def sdk(self) -> WeWorkFinanceSDK: """获取SDK实例(延迟初始化)""" if self._sdk is None: try: # 动态库路径 lib_path = ( Path(__file__).parent.parent.parent / "lib" / "sdk" / "libWeWorkFinanceSdk_C.so" ) if not lib_path.exists(): raise FileNotFoundError(f"动态库不存在: {lib_path}") self._sdk = WeWorkFinanceSDK(self.corpid, self.secret, str(lib_path)) logger.info("企业微信SDK初始化成功") except Exception as e: logger.error(f"企业微信SDK初始化失败: {e}") raise return self._sdk def _load_rsa_private_key(self, key_path: str) -> str: """ 加载RSA私钥 Args: key_path: 私钥文件路径 Returns: 私钥内容字符串 """ try: with open(key_path, "r") as f: key_content = f.read() return key_content except Exception as e: raise Exception(f"加载RSA私钥失败: {e}") def _rsa_decrypt_base64(self, encrypted_data_base64: str, private_key: str) -> str: """ 使用RSA私钥解密Base64编码的数据 Args: encrypted_data_base64: Base64编码的加密数据 private_key: RSA私钥字符串 Returns: 解密后的字符串 """ try: # 加载私钥 private_key_obj = serialization.load_pem_private_key( private_key.encode("utf-8"), password=None, backend=default_backend() ) # Base64解码 encrypted_data = base64.b64decode(encrypted_data_base64) # RSA解密 (使用PKCS1v15填充) decrypted_data = private_key_obj.decrypt(encrypted_data, padding.PKCS1v15()) return decrypted_data.decode("utf-8") except ImportError: raise Exception("需要安装cryptography库: pip install cryptography") except Exception as e: raise Exception(f"RSA解密失败: {e}") def _decrypt_chat_message( self, encrypt_random_key: str, encrypt_chat_msg: str, publickey_ver: int = None ) -> dict: """ 解密聊天消息 Args: encrypt_random_key: 加密的随机密钥(Base64编码) encrypt_chat_msg: 加密的聊天消息 publickey_ver: publickey_ver版本号,用于动态匹配私钥 Returns: 解密后的消息字典 """ # 动态确定私钥路径 actual_key_path = self.rsa_private_key_path # 如果提供了publickey_ver,则动态匹配 if publickey_ver is not None: dynamic_key_path = f"cert/{publickey_ver}_rsa.key" dynamic_path = Path(dynamic_key_path) if dynamic_path.exists(): actual_key_path = dynamic_path # 1. 加载RSA私钥 private_key = self._load_rsa_private_key(str(actual_key_path)) # 2. 使用RSA私钥解密encrypt_random_key得到encrypt_key try: encrypt_key = self._rsa_decrypt_base64(encrypt_random_key, private_key) except Exception as e: # 尝试直接使用encrypt_random_key作为encrypt_key(某些情况下可能已经解密) encrypt_key = encrypt_random_key # 3. 使用SDK解密聊天消息 decrypted_msg = self.sdk.decrypt_data(encrypt_key, encrypt_chat_msg) # 4. 解析JSON try: result = json.loads(decrypted_msg) return result except json.JSONDecodeError: # 如果不是JSON,返回原始文本 return {"raw_message": decrypted_msg} async def _decrypt_chat_data(self, chatdata: List[Dict[str, Any]]) -> tuple: """ 解密聊天数据 Args: chatdata: 原始聊天数据列表 Returns: (decrypted_chatdata, stats) decrypted_chatdata: 解密后的聊天数据列表 stats: 解密统计信息字典 """ decrypted_chatdata = [] stats = { "total_encrypted_messages": 0, "successful_decrypts": 0, "failed_decrypts": 0, } # 检查私钥文件是否存在 if not self.rsa_private_key_path.exists(): logger.warning(f"RSA私钥文件不存在: {self.rsa_private_key_path}") logger.warning("将跳过解密,保存原始加密数据") return chatdata, stats logger.info(f"使用RSA私钥: {self.rsa_private_key_path.absolute()}") logger.info("开始解密聊天数据...") for i, msg in enumerate(chatdata): encrypt_random_key = msg.get("encrypt_random_key", "") encrypt_chat_msg = msg.get("encrypt_chat_msg", "") # 检查是否有加密数据 if not encrypt_random_key or not encrypt_chat_msg: # 没有加密数据,直接添加到结果中 decrypted_chatdata.append(msg) continue stats["total_encrypted_messages"] += 1 try: # 获取publickey_ver publickey_ver = msg.get("publickey_ver") # 解密消息 decrypted_msg = self._decrypt_chat_message( encrypt_random_key, encrypt_chat_msg, publickey_ver, ) # 创建新的消息对象,保留原始字段但移除加密字段 new_msg = {} for key, value in msg.items(): if key not in ["encrypt_random_key", "encrypt_chat_msg"]: new_msg[key] = value # 将解密后的字段合并到消息中 if isinstance(decrypted_msg, dict): for key, value in decrypted_msg.items(): new_msg[key] = value decrypted_chatdata.append(new_msg) stats["successful_decrypts"] += 1 # 显示进度 if stats["successful_decrypts"] % 100 == 0: logger.info(f" 已解密 {stats['successful_decrypts']} 条消息...") except Exception as e: stats["failed_decrypts"] += 1 logger.warning(f"解密消息失败 (seq: {msg.get('seq', 'N/A')}): {e}") # 解密失败,跳过此消息(不添加到结果中) continue return decrypted_chatdata, stats async def _save_messages_to_database( self, messages: List[Dict[str, Any]] ) -> Dict[str, int]: """ 将消息保存到数据库 Args: messages: 消息字典列表 Returns: 存储统计信息字典 """ stats = { "total_messages": len(messages), "saved": 0, "skipped_duplicates": 0, "failed": 0, } if not messages: logger.info("没有消息需要存储到数据库") return stats logger.info(f"开始将 {len(messages)} 条消息存储到数据库...") try: # 导入数据库相关模块 from pymongo import AsyncMongoClient from beanie import init_beanie from app.models import models from app.models.message import Message # 从环境变量获取数据库配置 mongodb_url = os.getenv( "MONGODB_URL", "mongodb://dblink:mgdb4link@localhost:27017/?authSource=admin", ) database_name = os.getenv("DATABASE_NAME", "douba_chat_dev") # 初始化数据库连接 client = AsyncMongoClient(mongodb_url) await init_beanie(database=client[database_name], document_models=models) logger.info(f"数据库连接成功: {database_name}") # 批量处理消息 batch_size = 100 for i in range(0, len(messages), batch_size): batch = messages[i : i + batch_size] batch_msgids = [msg.get("msgid") for msg in batch if msg.get("msgid")] # 检查哪些消息已经存在 existing_messages = await Message.find( {"msgid": {"$in": batch_msgids}} ).to_list() existing_msgids = {msg.msgid for msg in existing_messages} # 过滤掉已存在的消息 new_messages = [] for msg in batch: msgid = msg.get("msgid") if not msgid: stats["failed"] += 1 continue if msgid in existing_msgids: stats["skipped_duplicates"] += 1 continue try: # 创建Message实例 message_data = { "msgid": msg.get("msgid"), "action": msg.get("action"), "from_user": msg.get( "from" ), # 从原始数据的from字段读取,保存为from_user "tolist": msg.get("tolist", []), "roomid": msg.get("roomid"), "text": msg.get("text", {}), "msgtime": msg.get("msgtime"), "msgtype": msg.get("msgtype"), "remark": msg.get("remark"), "seq": msg.get("seq", 1000), "isEnabled": True, "created_at": datetime.now(), "updated_at": datetime.now(), } # 移除None值 message_data = { k: v for k, v in message_data.items() if v is not None } message = Message(**message_data) new_messages.append(message) except Exception as e: logger.error(f"创建消息实例失败 (msgid: {msgid}): {e}") stats["failed"] += 1 # 批量插入新消息 if new_messages: try: await Message.insert_many(new_messages) stats["saved"] += len(new_messages) logger.info( f" 已存储 {len(new_messages)} 条消息 (进度: {i + len(batch)}/{len(messages)})" ) except Exception as e: logger.error(f"批量插入失败: {e}") stats["failed"] += len(new_messages) # 关闭数据库连接 await client.close() logger.info(f"数据库存储完成:") logger.info(f" 总计消息: {stats['total_messages']}") logger.info(f" 成功存储: {stats['saved']}") logger.info(f" 跳过重复: {stats['skipped_duplicates']}") logger.info(f" 存储失败: {stats['failed']}") except Exception as e: logger.error(f"数据库存储过程中发生错误: {e}") import traceback traceback.print_exc() stats["failed"] = len(messages) return stats async def get_messages( self, page: int = 1, page_size: int = 20, msgtype: Optional[str] = None, roomid: Optional[str] = None, from_user: Optional[str] = None, ) -> Dict[str, Any]: """ 查询消息列表,支持分页和过滤 Args: page: 页码(从1开始) page_size: 每页数量 msgtype: 消息类型过滤 roomid: 群聊ID过滤 from_user: 发送者过滤 Returns: 分页查询结果 """ try: # 导入数据库相关模块 from pymongo import AsyncMongoClient from beanie import init_beanie from app.models import models from app.models.message import Message # 从环境变量获取数据库配置 mongodb_url = os.getenv( "MONGODB_URL", "mongodb://dblink:mgdb4link@localhost:27017/?authSource=admin", ) database_name = os.getenv("DATABASE_NAME", "douba_chat_dev") # 初始化数据库连接 client = AsyncMongoClient(mongodb_url) await init_beanie(database=client[database_name], document_models=models) # 构建查询条件 query = {} if msgtype: query["msgtype"] = msgtype if roomid: query["roomid"] = roomid if from_user: query["from_user"] = from_user # 使用from_user字段进行查询 # 计算跳过的文档数量 skip_count = (page - 1) * page_size # 执行查询 messages = await Message.find( query, skip=skip_count, limit=page_size, sort=[("msgtime", -1)], # 按消息时间倒序排列 ).to_list() # 获取总记录数 total_count = await Message.find(query).count() # 关闭数据库连接 await client.close() # 准备响应数据 return { "success": True, "data": { "page": page, "page_size": page_size, "total": total_count, "total_pages": ( (total_count + page_size - 1) // page_size if page_size > 0 else 0 ), "messages": messages, }, } except Exception as e: logger.error(f"查询消息列表失败: {e}") import traceback traceback.print_exc() return { "success": False, "message": f"查询消息列表失败: {str(e)}", "data": { "page": page, "page_size": page_size, "total": 0, "total_pages": 0, "messages": [], }, } async def load_messages(self, seq: int = 0, limit: int = 100) -> Dict[str, Any]: """ 加载会话存档到数据库 完整流程:拉取 → 解密 → 保存 Args: seq: 起始seq值,首次使用0 limit: 拉取数量限制 Returns: 执行结果和统计信息 """ start_time = datetime.now() try: # 从环境变量获取代理配置 proxy = os.getenv("WEWORK_PROXY", "") passwd = os.getenv("WEWORK_PROXY_PASSWD", "") timeout = int(os.getenv("WEWORK_TIMEOUT", "10")) logger.info(f"开始加载会话存档: seq={seq}, limit={limit}") # 1. 拉取会话数据 logger.info("步骤1: 从企业微信拉取会话数据...") result_str = self.sdk.get_chat_data( seq=seq, limit=limit, proxy=proxy, passwd=passwd, timeout=timeout ) # 解析结果 result = json.loads(result_str) errcode = result.get("errcode", -1) errmsg = result.get("errmsg", "") if errcode != 0: raise Exception(f"拉取会话数据失败: errcode={errcode}, errmsg={errmsg}") chatdata = result.get("chatdata", []) total_fetched = len(chatdata) logger.info(f"成功拉取 {total_fetched} 条聊天记录") if total_fetched == 0: return { "success": True, "message": "没有新的聊天记录", "data": { "total_fetched": 0, "encrypted_count": 0, "decrypted_success": 0, "decrypted_failed": 0, "saved_count": 0, "duplicates_skipped": 0, "max_seq": seq, "next_seq": seq, }, "execution_time": str(datetime.now() - start_time), } # 2. 解密消息 logger.info("步骤2: 解密加密消息...") decrypted_chatdata, decrypt_stats = await self._decrypt_chat_data(chatdata) # 3. 保存到数据库 logger.info("步骤3: 保存消息到数据库...") db_stats = await self._save_messages_to_database(decrypted_chatdata) # 计算最大seq值 max_seq = max( (msg.get("seq", 0) for msg in decrypted_chatdata), default=seq ) # 准备响应数据 execution_time = datetime.now() - start_time response_data = { "success": True, "message": "消息加载完成", "data": { "total_fetched": total_fetched, "encrypted_count": decrypt_stats["total_encrypted_messages"], "decrypted_success": decrypt_stats["successful_decrypts"], "decrypted_failed": decrypt_stats["failed_decrypts"], "saved_count": db_stats["saved"], "duplicates_skipped": db_stats["skipped_duplicates"], "failed_to_save": db_stats["failed"], "max_seq": max_seq, "next_seq": max_seq + 1 if total_fetched > 0 else seq, }, "execution_time": str(execution_time), "timestamp": datetime.now().isoformat(), } logger.info(f"消息加载完成,耗时: {execution_time}") return response_data except json.JSONDecodeError as e: error_msg = f"解析JSON数据失败: {e}" logger.error(error_msg) return { "success": False, "message": error_msg, "execution_time": str(datetime.now() - start_time), } except Exception as e: error_msg = f"加载消息失败: {e}" logger.error(error_msg) import traceback traceback.print_exc() return { "success": False, "message": error_msg, "execution_time": str(datetime.now() - start_time), } def close(self): """关闭服务,释放资源""" if self._sdk: self._sdk.close() self._sdk = None logger.info("消息服务已关闭") # 全局服务实例(单例模式) _message_service: Optional[MessageService] = None def get_message_service() -> MessageService: """获取消息服务实例(单例)""" global _message_service if _message_service is None: _message_service = MessageService() return _message_service def close_message_service(): """关闭消息服务""" global _message_service if _message_service: _message_service.close() _message_service = None