diff --git a/core/api/group.py b/core/api/group.py index b2ffb71..2f2b6ac 100644 --- a/core/api/group.py +++ b/core/api/group.py @@ -4,7 +4,7 @@ 该模块定义了 `GroupAPI` Mixin 类,提供了所有与群组管理、成员操作 等相关的 OneBot v11 API 封装。 """ -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any import json from core.redis_manager import redis_manager from .base import BaseAPI diff --git a/core/api/message.py b/core/api/message.py index 25458a4..c37b5a1 100644 --- a/core/api/message.py +++ b/core/api/message.py @@ -105,7 +105,7 @@ class MessageAPI(BaseAPI): """ return await self.call_api("get_msg", {"message_id": message_id}) - async def get_forward_msg(self, id: str) -> Dict[str, Any]: + async def get_forward_msg(self, id: str) -> List[Dict[str, Any]]: """ 获取合并转发消息的内容。 @@ -113,9 +113,21 @@ class MessageAPI(BaseAPI): id (str): 合并转发消息的 ID。 Returns: - Dict[str, Any]: OneBot API 的响应数据,包含转发消息的节点列表。 + List[Dict[str, Any]]: 转发消息的节点列表。 """ - return await self.call_api("get_forward_msg", {"id": id}) + forward_data = await self.call_api("get_forward_msg", {"id": id}) + nodes = forward_data.get("data") + + if not isinstance(nodes, list): + # 兼容某些实现可能将节点放在 'messages' 键下 + data = forward_data.get('data', {}) + if isinstance(data, dict): + nodes = data.get('messages') + + if not isinstance(nodes, list): + raise ValueError("在 get_forward_msg 响应中找不到消息节点列表") + + return nodes async def send_group_forward_msg(self, group_id: int, messages: List[Dict[str, Any]]) -> Dict[str, Any]: """ diff --git a/models/objects.py b/models/objects.py index 2f20c51..6f8a5ac 100644 --- a/models/objects.py +++ b/models/objects.py @@ -24,6 +24,12 @@ class GroupInfo: max_member_count: int = 0 """最大成员数""" + group_remark: str = "" + """群备注""" + + group_all_shut: int = 0 + """是否全员禁言""" + @dataclass class GroupMemberInfo: diff --git a/plugins/broadcast.py b/plugins/broadcast.py index 05658ca..b150214 100644 --- a/plugins/broadcast.py +++ b/plugins/broadcast.py @@ -6,83 +6,111 @@ - 通过回复一条消息并发送指令,将该消息转发给机器人所在的所有群聊。 - 此插件不写入 __plugin_meta__,保持隐藏。 """ +import asyncio from core.command_manager import matcher -from models import MessageEvent +from models import MessageEvent, PrivateMessageEvent from core.permission_manager import ADMIN from core.logger import logger +# --- 会话状态管理 --- +# 结构: {user_id: asyncio.TimerHandle} +broadcast_sessions: dict[int, asyncio.TimerHandle] = {} + +def cleanup_session(user_id: int): + """ + 清理超时的广播会话。 + """ + if user_id in broadcast_sessions: + del broadcast_sessions[user_id] + logger.info(f"[Broadcast] 会话 {user_id} 已超时,自动取消。") + @matcher.command("broadcast", "广播", permission=ADMIN) -async def broadcast_message(event: MessageEvent): +async def broadcast_start(event: MessageEvent): """ - 广播指令处理器。 - - :param event: 消息事件对象。 + 广播指令的入口,启动一个等待用户消息的会话。 """ - # 1. 检查是否为私聊消息 - # 使用 hasattr 安全地检查 group_id 属性,避免 AttributeError - if hasattr(event, 'group_id') and event.group_id: - # 在群聊中调用时,静默处理,不予响应 + # 1. 仅限私聊 + if not isinstance(event, PrivateMessageEvent): return - # 2. 检查是否回复了某条消息 - reply = event.reply - if not reply: - await event.reply("请通过“回复”一条您想广播的消息来使用此功能。") + user_id = event.user_id + + # 如果上一个会话的超时任务还在,先取消它 + if user_id in broadcast_sessions: + broadcast_sessions[user_id].cancel() + + await event.reply("已进入广播模式,请在 60 秒内发送您想要广播的消息内容。") + + # 设置 60 秒超时 + loop = asyncio.get_running_loop() + timeout_handler = loop.call_later( + 60, + cleanup_session, + user_id + ) + broadcast_sessions[user_id] = timeout_handler + +@matcher.on_message() +async def handle_broadcast_content(event: MessageEvent): + """ + 通用消息处理器,用于捕获广播模式下的消息输入。 + 将捕获到的消息打包成一个新的合并转发消息并广播。 + """ + # 仅处理私聊消息,且用户在广播会话中 + if not isinstance(event, PrivateMessageEvent) or event.user_id not in broadcast_sessions: return - # 3. 获取机器人所在的群聊列表 + user_id = event.user_id + + # 成功捕获到消息,取消超时任务并清理会话 + broadcast_sessions[user_id].cancel() + del broadcast_sessions[user_id] + + message_to_broadcast = event.message + if not message_to_broadcast: + await event.reply("捕获到的消息为空,已取消广播。") + return True + + # --- 执行广播逻辑 --- bot = event.bot try: group_list = await bot.get_group_list() if not group_list: await event.reply("机器人目前没有加入任何群聊。") - return + return True except Exception as e: logger.error(f"[Broadcast] 获取群聊列表失败: {e}") - await event.reply(f"获取群聊列表时发生错误,无法广播。错误信息: {e}") - return + await event.reply(f"获取群聊列表时发生错误: {e}") + return True - # 4. 获取被回复的消息内容 - try: - message_data = await bot.get_msg(reply.message_id) - message_content = message_data.get("message", "") - if not message_content: - await event.reply("无法获取被回复的消息内容,广播失败。") - return - except Exception as e: - logger.error(f"[Broadcast] 获取消息内容失败: {e}") - await event.reply(f"获取消息内容时发生错误: {e}") - return - - # 5. 遍历所有群聊并发送消息 - success_count = 0 - failed_count = 0 + success_count, failed_count = 0, 0 total_groups = len(group_list) - - await event.reply(f"准备向 {total_groups} 个群聊广播消息,请稍候...") + await event.reply(f"已收到广播内容,准备打包并向 {total_groups} 个群聊广播...") - for group in group_list: - group_id = group.group_id - if not group_id: - continue - - try: - # 发送消息到群聊 - await bot.send_group_msg( - group_id=group_id, - message=message_content + # --- 将管理员发送的消息打包成一个单节点的合并转发消息 --- + try: + nodes_to_send = [ + bot.build_forward_node( + user_id=event.user_id, + nickname=event.sender.nickname, + message=message_to_broadcast ) + ] + except Exception as e: + logger.error(f"[Broadcast] 构建转发节点失败: {e}") + await event.reply(f"构建转发消息节点时发生错误: {e}") + return True + + # --- 向所有群聊发送打包好的合并转发消息 --- + for group in group_list: + try: + await bot.send_group_forward_msg(group.group_id, nodes_to_send) success_count += 1 - logger.info(f"[Broadcast] 已成功将消息发送至群聊: {group_id}") except Exception as e: failed_count += 1 - logger.error(f"[Broadcast] 发送消息至群聊 {group_id} 失败: {e}") - - # 6. 向管理员报告结果 - report_message = ( - f"广播任务完成。\n" - f"总群聊数: {total_groups}\n" - f"成功: {success_count}\n" - f"失败: {failed_count}" - ) - await event.reply(report_message) + logger.error(f"[Broadcast] 转发至群聊 {group.group_id} 失败: {e}") + + report = f"广播完成。\n总群聊: {total_groups}\n成功: {success_count}\n失败: {failed_count}" + await event.reply(report) + + return True # 消费事件,防止其他处理器响应