1332 lines
42 KiB
Python
1332 lines
42 KiB
Python
"""
|
||
WechatHookClient - 微信 Hook API 客户端
|
||
|
||
封装所有微信操作的高级 API
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import uuid
|
||
from typing import List, Dict, Optional
|
||
from loguru import logger
|
||
|
||
from .loader import NoveLoader
|
||
from .message_types import MessageType
|
||
from .callbacks import RECV_CALLBACK, add_callback_handler
|
||
|
||
|
||
class WechatHookClient:
|
||
"""
|
||
微信 Hook API 客户端
|
||
|
||
提供统一的异步 API 接口
|
||
"""
|
||
|
||
def __init__(self, loader: NoveLoader, client_id: int):
|
||
"""
|
||
初始化客户端
|
||
|
||
Args:
|
||
loader: NoveLoader 实例
|
||
client_id: 客户端 ID(进程 ID)
|
||
"""
|
||
self.loader = loader
|
||
self.client_id = client_id
|
||
|
||
# 存储待处理的API请求
|
||
self.pending_requests = {}
|
||
|
||
# 注册回调处理器
|
||
add_callback_handler(self)
|
||
|
||
logger.info(f"WechatHookClient 初始化: client_id={client_id}")
|
||
|
||
async def _log_bot_message(self, to_wxid: str, content: str, msg_type: str = "text", media_url: str = ""):
|
||
"""记录机器人发送的消息到 MessageLogger"""
|
||
try:
|
||
logger.info(f"尝试记录机器人消息: {to_wxid} - {content[:50]}...")
|
||
from utils.message_hook import log_bot_message
|
||
await log_bot_message(to_wxid, content, msg_type, media_url)
|
||
logger.info(f"机器人消息记录成功")
|
||
except Exception as e:
|
||
logger.error(f"记录机器人消息失败: {e}")
|
||
import traceback
|
||
logger.error(f"详细错误: {traceback.format_exc()}")
|
||
|
||
def _send_data(self, msg_type: int, data: dict) -> bool:
|
||
"""
|
||
发送数据到微信(同步)
|
||
|
||
Args:
|
||
msg_type: 消息类型
|
||
data: 消息数据
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
payload = {
|
||
"type": msg_type,
|
||
"data": data
|
||
}
|
||
message = json.dumps(payload, ensure_ascii=False)
|
||
return self.loader.SendWeChatData(self.client_id, message)
|
||
|
||
async def _send_data_async(self, msg_type: int, data: dict) -> bool:
|
||
"""
|
||
发送数据到微信(异步)
|
||
|
||
Args:
|
||
msg_type: 消息类型
|
||
data: 消息数据
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
return await asyncio.to_thread(self._send_data, msg_type, data)
|
||
|
||
# ==================== 消息发送 ====================
|
||
|
||
async def send_text(self, to_wxid: str, content: str) -> bool:
|
||
"""
|
||
发送文本消息
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
content: 文本内容
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"content": content
|
||
}
|
||
result = await self._send_data_async(MessageType.MT_SEND_TEXT, data)
|
||
if result:
|
||
logger.info(f"发送文本成功: {to_wxid}")
|
||
# 记录机器人发送的消息到 MessageLogger
|
||
await self._log_bot_message(to_wxid, content, "text")
|
||
else:
|
||
logger.error(f"发送文本失败: {to_wxid}")
|
||
return result
|
||
|
||
async def send_image(self, to_wxid: str, image_path: str) -> bool:
|
||
"""
|
||
发送图片消息
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
image_path: 图片文件路径
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"file": image_path
|
||
}
|
||
# 使用正确的图片发送API类型
|
||
result = await self._send_data_async(11040, data)
|
||
if result:
|
||
logger.info(f"发送图片成功: {to_wxid}")
|
||
# 记录机器人发送的图片消息
|
||
import os
|
||
filename = os.path.basename(image_path)
|
||
await self._log_bot_message(to_wxid, f"[图片] {filename}", "image", image_path)
|
||
else:
|
||
logger.error(f"发送图片失败: {to_wxid}")
|
||
return result
|
||
|
||
async def send_file(self, to_wxid: str, file_path: str) -> bool:
|
||
"""
|
||
发送文件消息(普通发送)
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
file_path: 文件路径
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"file": file_path # 注意:参数名是 "file" 而不是 "file_path"
|
||
}
|
||
result = await self._send_data_async(11041, data) # 使用正确的 type=11041
|
||
if result:
|
||
logger.info(f"发送文件成功: {to_wxid}")
|
||
# 记录机器人发送的文件消息
|
||
import os
|
||
filename = os.path.basename(file_path)
|
||
await self._log_bot_message(to_wxid, f"[文件] {filename}", "file", file_path)
|
||
else:
|
||
logger.error(f"发送文件失败: {to_wxid}")
|
||
return result
|
||
|
||
async def send_video(self, to_wxid: str, video_path: str) -> bool:
|
||
"""
|
||
发送视频消息
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
video_path: 视频文件路径
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"video_path": video_path
|
||
}
|
||
result = await self._send_data_async(11039, data)
|
||
if result:
|
||
logger.info(f"发送视频成功: {to_wxid}")
|
||
# 记录机器人发送的视频消息
|
||
import os
|
||
filename = os.path.basename(video_path)
|
||
await self._log_bot_message(to_wxid, f"[视频] {filename}", "video", video_path)
|
||
else:
|
||
logger.error(f"发送视频失败: {to_wxid}")
|
||
return result
|
||
|
||
async def send_at_message(
|
||
self,
|
||
chatroom_id: str,
|
||
content: str,
|
||
at_list: List[str]
|
||
) -> bool:
|
||
"""
|
||
发送群聊 @ 消息
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
content: 消息内容
|
||
at_list: 要 @ 的 wxid 列表,["notify@all"] 表示 @所有人
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"chatroom_id": chatroom_id,
|
||
"content": content,
|
||
"at_list": at_list
|
||
}
|
||
result = await self._send_data_async(11040, data)
|
||
if result:
|
||
logger.info(f"发送 @ 消息成功: {chatroom_id}")
|
||
else:
|
||
logger.error(f"发送 @ 消息失败: {chatroom_id}")
|
||
return result
|
||
|
||
async def send_card(
|
||
self,
|
||
to_wxid: str,
|
||
card_wxid: str,
|
||
card_nickname: str
|
||
) -> bool:
|
||
"""
|
||
发送名片消息
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
card_wxid: 名片的 wxid
|
||
card_nickname: 名片昵称
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"card_wxid": card_wxid,
|
||
"card_nickname": card_nickname
|
||
}
|
||
result = await self._send_data_async(11041, data)
|
||
if result:
|
||
logger.info(f"发送名片成功: {to_wxid}")
|
||
else:
|
||
logger.error(f"发送名片失败: {to_wxid}")
|
||
return result
|
||
|
||
async def send_link(
|
||
self,
|
||
to_wxid: str,
|
||
title: str,
|
||
desc: str,
|
||
url: str,
|
||
thumb_url: str = ""
|
||
) -> bool:
|
||
"""
|
||
发送链接消息
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
title: 链接标题
|
||
desc: 链接描述
|
||
url: 链接地址
|
||
thumb_url: 缩略图 URL
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"title": title,
|
||
"desc": desc,
|
||
"url": url,
|
||
"thumb_url": thumb_url
|
||
}
|
||
result = await self._send_data_async(11042, data)
|
||
if result:
|
||
logger.info(f"发送链接成功: {to_wxid}")
|
||
else:
|
||
logger.error(f"发送链接失败: {to_wxid}")
|
||
return result
|
||
|
||
async def cdn_init(self) -> bool:
|
||
"""
|
||
初始化 CDN 环境
|
||
|
||
用于初始化 CDN 环境,收到登录消息后执行一次即可。
|
||
初始化后才能使用协议 API(如获取群成员信息等)。
|
||
|
||
Returns:
|
||
是否初始化成功
|
||
"""
|
||
result = await self._send_data_async(11228, {})
|
||
if result:
|
||
logger.success("CDN 初始化成功")
|
||
else:
|
||
logger.error("CDN 初始化失败")
|
||
return result
|
||
|
||
async def cdn_download(self, file_id: str, aes_key: str, save_path: str, file_type: int = 2) -> bool:
|
||
"""
|
||
CDN 下载文件(图片/视频等)
|
||
|
||
Args:
|
||
file_id: 文件 ID(从消息中获取的 cdnbigimgurl 等)
|
||
aes_key: AES 密钥
|
||
save_path: 保存路径
|
||
file_type: 文件类型 (1=原图, 2=中图, 3=缩略图, 4=视频, 5=文件&GIF)
|
||
|
||
Returns:
|
||
是否下载成功
|
||
"""
|
||
data = {
|
||
"file_id": file_id,
|
||
"file_type": file_type,
|
||
"aes_key": aes_key,
|
||
"save_path": save_path
|
||
}
|
||
result = await self._send_data_async(11230, data)
|
||
if result:
|
||
logger.info(f"CDN 下载成功: {save_path}")
|
||
else:
|
||
logger.error(f"CDN 下载失败: {file_id}")
|
||
return result
|
||
|
||
async def cdn_upload(self, file_path: str, file_type: int = 1) -> Optional[Dict]:
|
||
"""
|
||
CDN 上传文件
|
||
|
||
Args:
|
||
file_path: 文件路径
|
||
file_type: 文件类型 (1=原图, 2=中图, 3=缩略图, 4=视频, 5=文件&GIF)
|
||
|
||
Returns:
|
||
上传结果字典,包含 aes_key, file_md5 等信息
|
||
失败返回 None
|
||
"""
|
||
# 生成唯一请求ID
|
||
request_id = str(uuid.uuid4())
|
||
|
||
# 创建等待事件
|
||
event = asyncio.Event()
|
||
result_data = {"cdn_info": None}
|
||
|
||
# 存储待处理请求
|
||
request_key = f"cdn_upload_{request_id}"
|
||
self.pending_requests[request_key] = {
|
||
"request_id": request_id,
|
||
"event": event,
|
||
"result": result_data,
|
||
"type": "cdn_upload",
|
||
"file_path": file_path
|
||
}
|
||
|
||
data = {
|
||
"file_path": file_path,
|
||
"file_type": file_type
|
||
}
|
||
await self._send_data_async(11229, data)
|
||
logger.info(f"CDN 上传请求已发送: {file_path}")
|
||
|
||
# 等待回调结果
|
||
try:
|
||
await asyncio.wait_for(event.wait(), timeout=30)
|
||
cdn_info = result_data["cdn_info"]
|
||
if cdn_info and cdn_info.get("error_code") == 0:
|
||
logger.success(f"CDN 上传成功: {file_path}")
|
||
return cdn_info
|
||
else:
|
||
error_code = cdn_info.get("error_code") if cdn_info else "unknown"
|
||
logger.error(f"CDN 上传失败: {file_path}, error_code={error_code}")
|
||
return None
|
||
except asyncio.TimeoutError:
|
||
logger.error(f"CDN 上传超时: {file_path}")
|
||
return None
|
||
finally:
|
||
if request_key in self.pending_requests:
|
||
del self.pending_requests[request_key]
|
||
|
||
async def send_link_card(
|
||
self,
|
||
to_wxid: str,
|
||
title: str,
|
||
desc: str,
|
||
url: str,
|
||
image_url: str = ""
|
||
) -> bool:
|
||
"""
|
||
发送链接卡片(CDN发送)
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
title: 卡片标题
|
||
desc: 卡片描述
|
||
url: 链接地址
|
||
image_url: 卡片图片 URL
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"title": title,
|
||
"desc": desc,
|
||
"url": url,
|
||
"image_url": image_url
|
||
}
|
||
result = await self._send_data_async(11236, data)
|
||
if result:
|
||
logger.info(f"发送链接卡片成功: {to_wxid}")
|
||
else:
|
||
logger.error(f"发送链接卡片失败: {to_wxid}")
|
||
return result
|
||
|
||
async def send_link_card_and_get_response(
|
||
self,
|
||
to_wxid: str,
|
||
title: str,
|
||
desc: str,
|
||
url: str,
|
||
image_url: str = "",
|
||
timeout_sec: int = 15
|
||
) -> Optional[dict]:
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"title": title,
|
||
"desc": desc,
|
||
"url": url,
|
||
"image_url": image_url
|
||
}
|
||
request_key = f"send_link_card_{to_wxid}"
|
||
event = asyncio.Event()
|
||
self.pending_requests[request_key] = {"event": event, "result": None}
|
||
await self._send_data_async(11236, data)
|
||
try:
|
||
await asyncio.wait_for(event.wait(), timeout=timeout_sec)
|
||
result = self.pending_requests[request_key]["result"]
|
||
del self.pending_requests[request_key]
|
||
return result
|
||
except asyncio.TimeoutError:
|
||
del self.pending_requests[request_key]
|
||
return None
|
||
|
||
async def revoke_message(self, msg_id: str) -> bool:
|
||
"""
|
||
撤回消息
|
||
|
||
Args:
|
||
msg_id: 消息 ID
|
||
|
||
Returns:
|
||
是否撤回成功
|
||
"""
|
||
data = {"msg_id": msg_id}
|
||
result = await self._send_data_async(11043, data)
|
||
if result:
|
||
logger.info(f"撤回消息成功: {msg_id}")
|
||
else:
|
||
logger.error(f"撤回消息失败: {msg_id}")
|
||
return result
|
||
|
||
# ==================== 好友管理 ====================
|
||
|
||
async def get_friend_list(self) -> List[Dict]:
|
||
"""
|
||
获取好友列表
|
||
|
||
Returns:
|
||
好友列表
|
||
"""
|
||
# 需要实际测试确认返回格式
|
||
data = {}
|
||
await self._send_data_async(11050, data)
|
||
logger.info("请求好友列表")
|
||
return []
|
||
|
||
async def get_friend_info(self, wxid: str) -> Optional[Dict]:
|
||
"""
|
||
获取好友信息
|
||
|
||
Args:
|
||
wxid: 好友 wxid
|
||
|
||
Returns:
|
||
好友信息字典
|
||
"""
|
||
data = {"wxid": wxid}
|
||
await self._send_data_async(11051, data)
|
||
logger.info(f"请求好友信息: {wxid}")
|
||
return None
|
||
|
||
async def add_friend(
|
||
self,
|
||
wxid: str,
|
||
verify_msg: str = "",
|
||
scene: int = 3
|
||
) -> bool:
|
||
"""
|
||
添加好友
|
||
|
||
Args:
|
||
wxid: 要添加的 wxid
|
||
verify_msg: 验证消息
|
||
scene: 添加场景(3=搜索,15=名片)
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
data = {
|
||
"wxid": wxid,
|
||
"verify_msg": verify_msg,
|
||
"scene": scene
|
||
}
|
||
result = await self._send_data_async(11052, data)
|
||
if result:
|
||
logger.info(f"发送好友请求成功: {wxid}")
|
||
else:
|
||
logger.error(f"发送好友请求失败: {wxid}")
|
||
return result
|
||
|
||
async def accept_friend(self, v3: str, v4: str, scene: int) -> bool:
|
||
"""
|
||
同意好友请求
|
||
|
||
Args:
|
||
v3: 好友请求的 v3 参数
|
||
v4: 好友请求的 v4 参数
|
||
scene: 场景值
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {
|
||
"v3": v3,
|
||
"v4": v4,
|
||
"scene": scene
|
||
}
|
||
result = await self._send_data_async(11053, data)
|
||
if result:
|
||
logger.info("同意好友请求成功")
|
||
else:
|
||
logger.error("同意好友请求失败")
|
||
return result
|
||
|
||
async def delete_friend(self, wxid: str) -> bool:
|
||
"""
|
||
删除好友
|
||
|
||
Args:
|
||
wxid: 要删除的好友 wxid
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {"wxid": wxid}
|
||
result = await self._send_data_async(11054, data)
|
||
if result:
|
||
logger.info(f"删除好友成功: {wxid}")
|
||
else:
|
||
logger.error(f"删除好友失败: {wxid}")
|
||
return result
|
||
|
||
async def set_friend_remark(self, wxid: str, remark: str) -> bool:
|
||
"""
|
||
修改好友备注
|
||
|
||
Args:
|
||
wxid: 好友 wxid
|
||
remark: 新备注
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {
|
||
"wxid": wxid,
|
||
"remark": remark
|
||
}
|
||
result = await self._send_data_async(11055, data)
|
||
if result:
|
||
logger.info(f"修改备注成功: {wxid} -> {remark}")
|
||
else:
|
||
logger.error(f"修改备注失败: {wxid}")
|
||
return result
|
||
|
||
# ==================== 群聊管理 ====================
|
||
|
||
async def get_chatroom_list(self) -> List[Dict]:
|
||
"""
|
||
获取群聊列表
|
||
|
||
Returns:
|
||
群聊列表
|
||
"""
|
||
data = {}
|
||
await self._send_data_async(11060, data)
|
||
logger.info("请求群聊列表")
|
||
return []
|
||
|
||
async def get_chatroom_members(self, chatroom_id: str) -> List[Dict]:
|
||
"""
|
||
获取群成员列表(使用协议 API)
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
|
||
Returns:
|
||
群成员列表,每个成员包含: wxid, nickname, display_name, avatar
|
||
"""
|
||
# 生成唯一请求ID
|
||
request_id = str(uuid.uuid4())
|
||
|
||
# 创建等待事件
|
||
event = asyncio.Event()
|
||
result_data = {"members": [], "success": False}
|
||
|
||
# 存储待处理请求
|
||
self.pending_requests[f"chatroom_info_{chatroom_id}"] = {
|
||
"request_id": request_id,
|
||
"event": event,
|
||
"result": result_data,
|
||
"type": "chatroom_info"
|
||
}
|
||
|
||
# 使用 type=11174 获取群信息(协议),包含成员列表和头像
|
||
data = {"wxid": chatroom_id}
|
||
await self._send_data_async(11174, data)
|
||
logger.info(f"请求群信息(协议): {chatroom_id}, request_id: {request_id}")
|
||
|
||
# 等待回调结果
|
||
members = await self._wait_for_chatroom_info(chatroom_id, timeout=15)
|
||
|
||
return members
|
||
|
||
async def _wait_for_chatroom_info(self, chatroom_id: str, timeout: int = 15) -> List[Dict]:
|
||
"""等待群信息回调(type=11174)"""
|
||
request_key = f"chatroom_info_{chatroom_id}"
|
||
if request_key not in self.pending_requests:
|
||
logger.error(f"未找到待处理的群信息请求: {chatroom_id}")
|
||
return []
|
||
|
||
request_info = self.pending_requests[request_key]
|
||
event = request_info["event"]
|
||
|
||
try:
|
||
# 等待回调事件,设置超时
|
||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||
|
||
# 获取结果
|
||
result = request_info["result"]["members"]
|
||
logger.success(f"获取群成员成功(协议): {chatroom_id}, 成员数: {len(result)}")
|
||
return result
|
||
|
||
except asyncio.TimeoutError:
|
||
logger.error(f"获取群信息超时: {chatroom_id}")
|
||
return []
|
||
finally:
|
||
# 清理请求
|
||
if request_key in self.pending_requests:
|
||
del self.pending_requests[request_key]
|
||
|
||
async def _wait_for_chatroom_members(self, chatroom_id: str, timeout: int = 15) -> List[Dict]:
|
||
"""等待群成员信息回调(已废弃,保留用于兼容)"""
|
||
if chatroom_id not in self.pending_requests:
|
||
logger.error(f"未找到待处理的群成员请求: {chatroom_id}")
|
||
return []
|
||
|
||
request_info = self.pending_requests[chatroom_id]
|
||
event = request_info["event"]
|
||
|
||
try:
|
||
# 等待回调事件,设置超时
|
||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||
|
||
# 获取结果
|
||
result = request_info["result"]["members"]
|
||
logger.success(f"获取群成员成功: {chatroom_id}, 成员数: {len(result)}")
|
||
return result
|
||
|
||
except asyncio.TimeoutError:
|
||
logger.error(f"获取群成员超时: {chatroom_id}")
|
||
return []
|
||
finally:
|
||
# 清理请求
|
||
if chatroom_id in self.pending_requests:
|
||
del self.pending_requests[chatroom_id]
|
||
|
||
async def _get_chatroom_members_fallback(self, chatroom_id: str) -> List[Dict]:
|
||
"""
|
||
备用方案:使用11031获取群成员基本信息
|
||
|
||
Args:
|
||
chatroom_id: 群聊ID
|
||
|
||
Returns:
|
||
群成员列表(仅包含wxid)
|
||
"""
|
||
try:
|
||
# 生成唯一请求ID
|
||
request_id = str(uuid.uuid4())
|
||
|
||
# 创建等待事件
|
||
event = asyncio.Event()
|
||
result_data = {"chatrooms": []}
|
||
|
||
# 存储待处理请求
|
||
self.pending_requests[f"chatroom_list_{request_id}"] = {
|
||
"request_id": request_id,
|
||
"event": event,
|
||
"result": result_data,
|
||
"type": "chatroom_list",
|
||
"target_chatroom": chatroom_id
|
||
}
|
||
|
||
# 发送获取群聊列表请求(包含成员列表)
|
||
data = {"detail": 1}
|
||
await self._send_data_async(11031, data)
|
||
logger.info(f"请求群聊列表(备用方案): {chatroom_id}, request_id: {request_id}")
|
||
|
||
# 等待回调结果
|
||
try:
|
||
await asyncio.wait_for(event.wait(), timeout=15)
|
||
|
||
# 查找目标群聊的成员列表
|
||
chatrooms = result_data["chatrooms"]
|
||
for chatroom in chatrooms:
|
||
if chatroom.get("wxid") == chatroom_id:
|
||
member_list = chatroom.get("member_list", [])
|
||
# 转换为标准格式
|
||
members = []
|
||
for wxid in member_list:
|
||
members.append({
|
||
"wxid": wxid,
|
||
"nickname": wxid, # 使用wxid作为备用昵称
|
||
"display_name": "" # 群内昵称为空
|
||
})
|
||
logger.success(f"备用方案获取群成员成功: {chatroom_id}, 成员数: {len(members)}")
|
||
return members
|
||
|
||
logger.warning(f"在群聊列表中未找到目标群聊: {chatroom_id}")
|
||
return []
|
||
|
||
except asyncio.TimeoutError:
|
||
logger.error(f"备用方案获取群成员超时: {chatroom_id}")
|
||
return []
|
||
finally:
|
||
# 清理请求
|
||
key = f"chatroom_list_{request_id}"
|
||
if key in self.pending_requests:
|
||
del self.pending_requests[key]
|
||
|
||
except Exception as e:
|
||
logger.error(f"备用方案获取群成员失败: {e}")
|
||
return []
|
||
|
||
async def get_chatroom_info(self, chatroom_id: str) -> Optional[Dict]:
|
||
"""
|
||
获取群信息
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
|
||
Returns:
|
||
群信息字典
|
||
"""
|
||
data = {"chatroom_id": chatroom_id}
|
||
await self._send_data_async(11062, data)
|
||
logger.info(f"请求群信息: {chatroom_id}")
|
||
return None
|
||
|
||
async def create_chatroom(self, member_list: List[str]) -> Optional[str]:
|
||
"""
|
||
创建群聊
|
||
|
||
Args:
|
||
member_list: 成员 wxid 列表(至少2人)
|
||
|
||
Returns:
|
||
新群聊的 chatroom_id
|
||
"""
|
||
data = {"member_list": member_list}
|
||
result = await self._send_data_async(11063, data)
|
||
if result:
|
||
logger.info("创建群聊成功")
|
||
else:
|
||
logger.error("创建群聊失败")
|
||
return None
|
||
|
||
async def invite_to_chatroom(
|
||
self,
|
||
chatroom_id: str,
|
||
wxid_list: List[str]
|
||
) -> bool:
|
||
"""
|
||
邀请进群
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
wxid_list: 要邀请的 wxid 列表
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {
|
||
"chatroom_id": chatroom_id,
|
||
"wxid_list": wxid_list
|
||
}
|
||
result = await self._send_data_async(11064, data)
|
||
if result:
|
||
logger.info(f"邀请进群成功: {chatroom_id}")
|
||
else:
|
||
logger.error(f"邀请进群失败: {chatroom_id}")
|
||
return result
|
||
|
||
async def remove_chatroom_member(
|
||
self,
|
||
chatroom_id: str,
|
||
wxid_list: List[str]
|
||
) -> bool:
|
||
"""
|
||
踢出群成员
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
wxid_list: 要踢出的 wxid 列表
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {
|
||
"chatroom_id": chatroom_id,
|
||
"wxid_list": wxid_list
|
||
}
|
||
result = await self._send_data_async(11065, data)
|
||
if result:
|
||
logger.info(f"踢出群成员成功: {chatroom_id}")
|
||
else:
|
||
logger.error(f"踢出群成员失败: {chatroom_id}")
|
||
return result
|
||
|
||
async def quit_chatroom(self, chatroom_id: str) -> bool:
|
||
"""
|
||
退出群聊
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {"chatroom_id": chatroom_id}
|
||
result = await self._send_data_async(11066, data)
|
||
if result:
|
||
logger.info(f"退出群聊成功: {chatroom_id}")
|
||
else:
|
||
logger.error(f"退出群聊失败: {chatroom_id}")
|
||
return result
|
||
|
||
async def set_chatroom_name(self, chatroom_id: str, name: str) -> bool:
|
||
"""
|
||
修改群名称
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
name: 新群名称
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {
|
||
"chatroom_id": chatroom_id,
|
||
"name": name
|
||
}
|
||
result = await self._send_data_async(11067, data)
|
||
if result:
|
||
logger.info(f"修改群名称成功: {chatroom_id}")
|
||
else:
|
||
logger.error(f"修改群名称失败: {chatroom_id}")
|
||
return result
|
||
|
||
async def set_chatroom_announcement(
|
||
self,
|
||
chatroom_id: str,
|
||
announcement: str
|
||
) -> bool:
|
||
"""
|
||
修改群公告
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
announcement: 群公告内容
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {
|
||
"chatroom_id": chatroom_id,
|
||
"announcement": announcement
|
||
}
|
||
result = await self._send_data_async(11068, data)
|
||
if result:
|
||
logger.info(f"修改群公告成功: {chatroom_id}")
|
||
else:
|
||
logger.error(f"修改群公告失败: {chatroom_id}")
|
||
return result
|
||
|
||
async def set_my_chatroom_nickname(
|
||
self,
|
||
chatroom_id: str,
|
||
nickname: str
|
||
) -> bool:
|
||
"""
|
||
修改我的群昵称
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
nickname: 新昵称
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
data = {
|
||
"chatroom_id": chatroom_id,
|
||
"nickname": nickname
|
||
}
|
||
result = await self._send_data_async(11069, data)
|
||
if result:
|
||
logger.info(f"修改群昵称成功: {chatroom_id}")
|
||
else:
|
||
logger.error(f"修改群昵称失败: {chatroom_id}")
|
||
return result
|
||
|
||
# ==================== 登录信息 ====================
|
||
|
||
async def get_login_info(self) -> Optional[Dict]:
|
||
"""
|
||
获取当前登录信息
|
||
|
||
Returns:
|
||
登录信息字典
|
||
"""
|
||
data = {}
|
||
await self._send_data_async(MessageType.MT_GET_LOGIN_INFO, data)
|
||
logger.info("请求登录信息")
|
||
return None
|
||
|
||
async def get_user_info_in_chatroom(self, chatroom_id: str, user_wxid: str, max_retries: int = 2) -> Optional[Dict]:
|
||
"""
|
||
获取群内用户详细信息(使用协议API)
|
||
|
||
Args:
|
||
chatroom_id: 群聊 ID
|
||
user_wxid: 用户 wxid
|
||
max_retries: 最大重试次数
|
||
|
||
Returns:
|
||
用户详细信息字典
|
||
"""
|
||
for attempt in range(max_retries + 1):
|
||
# 生成唯一请求ID
|
||
request_id = str(uuid.uuid4())
|
||
|
||
# 创建等待事件
|
||
event = asyncio.Event()
|
||
result_data = {"user_info": None}
|
||
|
||
# 存储待处理请求
|
||
request_key = f"user_info_{chatroom_id}_{user_wxid}"
|
||
self.pending_requests[request_key] = {
|
||
"request_id": request_id,
|
||
"event": event,
|
||
"result": result_data,
|
||
"type": "user_info_in_chatroom",
|
||
"chatroom_id": chatroom_id,
|
||
"user_wxid": user_wxid
|
||
}
|
||
|
||
# 发送请求
|
||
data = {
|
||
"room_wxid": chatroom_id,
|
||
"wxid": user_wxid
|
||
}
|
||
await self._send_data_async(11174, data)
|
||
|
||
if attempt == 0:
|
||
logger.info(f"请求群内用户信息: chatroom={chatroom_id}, user={user_wxid}")
|
||
else:
|
||
logger.info(f"重试请求群内用户信息 (第{attempt}次): user={user_wxid}")
|
||
|
||
# 等待回调结果
|
||
try:
|
||
await asyncio.wait_for(event.wait(), timeout=15)
|
||
|
||
# 获取结果
|
||
user_info = result_data["user_info"]
|
||
if user_info:
|
||
logger.success(f"获取群内用户信息成功: {user_wxid}")
|
||
return user_info
|
||
else:
|
||
logger.warning(f"获取群内用户信息为空: {user_wxid}")
|
||
if attempt < max_retries:
|
||
await asyncio.sleep(1)
|
||
continue
|
||
return None
|
||
|
||
except asyncio.TimeoutError:
|
||
logger.warning(f"获取群内用户信息超时 (第{attempt + 1}次): {user_wxid}")
|
||
if attempt < max_retries:
|
||
await asyncio.sleep(1)
|
||
continue
|
||
logger.error(f"获取群内用户信息最终失败: {user_wxid}")
|
||
return None
|
||
finally:
|
||
# 清理请求
|
||
if request_key in self.pending_requests:
|
||
del self.pending_requests[request_key]
|
||
|
||
return None
|
||
|
||
@RECV_CALLBACK(in_class=True)
|
||
def on_api_response(self, client_id: int, msg_type: int, data: dict):
|
||
"""
|
||
处理API响应回调
|
||
|
||
Args:
|
||
client_id: 客户端ID
|
||
msg_type: 消息类型
|
||
data: 响应数据
|
||
"""
|
||
# 只处理本客户端的消息
|
||
if client_id != self.client_id:
|
||
return
|
||
|
||
# 处理群成员信息响应
|
||
if msg_type == 11032:
|
||
self._handle_chatroom_members_response(data)
|
||
# 处理群聊列表响应(备用方案)
|
||
elif msg_type == 11031:
|
||
self._handle_chatroom_list_response(data)
|
||
# 处理单个用户信息响应(协议API)
|
||
elif msg_type == 11174:
|
||
self._handle_user_info_in_chatroom_response(data)
|
||
# 处理CDN上传响应
|
||
elif msg_type == 11229:
|
||
self._handle_cdn_upload_response(data)
|
||
elif msg_type == 11236:
|
||
try:
|
||
to_user = data.get("toUserName", "")
|
||
request_key = f"send_link_card_{to_user}"
|
||
if request_key in self.pending_requests:
|
||
self.pending_requests[request_key]["result"] = data
|
||
self.pending_requests[request_key]["event"].set()
|
||
except Exception as e:
|
||
logger.error(f"处理链接卡片响应失败: {e}")
|
||
|
||
def _handle_chatroom_members_response(self, data: dict):
|
||
"""
|
||
处理群成员信息响应
|
||
|
||
Args:
|
||
data: 响应数据
|
||
"""
|
||
try:
|
||
# 检查是否有错误
|
||
errcode = data.get("errcode")
|
||
errmsg = data.get("errmsg", "")
|
||
|
||
if errcode is not None and errcode != 0:
|
||
logger.error(f"群成员信息API返回错误: errcode={errcode}, errmsg={errmsg}")
|
||
|
||
# 对于所有待处理的群成员请求,都触发事件(返回空结果)
|
||
for chatroom_id, request_info in list(self.pending_requests.items()):
|
||
if request_info.get("type") == "chatroom_members":
|
||
request_info["result"]["members"] = []
|
||
request_info["result"]["success"] = False
|
||
request_info["event"].set()
|
||
logger.warning(f"群成员请求因API错误而失败: {chatroom_id}")
|
||
return
|
||
|
||
group_wxid = data.get("group_wxid", "")
|
||
member_list = data.get("member_list", [])
|
||
|
||
logger.info(f"收到群成员信息响应: group_wxid={group_wxid}, 成员数={len(member_list)}")
|
||
|
||
# 查找对应的待处理请求
|
||
if group_wxid in self.pending_requests:
|
||
request_info = self.pending_requests[group_wxid]
|
||
|
||
# 存储结果数据
|
||
request_info["result"]["members"] = member_list
|
||
request_info["result"]["success"] = True
|
||
|
||
# 触发等待事件
|
||
request_info["event"].set()
|
||
|
||
logger.success(f"群成员信息处理完成: {group_wxid}")
|
||
else:
|
||
logger.warning(f"未找到对应的群成员请求: {group_wxid}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理群成员信息响应失败: {e}")
|
||
|
||
# 触发所有待处理的群成员请求(返回空结果)
|
||
for chatroom_id, request_info in list(self.pending_requests.items()):
|
||
if request_info.get("type") == "chatroom_members":
|
||
request_info["result"]["members"] = []
|
||
request_info["result"]["success"] = False
|
||
request_info["event"].set()
|
||
|
||
def _handle_chatroom_list_response(self, data: dict):
|
||
"""
|
||
处理群聊列表响应(备用方案)
|
||
|
||
Args:
|
||
data: 响应数据
|
||
"""
|
||
try:
|
||
# data应该是一个包含群聊信息的列表
|
||
chatrooms = data if isinstance(data, list) else []
|
||
|
||
logger.info(f"收到群聊列表响应: 群聊数={len(chatrooms)}")
|
||
|
||
# 查找所有等待群聊列表的请求
|
||
for key, request_info in list(self.pending_requests.items()):
|
||
if key.startswith("chatroom_list_") and request_info.get("type") == "chatroom_list":
|
||
# 存储结果数据
|
||
request_info["result"]["chatrooms"] = chatrooms
|
||
|
||
# 触发等待事件
|
||
request_info["event"].set()
|
||
|
||
logger.success(f"群聊列表处理完成: {key}")
|
||
break
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理群聊列表响应失败: {e}")
|
||
|
||
def _handle_user_info_in_chatroom_response(self, data: dict):
|
||
"""
|
||
处理群内用户信息响应(11174 API)
|
||
支持两种请求类型:
|
||
1. chatroom_info - 获取群信息(包含成员列表)
|
||
2. user_info_in_chatroom - 获取单个用户信息
|
||
|
||
Args:
|
||
data: 响应数据
|
||
"""
|
||
try:
|
||
# 检查基础响应
|
||
base_response = data.get("baseResponse", {})
|
||
ret_code = base_response.get("ret", -1)
|
||
|
||
if ret_code != 0:
|
||
logger.error(f"11174 API返回错误: ret={ret_code}")
|
||
# 触发所有相关的待处理请求
|
||
self._trigger_user_info_requests(None)
|
||
self._trigger_chatroom_info_requests([])
|
||
return
|
||
|
||
# 获取联系人列表
|
||
contact_list = data.get("contactList", [])
|
||
|
||
if not contact_list:
|
||
logger.warning("11174 响应中无联系人数据")
|
||
self._trigger_user_info_requests(None)
|
||
self._trigger_chatroom_info_requests([])
|
||
return
|
||
|
||
# 第一个联系人信息
|
||
contact_info = contact_list[0]
|
||
contact_wxid = contact_info.get("userName", {}).get("string", "")
|
||
|
||
# 判断是群聊还是个人
|
||
is_chatroom = contact_wxid.endswith("@chatroom")
|
||
|
||
if is_chatroom:
|
||
# 处理群信息请求
|
||
logger.info(f"收到群信息响应: chatroom_id={contact_wxid}")
|
||
|
||
# 提取群成员列表
|
||
new_chatroom_data = contact_info.get("newChatroomData", {})
|
||
member_list = new_chatroom_data.get("chatRoomMemberList", [])
|
||
|
||
# 转换成员数据格式
|
||
members = []
|
||
for member in member_list:
|
||
members.append({
|
||
"wxid": member.get("userName", ""),
|
||
"nickname": member.get("nickName", ""),
|
||
"display_name": member.get("displayName", ""),
|
||
"avatar": member.get("bigHeadImgUrl", ""),
|
||
"invite_by": member.get("inviteBy", "")
|
||
})
|
||
|
||
logger.info(f"解析到 {len(members)} 个群成员")
|
||
|
||
# 查找对应的待处理请求
|
||
request_key = f"chatroom_info_{contact_wxid}"
|
||
if request_key in self.pending_requests:
|
||
request_info = self.pending_requests[request_key]
|
||
request_info["result"]["members"] = members
|
||
request_info["result"]["success"] = True
|
||
request_info["event"].set()
|
||
logger.success(f"群信息处理完成: {contact_wxid}")
|
||
else:
|
||
logger.warning(f"未找到对应的群信息请求: {contact_wxid}")
|
||
else:
|
||
# 处理单个用户信息请求
|
||
logger.info(f"收到群内用户信息响应: user_wxid={contact_wxid}")
|
||
|
||
# 查找对应的待处理请求
|
||
for request_key, request_info in list(self.pending_requests.items()):
|
||
if (request_info.get("type") == "user_info_in_chatroom" and
|
||
request_info.get("user_wxid") == contact_wxid):
|
||
|
||
# 存储结果数据
|
||
request_info["result"]["user_info"] = contact_info
|
||
|
||
# 触发等待事件
|
||
request_info["event"].set()
|
||
|
||
logger.success(f"群内用户信息处理完成: {contact_wxid}")
|
||
return
|
||
|
||
logger.warning(f"未找到对应的群内用户信息请求: {contact_wxid}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理 11174 响应失败: {e}")
|
||
import traceback
|
||
logger.error(f"详细错误: {traceback.format_exc()}")
|
||
self._trigger_user_info_requests(None)
|
||
self._trigger_chatroom_info_requests([])
|
||
|
||
def _trigger_user_info_requests(self, user_info):
|
||
"""
|
||
触发所有待处理的用户信息请求
|
||
|
||
Args:
|
||
user_info: 用户信息,None表示失败
|
||
"""
|
||
for request_key, request_info in list(self.pending_requests.items()):
|
||
if request_info.get("type") == "user_info_in_chatroom":
|
||
request_info["result"]["user_info"] = user_info
|
||
request_info["event"].set()
|
||
|
||
def _trigger_chatroom_info_requests(self, members):
|
||
"""
|
||
触发所有待处理的群信息请求
|
||
|
||
Args:
|
||
members: 群成员列表,空列表表示失败
|
||
"""
|
||
for request_key, request_info in list(self.pending_requests.items()):
|
||
if request_info.get("type") == "chatroom_info":
|
||
request_info["result"]["members"] = members
|
||
request_info["result"]["success"] = len(members) > 0
|
||
request_info["event"].set()
|
||
|
||
def _handle_cdn_upload_response(self, data: dict):
|
||
"""
|
||
处理CDN上传响应
|
||
|
||
Args:
|
||
data: 响应数据,包含 aes_key, file_md5, error_code 等
|
||
"""
|
||
try:
|
||
file_path = data.get("file_path", "")
|
||
error_code = data.get("error_code", -1)
|
||
|
||
logger.info(f"收到CDN上传响应: file_path={file_path}, error_code={error_code}")
|
||
|
||
# 查找对应的待处理请求
|
||
for request_key, request_info in list(self.pending_requests.items()):
|
||
if request_info.get("type") == "cdn_upload":
|
||
# 存储结果数据
|
||
request_info["result"]["cdn_info"] = data
|
||
|
||
# 触发等待事件
|
||
request_info["event"].set()
|
||
|
||
logger.success(f"CDN上传响应处理完成: {file_path}")
|
||
break
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理CDN上传响应失败: {e}")
|
||
|
||
async def send_cdn_image(self, to_wxid: str, file_path: str) -> bool:
|
||
"""
|
||
通过CDN上传并发送图片
|
||
|
||
Args:
|
||
to_wxid: 接收者 wxid
|
||
file_path: 图片文件路径
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
try:
|
||
# 1. 上传到CDN
|
||
cdn_info = await self.cdn_upload(file_path, file_type=1)
|
||
if not cdn_info:
|
||
logger.error(f"CDN上传失败,无法发送图片: {file_path}")
|
||
return False
|
||
|
||
# 2. 使用CDN信息发送图片
|
||
data = {
|
||
"to_wxid": to_wxid,
|
||
"aes_key": cdn_info.get("aes_key", ""),
|
||
"file_md5": cdn_info.get("file_md5", ""),
|
||
"file_size": cdn_info.get("file_size", 0),
|
||
"mid_file_md5": cdn_info.get("mid_file_md5", ""),
|
||
"mid_file_size": cdn_info.get("mid_file_size", 0),
|
||
"thumb_file_md5": cdn_info.get("thumb_file_md5", ""),
|
||
"thumb_file_size": cdn_info.get("thumb_file_size", 0)
|
||
}
|
||
|
||
result = await self._send_data_async(11233, data)
|
||
if result:
|
||
logger.success(f"CDN图片发送成功: {to_wxid}")
|
||
import os
|
||
filename = os.path.basename(file_path)
|
||
await self._log_bot_message(to_wxid, f"[图片] {filename}", "image", file_path)
|
||
else:
|
||
logger.error(f"CDN图片发送失败: {to_wxid}")
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"发送CDN图片异常: {e}")
|
||
return False
|