586 lines
20 KiB
Python
586 lines
20 KiB
Python
"""NapCat QQ 平台查询服务。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, List, Mapping, Optional
|
|
|
|
from ..types import NapCatActionParams, NapCatActionResponse, NapCatPayloadDict, NapCatPayloadList
|
|
from .action_service import NapCatActionService
|
|
|
|
|
|
class NapCatQueryService:
|
|
"""NapCat QQ 平台查询与管理动作服务。"""
|
|
|
|
def __init__(self, action_service: NapCatActionService, logger: Any) -> None:
|
|
"""初始化查询服务。
|
|
|
|
Args:
|
|
action_service: NapCat 底层动作服务。
|
|
logger: 插件日志对象。
|
|
"""
|
|
self._action_service = action_service
|
|
self._logger = logger
|
|
|
|
async def call_action(self, action_name: str, params: NapCatActionParams) -> NapCatActionResponse:
|
|
"""调用 OneBot 动作并要求返回成功结果。
|
|
|
|
Args:
|
|
action_name: OneBot 动作名称。
|
|
params: 动作参数。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 返回的原始响应字典。
|
|
"""
|
|
return await self._action_service.call_action(action_name, params)
|
|
|
|
async def call_action_data(self, action_name: str, params: NapCatActionParams) -> Any:
|
|
"""调用 OneBot 动作并返回 ``data`` 字段。
|
|
|
|
Args:
|
|
action_name: OneBot 动作名称。
|
|
params: 动作参数。
|
|
|
|
Returns:
|
|
Any: NapCat 响应中的 ``data`` 字段。
|
|
"""
|
|
return await self._action_service.call_action_data(action_name, params)
|
|
|
|
async def get_login_info(self) -> Optional[NapCatPayloadDict]:
|
|
"""获取当前登录账号信息。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 登录信息字典;返回值不是字典时为 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data("get_login_info", {})
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def get_stranger_info(self, user_id: str, no_cache: bool = False) -> Optional[NapCatPayloadDict]:
|
|
"""获取陌生人信息。
|
|
|
|
Args:
|
|
user_id: 用户号。
|
|
no_cache: 是否禁用缓存。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 陌生人信息字典;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data(
|
|
"get_stranger_info",
|
|
{"user_id": user_id, "no_cache": bool(no_cache)},
|
|
)
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def get_friend_list(self, no_cache: bool = False) -> Optional[NapCatPayloadList]:
|
|
"""获取好友列表。
|
|
|
|
Args:
|
|
no_cache: 是否禁用缓存。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadList]: 好友信息列表;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data("get_friend_list", {"no_cache": bool(no_cache)})
|
|
return self._normalize_payload_list(response_data, action_name="get_friend_list")
|
|
|
|
async def get_group_info(self, group_id: str) -> Optional[NapCatPayloadDict]:
|
|
"""获取群信息。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 群信息字典;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data("get_group_info", {"group_id": group_id})
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def get_group_detail_info(self, group_id: str) -> Optional[NapCatPayloadDict]:
|
|
"""获取群详细信息。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 群详细信息字典;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data("get_group_detail_info", {"group_id": group_id})
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def get_group_list(self, no_cache: bool = False) -> Optional[NapCatPayloadList]:
|
|
"""获取群列表。
|
|
|
|
Args:
|
|
no_cache: 是否禁用缓存。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadList]: 群信息列表;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data("get_group_list", {"no_cache": bool(no_cache)})
|
|
return self._normalize_payload_list(response_data, action_name="get_group_list")
|
|
|
|
async def get_group_at_all_remain(self, group_id: str) -> Optional[NapCatPayloadDict]:
|
|
"""获取群 @ 全体成员剩余次数。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 剩余次数信息;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data("get_group_at_all_remain", {"group_id": group_id})
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def get_group_member_info(
|
|
self,
|
|
group_id: str,
|
|
user_id: str,
|
|
no_cache: bool = True,
|
|
) -> Optional[NapCatPayloadDict]:
|
|
"""获取群成员信息。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
user_id: 用户号。
|
|
no_cache: 是否禁用缓存。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 群成员信息字典;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data(
|
|
"get_group_member_info",
|
|
{"group_id": group_id, "user_id": user_id, "no_cache": bool(no_cache)},
|
|
)
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def get_group_member_list(self, group_id: str, no_cache: bool = False) -> Optional[NapCatPayloadList]:
|
|
"""获取群成员列表。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
no_cache: 是否禁用缓存。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadList]: 群成员信息列表;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data(
|
|
"get_group_member_list",
|
|
{"group_id": group_id, "no_cache": bool(no_cache)},
|
|
)
|
|
return self._normalize_payload_list(response_data, action_name="get_group_member_list")
|
|
|
|
async def get_message_detail(self, message_id: str) -> Optional[NapCatPayloadDict]:
|
|
"""获取消息详情。
|
|
|
|
Args:
|
|
message_id: 消息 ID。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 消息详情字典;失败时返回 ``None``。
|
|
"""
|
|
response_data = await self._safe_call_action_data("get_msg", {"message_id": message_id})
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def get_friend_message_history(
|
|
self,
|
|
user_id: str,
|
|
*,
|
|
message_seq: int | None = None,
|
|
count: int = 20,
|
|
reverse_order: bool = False,
|
|
) -> Optional[NapCatPayloadList]:
|
|
"""获取私聊历史消息列表。"""
|
|
|
|
params: NapCatActionResponse = {
|
|
"user_id": user_id,
|
|
"count": max(1, int(count)),
|
|
"reverse_order": bool(reverse_order),
|
|
}
|
|
if message_seq is not None:
|
|
params["message_seq"] = int(message_seq)
|
|
response_data = await self._safe_call_action_data("get_friend_msg_history", params)
|
|
return self._normalize_payload_list(response_data, action_name="get_friend_msg_history")
|
|
|
|
async def get_group_message_history(
|
|
self,
|
|
group_id: str,
|
|
*,
|
|
message_seq: int | None = None,
|
|
count: int = 20,
|
|
reverse_order: bool = False,
|
|
) -> Optional[NapCatPayloadList]:
|
|
"""获取群聊历史消息列表。"""
|
|
|
|
params: NapCatActionResponse = {
|
|
"group_id": group_id,
|
|
"count": max(1, int(count)),
|
|
"reverse_order": bool(reverse_order),
|
|
}
|
|
if message_seq is not None:
|
|
params["message_seq"] = int(message_seq)
|
|
response_data = await self._safe_call_action_data("get_group_msg_history", params)
|
|
return self._normalize_payload_list(response_data, action_name="get_group_msg_history")
|
|
|
|
async def get_forward_message(
|
|
self,
|
|
message_id: Optional[str] = None,
|
|
forward_id: Optional[str] = None,
|
|
) -> Optional[NapCatPayloadDict]:
|
|
"""获取合并转发消息详情。
|
|
|
|
Args:
|
|
message_id: 转发消息 ID。
|
|
forward_id: NapCat 官方文档中的兼容字段 ``id``。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 合并转发消息详情;失败时返回 ``None``。
|
|
"""
|
|
params: NapCatActionResponse = {}
|
|
if message_id:
|
|
params["message_id"] = message_id
|
|
if forward_id:
|
|
params["id"] = forward_id
|
|
if not params:
|
|
raise ValueError("message_id 或 id 至少提供一个")
|
|
|
|
response_data = await self._safe_call_action_data("get_forward_msg", params)
|
|
return self._normalize_forward_payload(response_data)
|
|
|
|
async def get_record_detail(
|
|
self,
|
|
file_name: Optional[str] = None,
|
|
file_id: Optional[str] = None,
|
|
out_format: str = "wav",
|
|
) -> Optional[NapCatPayloadDict]:
|
|
"""获取语音文件详情。
|
|
|
|
Args:
|
|
file_name: 语音文件名。
|
|
file_id: 可选文件 ID。
|
|
out_format: 输出格式。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 语音详情字典;失败时返回 ``None``。
|
|
"""
|
|
params: NapCatActionResponse = {}
|
|
if file_name:
|
|
params["file"] = file_name
|
|
if file_id:
|
|
params["file_id"] = file_id
|
|
if out_format:
|
|
params["out_format"] = out_format
|
|
if not params.get("file") and not params.get("file_id"):
|
|
raise ValueError("file 或 file_id 至少提供一个")
|
|
|
|
response_data = await self._safe_call_action_data("get_record", params)
|
|
return response_data if isinstance(response_data, dict) else None
|
|
|
|
async def set_group_ban(self, group_id: int, user_id: int, duration: int) -> NapCatActionResponse:
|
|
"""设置群成员禁言。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
user_id: 用户号。
|
|
duration: 禁言秒数。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action(
|
|
"set_group_ban",
|
|
{"group_id": group_id, "user_id": user_id, "duration": duration},
|
|
)
|
|
|
|
async def set_group_whole_ban(self, group_id: int, enable: bool) -> NapCatActionResponse:
|
|
"""设置群全体禁言。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
enable: 是否开启全体禁言。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action(
|
|
"set_group_whole_ban",
|
|
{"group_id": group_id, "enable": bool(enable)},
|
|
)
|
|
|
|
async def set_group_kick(
|
|
self,
|
|
group_id: int,
|
|
user_id: int,
|
|
reject_add_request: bool = False,
|
|
) -> NapCatActionResponse:
|
|
"""踢出群成员。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
user_id: 用户号。
|
|
reject_add_request: 是否拒绝再次加群。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action(
|
|
"set_group_kick",
|
|
{
|
|
"group_id": group_id,
|
|
"user_id": user_id,
|
|
"reject_add_request": bool(reject_add_request),
|
|
},
|
|
)
|
|
|
|
async def set_group_kick_members(
|
|
self,
|
|
group_id: int,
|
|
user_ids: List[int],
|
|
reject_add_request: bool = False,
|
|
) -> NapCatActionResponse:
|
|
"""批量踢出群成员。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
user_ids: 用户号列表。
|
|
reject_add_request: 是否拒绝再次加群。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action(
|
|
"set_group_kick_members",
|
|
{
|
|
"group_id": group_id,
|
|
"user_id": user_ids,
|
|
"reject_add_request": bool(reject_add_request),
|
|
},
|
|
)
|
|
|
|
async def send_poke(
|
|
self,
|
|
user_id: int,
|
|
group_id: Optional[int] = None,
|
|
target_id: Optional[int] = None,
|
|
) -> NapCatActionResponse:
|
|
"""发送戳一戳。
|
|
|
|
Args:
|
|
user_id: 目标用户号。
|
|
group_id: 可选群号;私聊时为空。
|
|
target_id: NapCat 官方 ``send_poke`` 动作支持的目标 ID。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
params: NapCatActionResponse = {"user_id": user_id}
|
|
if group_id is not None:
|
|
params["group_id"] = group_id
|
|
if target_id is not None:
|
|
params["target_id"] = target_id
|
|
return await self.call_action("send_poke", params)
|
|
|
|
async def delete_message(self, message_id: int) -> NapCatActionResponse:
|
|
"""撤回消息。
|
|
|
|
Args:
|
|
message_id: 消息 ID。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action("delete_msg", {"message_id": message_id})
|
|
|
|
async def send_group_ai_record(self, group_id: int, character: str, text: str) -> NapCatActionResponse:
|
|
"""发送群 AI 语音。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
character: 角色标识。
|
|
text: 语音文本。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action(
|
|
"send_group_ai_record",
|
|
{"group_id": group_id, "character": character, "text": text},
|
|
)
|
|
|
|
async def set_message_emoji_like(
|
|
self,
|
|
message_id: int,
|
|
emoji_id: int,
|
|
set_like: bool = True,
|
|
) -> NapCatActionResponse:
|
|
"""给消息贴表情或取消表情。
|
|
|
|
Args:
|
|
message_id: 消息 ID。
|
|
emoji_id: 表情 ID。
|
|
set_like: 是否设置为已贴表情。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action(
|
|
"set_msg_emoji_like",
|
|
{"message_id": message_id, "emoji_id": emoji_id, "set": bool(set_like)},
|
|
)
|
|
|
|
async def set_group_name(self, group_id: int, group_name: str) -> NapCatActionResponse:
|
|
"""设置群名称。
|
|
|
|
Args:
|
|
group_id: 群号。
|
|
group_name: 新群名称。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
return await self.call_action(
|
|
"set_group_name",
|
|
{"group_id": group_id, "group_name": group_name},
|
|
)
|
|
|
|
async def set_qq_profile(
|
|
self,
|
|
nickname: str,
|
|
personal_note: str = "",
|
|
sex: str = "",
|
|
) -> NapCatActionResponse:
|
|
"""设置 QQ 账号资料。
|
|
|
|
Args:
|
|
nickname: 新昵称。
|
|
personal_note: 个性签名。
|
|
sex: 性别,支持 ``male``、``female``、``unknown``。
|
|
|
|
Returns:
|
|
NapCatActionResponse: NapCat 原始响应字典。
|
|
"""
|
|
params: NapCatActionResponse = {"nickname": nickname}
|
|
if personal_note:
|
|
params["personal_note"] = personal_note
|
|
if sex:
|
|
params["sex"] = sex
|
|
return await self.call_action("set_qq_profile", params)
|
|
|
|
async def download_binary(self, url: str) -> Optional[bytes]:
|
|
"""下载远程二进制资源。
|
|
|
|
Args:
|
|
url: 资源 URL。
|
|
|
|
Returns:
|
|
Optional[bytes]: 下载到的二进制内容;失败时返回 ``None``。
|
|
"""
|
|
return await self._action_service.download_binary(url)
|
|
|
|
async def _safe_call_action_data(self, action_name: str, params: NapCatActionParams) -> Any:
|
|
"""安全调用 OneBot 动作并返回 ``data`` 字段。
|
|
|
|
Args:
|
|
action_name: OneBot 动作名称。
|
|
params: 动作参数。
|
|
|
|
Returns:
|
|
Any: 响应中的 ``data`` 字段;失败时返回 ``None``。
|
|
"""
|
|
return await self._action_service.safe_call_action_data(action_name, params)
|
|
|
|
def _normalize_payload_list(self, response_data: Any, action_name: str) -> Optional[NapCatPayloadList]:
|
|
"""将列表类响应归一化为字典列表。
|
|
|
|
NapCat 在不同版本或不同动作下,``data`` 可能直接返回列表,
|
|
也可能再包一层字典,例如 ``{\"members\": [...]}``。
|
|
|
|
Args:
|
|
response_data: 原始 ``data`` 字段。
|
|
action_name: 当前动作名称。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadList]: 归一化后的列表;无法识别时返回 ``None``。
|
|
"""
|
|
|
|
if isinstance(response_data, list):
|
|
return [dict(item) for item in response_data if isinstance(item, Mapping)]
|
|
|
|
if not isinstance(response_data, Mapping):
|
|
self._logger.warning(
|
|
"NapCat 列表接口返回了无法识别的数据类型: action=%s type=%s payload=%r",
|
|
action_name,
|
|
type(response_data).__name__,
|
|
response_data,
|
|
)
|
|
return None
|
|
|
|
for key in (
|
|
"list",
|
|
"items",
|
|
"members",
|
|
"member_list",
|
|
"group_list",
|
|
"friend_list",
|
|
"friends",
|
|
"records",
|
|
"rows",
|
|
"data",
|
|
):
|
|
candidate = response_data.get(key)
|
|
if isinstance(candidate, list):
|
|
return [dict(item) for item in candidate if isinstance(item, Mapping)]
|
|
|
|
for candidate in response_data.values():
|
|
if isinstance(candidate, list):
|
|
return [dict(item) for item in candidate if isinstance(item, Mapping)]
|
|
|
|
self._logger.warning(
|
|
"NapCat 列表接口返回了无法归一化的字典结构: action=%s payload=%r",
|
|
action_name,
|
|
response_data,
|
|
)
|
|
return None
|
|
|
|
def _normalize_forward_payload(self, response_data: Any) -> Optional[NapCatPayloadDict]:
|
|
"""将合并转发响应归一化为统一字典结构。
|
|
|
|
NapCat 的 ``get_forward_msg`` 在不同版本下,``data`` 可能直接返回节点列表,
|
|
也可能返回 ``{\"messages\": [...]}``,甚至包在 ``content`` 字段中。
|
|
|
|
Args:
|
|
response_data: ``get_forward_msg`` 的原始 ``data`` 字段。
|
|
|
|
Returns:
|
|
Optional[NapCatPayloadDict]: 归一化后的转发消息详情;失败时返回 ``None``。
|
|
"""
|
|
if isinstance(response_data, list):
|
|
return {"messages": [dict(item) for item in response_data if isinstance(item, Mapping)]}
|
|
|
|
if not isinstance(response_data, Mapping):
|
|
self._logger.warning(
|
|
"NapCat 转发接口返回了无法识别的数据类型: type=%s payload=%r",
|
|
type(response_data).__name__,
|
|
response_data,
|
|
)
|
|
return None
|
|
|
|
direct_messages = response_data.get("messages")
|
|
if isinstance(direct_messages, list):
|
|
return dict(response_data)
|
|
|
|
direct_content = response_data.get("content")
|
|
if isinstance(direct_content, list):
|
|
return {"messages": [dict(item) for item in direct_content if isinstance(item, Mapping)]}
|
|
|
|
nested_data = response_data.get("data")
|
|
if isinstance(nested_data, Mapping):
|
|
nested_messages = nested_data.get("messages")
|
|
if isinstance(nested_messages, list):
|
|
return {"messages": [dict(item) for item in nested_messages if isinstance(item, Mapping)]}
|
|
|
|
nested_content = nested_data.get("content")
|
|
if isinstance(nested_content, list):
|
|
return {"messages": [dict(item) for item in nested_content if isinstance(item, Mapping)]}
|
|
|
|
self._logger.warning("NapCat 转发接口未返回可识别的转发节点列表: payload=%r", response_data)
|
|
return None
|