import base64 import json from typing import Any from nonebot.adapters import Bot, Event from nonebot.adapters.onebot.v11 import Message as V11Message from nonebot.adapters.onebot.v11 import MessageSegment as V11MessageSegment from nonebot.exception import ActionFailed import nonebot_plugin_alconna as alc from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna.uniseg.segment import ( At, AtAll, CustomNode, Image, Reference, Reply, Text, Video, ) from nonebot_plugin_alconna.uniseg.tools import reply_fetch from nonebot_plugin_session import EventSession from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger from zhenxun.services.tags import tag_manager as TagManager from zhenxun.utils.common_utils import CommonUtils from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.message import MessageUtils from .broadcast_manager import BroadcastManager MAX_FORWARD_DEPTH = 3 async def _process_forward_content( forward_content: Any, forward_id: str | None, bot: Bot, depth: int ) -> list[CustomNode]: """处理转发消息内容""" nodes_for_alc = [] content_parsed = False if forward_content: nodes_from_content = None if isinstance(forward_content, list): nodes_from_content = forward_content elif isinstance(forward_content, str): try: parsed_content = json.loads(forward_content) if isinstance(parsed_content, list): nodes_from_content = parsed_content except Exception as json_e: logger.debug( f"[Depth {depth}] JSON解析失败: {json_e}", "广播", ) if nodes_from_content is not None: logger.debug( f"[D{depth}] 节点数: {len(nodes_from_content)}", "广播", ) content_parsed = True for node_data in nodes_from_content: node = await _create_custom_node_from_data(node_data, bot, depth + 1) if node: nodes_for_alc.append(node) if not content_parsed and forward_id: logger.debug( f"[D{depth}] 尝试API调用ID: {forward_id}", "广播", ) try: forward_data = await bot.call_api("get_forward_msg", id=forward_id) nodes_list = None if isinstance(forward_data, dict) and "messages" in forward_data: nodes_list = forward_data["messages"] elif ( isinstance(forward_data, dict) and "data" in forward_data and isinstance(forward_data["data"], dict) and "message" in forward_data["data"] ): nodes_list = forward_data["data"]["message"] elif isinstance(forward_data, list): nodes_list = forward_data if nodes_list: node_count = len(nodes_list) logger.debug( f"[D{depth + 1}] 节点:{node_count}", "广播", ) for node_data in nodes_list: node = await _create_custom_node_from_data( node_data, bot, depth + 1 ) if node: nodes_for_alc.append(node) else: logger.warning( f"[D{depth + 1}] ID:{forward_id}无节点", "广播", ) nodes_for_alc.append( CustomNode( uid="0", name="错误", content="[嵌套转发消息获取失败]", ) ) except ActionFailed as af_e: logger.error( f"[D{depth + 1}] API失败: {af_e}", "广播", e=af_e, ) nodes_for_alc.append( CustomNode( uid="0", name="错误", content="[嵌套转发消息获取失败]", ) ) except Exception as e: logger.error( f"[D{depth + 1}] 处理出错: {e}", "广播", e=e, ) nodes_for_alc.append( CustomNode( uid="0", name="错误", content="[处理嵌套转发时出错]", ) ) elif not content_parsed and not forward_id: logger.warning( f"[D{depth}] 转发段无内容也无ID", "广播", ) nodes_for_alc.append( CustomNode( uid="0", name="错误", content="[嵌套转发消息无法解析]", ) ) elif content_parsed and not nodes_for_alc: logger.warning( f"[D{depth}] 解析成功但无有效节点", "广播", ) nodes_for_alc.append( CustomNode( uid="0", name="信息", content="[嵌套转发内容为空]", ) ) return nodes_for_alc async def _create_custom_node_from_data( node_data: dict, bot: Bot, depth: int ) -> CustomNode | None: """从节点数据创建CustomNode""" node_content_raw = node_data.get("message") or node_data.get("content") if not node_content_raw: logger.warning(f"[D{depth}] 节点缺少消息内容", "广播") return None sender = node_data.get("sender", {}) uid = str(sender.get("user_id", "10000")) name = sender.get("nickname", f"用户{uid[:4]}") extracted_uni_msg = await _extract_content_from_message( node_content_raw, bot, depth ) if not extracted_uni_msg: return None return CustomNode(uid=uid, name=name, content=extracted_uni_msg) async def _extract_broadcast_content( bot: Bot, event: Event, arp: alc.Arparma, session: EventSession, ) -> UniMessage | None: """从命令参数或引用消息中提取广播内容""" broadcast_content_msg: UniMessage | None = None command_content_list = arp.all_matched_args.get("content", []) processed_command_list = [] has_command_content = False if command_content_list: for item in command_content_list: if isinstance(item, alc.Segment): processed_command_list.append(item) if not (isinstance(item, Text) and not item.text.strip()): has_command_content = True elif isinstance(item, str): if item.strip(): processed_command_list.append(Text(item.strip())) has_command_content = True else: logger.warning( f"Unexpected type in command content: {type(item)}", "广播" ) if has_command_content: logger.debug("检测到命令参数内容,优先使用参数内容", "广播", session=session) broadcast_content_msg = UniMessage(processed_command_list) if not broadcast_content_msg.filter( lambda x: not (isinstance(x, Text) and not x.text.strip()) ): logger.warning( "命令参数内容解析后为空或只包含空白", "广播", session=session ) broadcast_content_msg = None if not broadcast_content_msg: reply_segment_obj: Reply | None = await reply_fetch(event, bot) if ( reply_segment_obj and hasattr(reply_segment_obj, "msg") and reply_segment_obj.msg ): logger.debug( "未检测到有效命令参数,检测到引用消息", "广播", session=session ) raw_quoted_content = reply_segment_obj.msg is_forward = False forward_id = None if isinstance(raw_quoted_content, V11Message): for seg in raw_quoted_content: if isinstance(seg, V11MessageSegment): if seg.type == "forward": forward_id = seg.data.get("id") is_forward = bool(forward_id) break elif seg.type == "json": try: json_data_str = seg.data.get("data", "{}") if isinstance(json_data_str, str): import json json_data = json.loads(json_data_str) if ( json_data.get("app") == "com.tencent.multimsg" or json_data.get("view") == "Forward" ) and json_data.get("meta", {}).get( "detail", {} ).get("resid"): forward_id = json_data["meta"]["detail"][ "resid" ] is_forward = True break except Exception: pass if is_forward and forward_id: logger.info( f"尝试获取并构造合并转发内容 (ID: {forward_id})", "广播", session=session, ) nodes_to_forward: list[CustomNode] = [] try: forward_data = await bot.call_api("get_forward_msg", id=forward_id) nodes_list = None if isinstance(forward_data, dict) and "messages" in forward_data: nodes_list = forward_data["messages"] elif ( isinstance(forward_data, dict) and "data" in forward_data and isinstance(forward_data["data"], dict) and "message" in forward_data["data"] ): nodes_list = forward_data["data"]["message"] elif isinstance(forward_data, list): nodes_list = forward_data if nodes_list is not None: for node_data in nodes_list: node_sender = node_data.get("sender", {}) node_user_id = str(node_sender.get("user_id", "10000")) node_nickname = node_sender.get( "nickname", f"用户{node_user_id[:4]}" ) node_content_raw = node_data.get( "message" ) or node_data.get("content") if node_content_raw: extracted_node_uni_msg = ( await _extract_content_from_message( node_content_raw, bot ) ) if extracted_node_uni_msg: nodes_to_forward.append( CustomNode( uid=node_user_id, name=node_nickname, content=extracted_node_uni_msg, ) ) if nodes_to_forward: broadcast_content_msg = UniMessage( Reference(nodes=nodes_to_forward) ) except ActionFailed: await MessageUtils.build_message( "获取合并转发消息失败,可能不支持此 API。" ).send(reply_to=True) return None except Exception as api_e: logger.error(f"处理合并转发时出错: {api_e}", "广播", e=api_e) await MessageUtils.build_message( "处理合并转发消息时发生内部错误。" ).send(reply_to=True) return None else: broadcast_content_msg = await _extract_content_from_message( raw_quoted_content, bot ) else: logger.debug("未检测到命令参数和引用消息", "广播", session=session) await MessageUtils.build_message("请提供广播内容或引用要广播的消息").send( reply_to=True ) return None if not broadcast_content_msg: logger.error( "未能从命令参数或引用消息中获取有效的广播内容", "广播", session=session ) await MessageUtils.build_message("错误:未能获取有效的广播内容。").send( reply_to=True ) return None return broadcast_content_msg async def _process_v11_segment( seg_obj: V11MessageSegment | dict, depth: int, index: int, bot: Bot ) -> list[alc.Segment]: """处理V11消息段""" result = [] seg_type = None data_dict = None if isinstance(seg_obj, V11MessageSegment): seg_type = seg_obj.type data_dict = seg_obj.data elif isinstance(seg_obj, dict): seg_type = seg_obj.get("type") data_dict = seg_obj.get("data") else: return result if not (seg_type and data_dict is not None): logger.warning(f"[D{depth}] 跳过无效数据: {type(seg_obj)}", "广播") return result if seg_type == "text": text_content = data_dict.get("text", "") if isinstance(text_content, str) and text_content.strip(): result.append(Text(text_content)) elif seg_type == "image": img_seg = None if data_dict.get("url"): img_seg = Image(url=data_dict["url"]) elif data_dict.get("file"): file_val = data_dict["file"] if isinstance(file_val, str) and file_val.startswith("base64://"): b64_data = file_val[9:] raw_bytes = base64.b64decode(b64_data) img_seg = Image(raw=raw_bytes) else: img_seg = Image(path=file_val) if img_seg: result.append(img_seg) else: logger.warning(f"[Depth {depth}] V11 图片 {index} 缺少URL/文件", "广播") elif seg_type == "at": target_qq = data_dict.get("qq", "") if target_qq.lower() == "all": result.append(AtAll()) elif target_qq: result.append(At(flag="user", target=target_qq)) elif seg_type == "video": if url := data_dict.get("url"): try: logger.debug(f"[D{depth}] 正在下载视频用于广播: {url}", "广播") video_bytes = await AsyncHttpx.get_content(url) video_seg = Video(raw=video_bytes) logger.debug( f"[D{depth}] 视频下载成功, 大小: {len(video_bytes)} bytes", "广播", ) result.append(video_seg) except Exception as e: logger.error(f"[D{depth}] 广播时下载视频失败: {url}", "广播", e=e) result.append(Text(f"[视频下载失败: {url}]")) elif file_val := data_dict.get("file"): if isinstance(file_val, str) and file_val.startswith("base64://"): b64_data = file_val[9:] raw_bytes = base64.b64decode(b64_data) video_seg = Video(raw=raw_bytes) result.append(video_seg) else: video_seg = Video(path=file_val) result.append(video_seg) return result elif seg_type == "forward": nested_forward_id = data_dict.get("id") or data_dict.get("resid") nested_forward_content = data_dict.get("content") logger.debug(f"[D{depth}] 嵌套转发ID: {nested_forward_id}", "广播") nested_nodes = await _process_forward_content( nested_forward_content, nested_forward_id, bot, depth ) if nested_nodes: result.append(Reference(nodes=nested_nodes)) else: logger.warning(f"[D{depth}] 跳过类型: {seg_type}", "广播") return result async def _extract_content_from_message( message_content: Any, bot: Bot, depth: int = 0 ) -> UniMessage: """提取消息内容到UniMessage""" temp_msg = UniMessage() input_type_str = str(type(message_content)) if depth >= MAX_FORWARD_DEPTH: logger.warning( f"[Depth {depth}] 达到最大递归深度 {MAX_FORWARD_DEPTH},停止解析嵌套转发。", "广播", ) temp_msg.append(Text("[嵌套转发层数过多,内容已省略]")) return temp_msg segments_to_process = [] if isinstance(message_content, UniMessage): segments_to_process = list(message_content) elif isinstance(message_content, V11Message): segments_to_process = list(message_content) elif isinstance(message_content, list): segments_to_process = message_content elif ( isinstance(message_content, dict) and "type" in message_content and "data" in message_content ): segments_to_process = [message_content] elif isinstance(message_content, str): if message_content.strip(): temp_msg.append(Text(message_content)) return temp_msg else: logger.warning(f"[Depth {depth}] 无法处理的输入类型: {input_type_str}", "广播") return temp_msg if segments_to_process: for index, seg_obj in enumerate(segments_to_process): try: if isinstance(seg_obj, Text): text_content = getattr(seg_obj, "text", None) if isinstance(text_content, str) and text_content.strip(): temp_msg.append(seg_obj) elif isinstance(seg_obj, Image): if ( getattr(seg_obj, "url", None) or getattr(seg_obj, "path", None) or getattr(seg_obj, "raw", None) ): temp_msg.append(seg_obj) elif isinstance(seg_obj, At): temp_msg.append(seg_obj) elif isinstance(seg_obj, AtAll): temp_msg.append(seg_obj) elif isinstance(seg_obj, Video): if ( getattr(seg_obj, "url", None) or getattr(seg_obj, "path", None) or getattr(seg_obj, "raw", None) ): temp_msg.append(seg_obj) logger.debug(f"[D{depth}] 处理Video对象成功", "广播") else: processed_segments = await _process_v11_segment( seg_obj, depth, index, bot ) temp_msg.extend(processed_segments) except Exception as e_conv_seg: logger.warning( f"[D{depth}] 处理段 {index} 出错: {e_conv_seg}", "广播", e=e_conv_seg, ) if not temp_msg and message_content: logger.warning(f"未能从类型 {input_type_str} 中提取内容", "广播") return temp_msg async def get_broadcast_target_groups( bot: Bot, session: EventSession, tag_name: str | None = None, force_send: bool = False, ) -> tuple[list, list]: """获取广播目标群组和启用了广播功能的群组""" target_groups_console: list[GroupConsole] = [] current_group_raw = getattr(session, "id2", None) or getattr( session, "group_id", None ) current_group_id = str(current_group_raw) if current_group_raw else None logger.debug(f"当前群组ID: {current_group_id}", "广播") if tag_name: tagged_group_ids = await TagManager.resolve_tag_to_group_ids(tag_name, bot=bot) if not tagged_group_ids: return [], [] valid_groups = await GroupConsole.filter(group_id__in=tagged_group_ids) if current_group_id: target_groups_console = [ group for group in valid_groups if str(group.group_id) != current_group_id ] excluded_msg = ( f",已排除当前群组({current_group_id})" if any( str(group.group_id) == current_group_id for group in valid_groups ) else "" ) broadcast_msg = ( f"向标签 '{tag_name}' 中的 {len(target_groups_console)} 个群组广播 " f"(ForceSend: {force_send}){excluded_msg}" ) logger.info(broadcast_msg, "广播", session=session) else: target_groups_console = valid_groups broadcast_msg = ( f"向标签 '{tag_name}' 中的 {len(target_groups_console)} 个群组广播 " f"(ForceSend: {force_send})" ) logger.info(broadcast_msg, "广播", session=session) else: all_groups, _ = await BroadcastManager.get_all_groups(bot) if current_group_id: target_groups_console = [ group for group in all_groups if str(group.group_id) != current_group_id ] logger.info( ( f"向除当前群组({current_group_id})外的所有群组广播 " f"(ForceSend: {force_send})" ), "广播", session=session, ) else: target_groups_console = all_groups logger.info( f"向所有群组广播 (ForceSend: {force_send})", "广播", session=session ) if not target_groups_console: if not tag_name: await MessageUtils.build_message("没有找到符合条件的广播目标群组。").send( reply_to=True ) return [], [] groups_to_actually_send = [] if force_send: groups_to_actually_send = target_groups_console logger.debug( f"强制发送模式,将向 {len(groups_to_actually_send)} 个目标群组尝试发送。", "广播", ) else: for group in target_groups_console: if not await CommonUtils.task_is_block(bot, "broadcast", group.group_id): groups_to_actually_send.append(group) logger.debug( f"普通发送模式,筛选后将向 {len(groups_to_actually_send)} " f"个目标群组尝试发送", "广播", ) return target_groups_console, groups_to_actually_send async def send_broadcast_and_notify( bot: Bot, event: Event, message: UniMessage, groups_to_send: list, all_target_groups_for_stats: list, session: EventSession, force_send: bool = False, ) -> None: """发送广播并通知结果""" BroadcastManager.clear_last_broadcast_msg_ids() count, error_count = await BroadcastManager.send_to_specific_groups( bot, message, groups_to_send, session, force_send ) result = f"成功广播 {count} 个群组" if error_count: result += f"\n发送失败 {error_count} 个群组" effective_sent_count = len(groups_to_send) total_considered_count = len(all_target_groups_for_stats) result += f"\n有效: {effective_sent_count} / 总计目标: {total_considered_count}" user_id = str(event.get_user_id()) await bot.send_private_msg(user_id=user_id, message=f"发送广播完成!\n{result}") BroadcastManager.log_info( f"广播完成,有效/总计目标: {effective_sent_count}/{total_considered_count}", session, )