Merge pull request #33 from xuqian13/feat/voice

可以发送音乐卡片了
pull/35/head
UnCLAS-Prommer 2025-06-22 10:43:36 +08:00 committed by GitHub
commit eb823f056b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 362 additions and 342 deletions

View File

@ -1,342 +1,362 @@
import json import json
import websockets as Server import websockets as Server
import uuid import uuid
from maim_message import ( from maim_message import (
UserInfo, UserInfo,
GroupInfo, GroupInfo,
Seg, Seg,
BaseMessageInfo, BaseMessageInfo,
MessageBase, MessageBase,
) )
from typing import Dict, Any, Tuple from typing import Dict, Any, Tuple
from . import CommandType from . import CommandType
from .config import global_config from .config import global_config
from .response_pool import get_response from .response_pool import get_response
from .logger import logger from .logger import logger
from .utils import get_image_format, convert_image_to_gif from .utils import get_image_format, convert_image_to_gif
class SendHandler: class SendHandler:
def __init__(self): def __init__(self):
self.server_connection: Server.ServerConnection = None self.server_connection: Server.ServerConnection = None
async def handle_message(self, raw_message_base_dict: dict) -> None: async def handle_message(self, raw_message_base_dict: dict) -> None:
raw_message_base: MessageBase = MessageBase.from_dict(raw_message_base_dict) raw_message_base: MessageBase = MessageBase.from_dict(raw_message_base_dict)
message_segment: Seg = raw_message_base.message_segment message_segment: Seg = raw_message_base.message_segment
logger.info("接收到来自MaiBot的消息处理中") logger.info("接收到来自MaiBot的消息处理中")
if message_segment.type == "command": if message_segment.type == "command":
return await self.send_command(raw_message_base) return await self.send_command(raw_message_base)
else: else:
return await self.send_normal_message(raw_message_base) return await self.send_normal_message(raw_message_base)
async def send_normal_message(self, raw_message_base: MessageBase) -> None: async def send_normal_message(self, raw_message_base: MessageBase) -> None:
""" """
处理普通消息发送 处理普通消息发送
""" """
logger.info("处理普通信息中") logger.info("处理普通信息中")
message_info: BaseMessageInfo = raw_message_base.message_info message_info: BaseMessageInfo = raw_message_base.message_info
message_segment: Seg = raw_message_base.message_segment message_segment: Seg = raw_message_base.message_segment
group_info: GroupInfo = message_info.group_info group_info: GroupInfo = message_info.group_info
user_info: UserInfo = message_info.user_info user_info: UserInfo = message_info.user_info
target_id: int = None target_id: int = None
action: str = None action: str = None
id_name: str = None id_name: str = None
processed_message: list = [] processed_message: list = []
try: try:
processed_message = await self.handle_seg_recursive(message_segment) processed_message = await self.handle_seg_recursive(message_segment)
except Exception as e: except Exception as e:
logger.error(f"处理消息时发生错误: {e}") logger.error(f"处理消息时发生错误: {e}")
return return
if not processed_message: if not processed_message:
logger.critical("现在暂时不支持解析此回复!") logger.critical("现在暂时不支持解析此回复!")
return None return None
if group_info and user_info: if group_info and user_info:
logger.debug("发送群聊消息") logger.debug("发送群聊消息")
target_id = group_info.group_id target_id = group_info.group_id
action = "send_group_msg" action = "send_group_msg"
id_name = "group_id" id_name = "group_id"
elif user_info: elif user_info:
logger.debug("发送私聊消息") logger.debug("发送私聊消息")
target_id = user_info.user_id target_id = user_info.user_id
action = "send_private_msg" action = "send_private_msg"
id_name = "user_id" id_name = "user_id"
else: else:
logger.error("无法识别的消息类型") logger.error("无法识别的消息类型")
return return
logger.info("尝试发送到napcat") logger.info("尝试发送到napcat")
response = await self.send_message_to_napcat( response = await self.send_message_to_napcat(
action, action,
{ {
id_name: target_id, id_name: target_id,
"message": processed_message, "message": processed_message,
}, },
) )
if response.get("status") == "ok": if response.get("status") == "ok":
logger.info("消息发送成功") logger.info("消息发送成功")
else: else:
logger.warning(f"消息发送失败napcat返回{str(response)}") logger.warning(f"消息发送失败napcat返回{str(response)}")
async def send_command(self, raw_message_base: MessageBase) -> None: async def send_command(self, raw_message_base: MessageBase) -> None:
""" """
处理命令类 处理命令类
""" """
logger.info("处理命令中") logger.info("处理命令中")
message_info: BaseMessageInfo = raw_message_base.message_info message_info: BaseMessageInfo = raw_message_base.message_info
message_segment: Seg = raw_message_base.message_segment message_segment: Seg = raw_message_base.message_segment
group_info: GroupInfo = message_info.group_info group_info: GroupInfo = message_info.group_info
seg_data: Dict[str, Any] = message_segment.data seg_data: Dict[str, Any] = message_segment.data
command_name: str = seg_data.get("name") command_name: str = seg_data.get("name")
try: try:
match command_name: match command_name:
case CommandType.GROUP_BAN.name: case CommandType.GROUP_BAN.name:
command, args_dict = self.handle_ban_command(seg_data.get("args"), group_info) command, args_dict = self.handle_ban_command(seg_data.get("args"), group_info)
case CommandType.GROUP_WHOLE_BAN.name: case CommandType.GROUP_WHOLE_BAN.name:
command, args_dict = self.handle_whole_ban_command(seg_data.get("args"), group_info) command, args_dict = self.handle_whole_ban_command(seg_data.get("args"), group_info)
case CommandType.GROUP_KICK.name: case CommandType.GROUP_KICK.name:
command, args_dict = self.handle_kick_command(seg_data.get("args"), group_info) command, args_dict = self.handle_kick_command(seg_data.get("args"), group_info)
case CommandType.SEND_POKE.name: case CommandType.SEND_POKE.name:
command, args_dict = self.handle_poke_command(seg_data.get("args"), group_info) command, args_dict = self.handle_poke_command(seg_data.get("args"), group_info)
case _: case _:
logger.error(f"未知命令: {command_name}") logger.error(f"未知命令: {command_name}")
return return
except Exception as e: except Exception as e:
logger.error(f"处理命令时发生错误: {e}") logger.error(f"处理命令时发生错误: {e}")
return None return None
if not command or not args_dict: if not command or not args_dict:
logger.error("命令或参数缺失") logger.error("命令或参数缺失")
return None return None
response = await self.send_message_to_napcat(command, args_dict) response = await self.send_message_to_napcat(command, args_dict)
if response.get("status") == "ok": if response.get("status") == "ok":
logger.info(f"命令 {command_name} 执行成功") logger.info(f"命令 {command_name} 执行成功")
else: else:
logger.warning(f"命令 {command_name} 执行失败napcat返回{str(response)}") logger.warning(f"命令 {command_name} 执行失败napcat返回{str(response)}")
def get_level(self, seg_data: Seg) -> int: def get_level(self, seg_data: Seg) -> int:
if seg_data.type == "seglist": if seg_data.type == "seglist":
return 1 + max(self.get_level(seg) for seg in seg_data.data) return 1 + max(self.get_level(seg) for seg in seg_data.data)
else: else:
return 1 return 1
async def handle_seg_recursive(self, seg_data: Seg) -> list: async def handle_seg_recursive(self, seg_data: Seg) -> list:
payload: list = [] payload: list = []
if seg_data.type == "seglist": if seg_data.type == "seglist":
# level = self.get_level(seg_data) # 给以后可能的多层嵌套做准备,此处不使用 # level = self.get_level(seg_data) # 给以后可能的多层嵌套做准备,此处不使用
if not seg_data.data: if not seg_data.data:
return [] return []
for seg in seg_data.data: for seg in seg_data.data:
payload = self.process_message_by_type(seg, payload) payload = self.process_message_by_type(seg, payload)
else: else:
payload = self.process_message_by_type(seg_data, payload) payload = self.process_message_by_type(seg_data, payload)
return payload return payload
def process_message_by_type(self, seg: Seg, payload: list) -> list: def process_message_by_type(self, seg: Seg, payload: list) -> list:
# sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression # sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression
new_payload = payload new_payload = payload
if seg.type == "reply": if seg.type == "reply":
target_id = seg.data target_id = seg.data
if target_id == "notice": if target_id == "notice":
return payload return payload
new_payload = self.build_payload(payload, self.handle_reply_message(target_id), True) new_payload = self.build_payload(payload, self.handle_reply_message(target_id), True)
elif seg.type == "text": elif seg.type == "text":
text = seg.data text = seg.data
if not text: if not text:
return payload return payload
new_payload = self.build_payload(payload, self.handle_text_message(text), False) new_payload = self.build_payload(payload, self.handle_text_message(text), False)
elif seg.type == "face": elif seg.type == "face":
logger.warning("MaiBot 发送了qq原生表情暂时不支持") logger.warning("MaiBot 发送了qq原生表情暂时不支持")
elif seg.type == "image": elif seg.type == "image":
image = seg.data image = seg.data
new_payload = self.build_payload(payload, self.handle_image_message(image), False) new_payload = self.build_payload(payload, self.handle_image_message(image), False)
elif seg.type == "emoji": elif seg.type == "emoji":
emoji = seg.data emoji = seg.data
new_payload = self.build_payload(payload, self.handle_emoji_message(emoji), False) new_payload = self.build_payload(payload, self.handle_emoji_message(emoji), False)
elif seg.type == "voice": elif seg.type == "voice":
voice = seg.data voice = seg.data
new_payload = self.build_payload(payload, self.handle_voice_message(voice), False) new_payload = self.build_payload(payload, self.handle_voice_message(voice), False)
return new_payload elif seg.type == "voiceurl":
voice_url = seg.data
def build_payload(self, payload: list, addon: dict, is_reply: bool = False) -> list: new_payload = self.build_payload(payload, self.handle_voiceurl_message(voice_url), False)
# sourcery skip: for-append-to-extend, merge-list-append, simplify-generator elif seg.type == "music":
"""构建发送的消息体""" song_id = seg.data
if is_reply: new_payload = self.build_payload(payload, self.handle_music_message(song_id), False)
temp_list = [] return new_payload
temp_list.append(addon)
for i in payload: def build_payload(self, payload: list, addon: dict, is_reply: bool = False) -> list:
if i.get("type") == "reply": # sourcery skip: for-append-to-extend, merge-list-append, simplify-generator
logger.debug("检测到多个回复,使用最新的回复") """构建发送的消息体"""
continue if is_reply:
temp_list.append(i) temp_list = []
return temp_list temp_list.append(addon)
else: for i in payload:
payload.append(addon) if i.get("type") == "reply":
return payload logger.debug("检测到多个回复,使用最新的回复")
continue
def handle_reply_message(self, id: str) -> dict: temp_list.append(i)
"""处理回复消息""" return temp_list
return {"type": "reply", "data": {"id": id}} else:
payload.append(addon)
def handle_text_message(self, message: str) -> dict: return payload
"""处理文本消息"""
return {"type": "text", "data": {"text": message}} def handle_reply_message(self, id: str) -> dict:
"""处理回复消息"""
def handle_image_message(self, encoded_image: str) -> dict: return {"type": "reply", "data": {"id": id}}
"""处理图片消息"""
return { def handle_text_message(self, message: str) -> dict:
"type": "image", """处理文本消息"""
"data": { return {"type": "text", "data": {"text": message}}
"file": f"base64://{encoded_image}",
"subtype": 0, def handle_image_message(self, encoded_image: str) -> dict:
}, """处理图片消息"""
} # base64 编码的图片 return {
"type": "image",
def handle_emoji_message(self, encoded_emoji: str) -> dict: "data": {
"""处理表情消息""" "file": f"base64://{encoded_image}",
encoded_image = encoded_emoji "subtype": 0,
image_format = get_image_format(encoded_emoji) },
if image_format != "gif": } # base64 编码的图片
encoded_image = convert_image_to_gif(encoded_emoji)
return { def handle_emoji_message(self, encoded_emoji: str) -> dict:
"type": "image", """处理表情消息"""
"data": { encoded_image = encoded_emoji
"file": f"base64://{encoded_image}", image_format = get_image_format(encoded_emoji)
"subtype": 1, if image_format != "gif":
"summary": "[动画表情]", encoded_image = convert_image_to_gif(encoded_emoji)
}, return {
} "type": "image",
"data": {
def handle_voice_message(self, encoded_voice: str) -> dict: "file": f"base64://{encoded_image}",
"""处理语音消息""" "subtype": 1,
if not global_config.voice.use_tts: "summary": "[动画表情]",
logger.warning("未启用语音消息处理") },
return {} }
if not encoded_voice:
return {} def handle_voice_message(self, encoded_voice: str) -> dict:
return { """处理语音消息"""
"type": "record", if not global_config.voice.use_tts:
"data": {"file": f"base64://{encoded_voice}"}, logger.warning("未启用语音消息处理")
} return {}
if not encoded_voice:
def handle_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: return {}
"""处理封禁命令 return {
"type": "record",
Args: "data": {"file": f"base64://{encoded_voice}"},
args (Dict[str, Any]): 参数字典 }
group_info (GroupInfo): 群聊信息对应目标群聊
def handle_voiceurl_message(self, voice_url: str) -> dict:
Returns: """处理语音链接消息"""
Tuple[CommandType, Dict[str, Any]] return {
""" "type": "record",
duration: int = int(args["duration"]) "data": {"file": voice_url},
user_id: int = int(args["qq_id"]) }
group_id: int = int(group_info.group_id)
if duration <= 0: def handle_music_message(self, song_id: str) -> dict:
raise ValueError("封禁时间必须大于0") """处理音乐消息"""
if not user_id or not group_id: return {
raise ValueError("封禁命令缺少必要参数") "type": "music",
if duration > 2592000: "data": {"type": "163", "id": song_id},
raise ValueError("封禁时间不能超过30天") }
return (
CommandType.GROUP_BAN.value, def handle_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]:
{ """处理封禁命令
"group_id": group_id,
"user_id": user_id, Args:
"duration": duration, args (Dict[str, Any]): 参数字典
}, group_info (GroupInfo): 群聊信息对应目标群聊
)
Returns:
def handle_whole_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: Tuple[CommandType, Dict[str, Any]]
"""处理全体禁言命令 """
duration: int = int(args["duration"])
Args: user_id: int = int(args["qq_id"])
args (Dict[str, Any]): 参数字典 group_id: int = int(group_info.group_id)
group_info (GroupInfo): 群聊信息对应目标群聊 if duration <= 0:
raise ValueError("封禁时间必须大于0")
Returns: if not user_id or not group_id:
Tuple[CommandType, Dict[str, Any]] raise ValueError("封禁命令缺少必要参数")
""" if duration > 2592000:
enable = args["enable"] raise ValueError("封禁时间不能超过30天")
assert isinstance(enable, bool), "enable参数必须是布尔值" return (
group_id: int = int(group_info.group_id) CommandType.GROUP_BAN.value,
if group_id <= 0: {
raise ValueError("群组ID无效") "group_id": group_id,
return ( "user_id": user_id,
CommandType.GROUP_WHOLE_BAN.value, "duration": duration,
{ },
"group_id": group_id, )
"enable": enable,
}, def handle_whole_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]:
) """处理全体禁言命令
def handle_kick_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: Args:
"""处理群成员踢出命令 args (Dict[str, Any]): 参数字典
group_info (GroupInfo): 群聊信息对应目标群聊
Args:
args (Dict[str, Any]): 参数字典 Returns:
group_info (GroupInfo): 群聊信息对应目标群聊 Tuple[CommandType, Dict[str, Any]]
"""
Returns: enable = args["enable"]
Tuple[CommandType, Dict[str, Any]] assert isinstance(enable, bool), "enable参数必须是布尔值"
""" group_id: int = int(group_info.group_id)
user_id: int = int(args["qq_id"]) if group_id <= 0:
group_id: int = int(group_info.group_id) raise ValueError("群组ID无效")
if group_id <= 0: return (
raise ValueError("群组ID无效") CommandType.GROUP_WHOLE_BAN.value,
if user_id <= 0: {
raise ValueError("用户ID无效") "group_id": group_id,
return ( "enable": enable,
CommandType.GROUP_KICK.value, },
{ )
"group_id": group_id,
"user_id": user_id, def handle_kick_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]:
"reject_add_request": False, # 不拒绝加群请求 """处理群成员踢出命令
},
) Args:
args (Dict[str, Any]): 参数字典
def handle_poke_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: group_info (GroupInfo): 群聊信息对应目标群聊
"""处理戳一戳命令
Returns:
Args: Tuple[CommandType, Dict[str, Any]]
args (Dict[str, Any]): 参数字典 """
group_info (GroupInfo): 群聊信息对应目标群聊 user_id: int = int(args["qq_id"])
group_id: int = int(group_info.group_id)
Returns: if group_id <= 0:
Tuple[CommandType, Dict[str, Any]] raise ValueError("群组ID无效")
""" if user_id <= 0:
user_id: int = int(args["qq_id"]) raise ValueError("用户ID无效")
if group_info is None: return (
group_id = None CommandType.GROUP_KICK.value,
else: {
group_id: int = int(group_info.group_id) "group_id": group_id,
if group_id <= 0: "user_id": user_id,
raise ValueError("群组ID无效") "reject_add_request": False, # 不拒绝加群请求
if user_id <= 0: },
raise ValueError("用户ID无效") )
return (
CommandType.SEND_POKE.value, def handle_poke_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]:
{ """处理戳一戳命令
"group_id": group_id,
"user_id": user_id, Args:
}, args (Dict[str, Any]): 参数字典
) group_info (GroupInfo): 群聊信息对应目标群聊
async def send_message_to_napcat(self, action: str, params: dict) -> dict: Returns:
request_uuid = str(uuid.uuid4()) Tuple[CommandType, Dict[str, Any]]
payload = json.dumps({"action": action, "params": params, "echo": request_uuid}) """
await self.server_connection.send(payload) user_id: int = int(args["qq_id"])
try: if group_info is None:
response = await get_response(request_uuid) group_id = None
except TimeoutError: else:
logger.error("发送消息超时,未收到响应") group_id: int = int(group_info.group_id)
return {"status": "error", "message": "timeout"} if group_id <= 0:
except Exception as e: raise ValueError("群组ID无效")
logger.error(f"发送消息失败: {e}") if user_id <= 0:
return {"status": "error", "message": str(e)} raise ValueError("用户ID无效")
return response return (
CommandType.SEND_POKE.value,
{
send_handler = SendHandler() "group_id": group_id,
"user_id": user_id,
},
)
async def send_message_to_napcat(self, action: str, params: dict) -> dict:
request_uuid = str(uuid.uuid4())
payload = json.dumps({"action": action, "params": params, "echo": request_uuid})
await self.server_connection.send(payload)
try:
response = await get_response(request_uuid)
except TimeoutError:
logger.error("发送消息超时,未收到响应")
return {"status": "error", "message": "timeout"}
except Exception as e:
logger.error(f"发送消息失败: {e}")
return {"status": "error", "message": str(e)}
return response
send_handler = SendHandler()