|
| 1 | +from datetime import datetime, timedelta |
| 2 | + |
| 3 | +from nonebot import ( |
| 4 | + get_bots, |
| 5 | + get_driver, |
| 6 | + logger, |
| 7 | + on_command, |
| 8 | + require, |
| 9 | +) |
| 10 | +from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent |
| 11 | +from nonebot.permission import SUPERUSER |
| 12 | +from nonebot.plugin import PluginMetadata |
| 13 | + |
| 14 | +from src.common.config import BotConfig, GroupConfig |
| 15 | +from src.plugins.block import plugin_config as block_config |
| 16 | + |
| 17 | +from .config import MailConfig, plugin_config |
| 18 | +from .utils import send_mail |
| 19 | + |
| 20 | +__plugin_meta__ = PluginMetadata( |
| 21 | + name="牛牛状态查询", |
| 22 | + description="查询当前连接的Bot状态,检测Bot离线并发送通知", |
| 23 | + usage=""" |
| 24 | +牛牛在吗 - 查询当前连接的Bot列表 |
| 25 | +测试邮件 - 测试邮件发送功能 |
| 26 | +""", |
| 27 | + type="application", |
| 28 | + homepage="https://github.com/PallasBot", |
| 29 | + supported_adapters={"~onebot.v11"}, |
| 30 | + extra={ |
| 31 | + "version": "2.0.0", |
| 32 | + "menu_data": [ |
| 33 | + { |
| 34 | + "func": "查看牛牛在线状况", |
| 35 | + "trigger_method": "on_message", |
| 36 | + "trigger_condition": "牛牛在吗", |
| 37 | + "brief_des": "总计牛牛在线情况", |
| 38 | + "detail_des": "当牛牛离线时发送离线通知邮件给号主与Superuser", |
| 39 | + }, |
| 40 | + { |
| 41 | + "func": "发送测试邮件", |
| 42 | + "trigger_method": "on_message", |
| 43 | + "trigger_condition": "测试邮件", |
| 44 | + "brief_des": "发送测试邮件", |
| 45 | + "detail_des": "给配置中的邮箱发送测试邮件", |
| 46 | + }, |
| 47 | + ], |
| 48 | + "menu_template": "default", |
| 49 | + }, |
| 50 | +) |
| 51 | + |
| 52 | + |
| 53 | +STATUS_COOLDOWN_KEY: str = "bot_status" |
| 54 | + |
| 55 | +bot_status_cmd = on_command("牛牛在吗", permission=SUPERUSER, priority=5, block=True) |
| 56 | +test_mail_cmd = on_command("测试邮件", permission=SUPERUSER, priority=5, block=True) |
| 57 | + |
| 58 | +scheduler = require("nonebot_plugin_apscheduler").scheduler |
| 59 | + |
| 60 | +# 邮件配置 |
| 61 | +mail_config: MailConfig = MailConfig( |
| 62 | + user=plugin_config.bot_status_smtp_user, |
| 63 | + password=plugin_config.bot_status_smtp_password, |
| 64 | + server=plugin_config.bot_status_smtp_server, |
| 65 | + port=plugin_config.bot_status_smtp_port, |
| 66 | + notice_email=plugin_config.bot_status_notice_email, |
| 67 | +) |
| 68 | + |
| 69 | + |
| 70 | +offline_bots: dict[int, dict[str, str]] = {} |
| 71 | + |
| 72 | +driver = get_driver() |
| 73 | + |
| 74 | + |
| 75 | +@driver.on_startup |
| 76 | +async def startup() -> None: |
| 77 | + logger.info("Bot_status is running") |
| 78 | + |
| 79 | + |
| 80 | +@driver.on_bot_connect |
| 81 | +async def handle_bot_connect(bot: Bot) -> None: |
| 82 | + bot_id: int = int(bot.self_id) |
| 83 | + if bot_id in offline_bots: |
| 84 | + del offline_bots[bot_id] |
| 85 | + |
| 86 | + |
| 87 | +@driver.on_bot_disconnect |
| 88 | +async def handle_bot_disconnect(bot: Bot) -> None: |
| 89 | + bot_id: int = int(bot.self_id) |
| 90 | + |
| 91 | + nickname: str = "Unknown" |
| 92 | + try: |
| 93 | + bots = get_bots() |
| 94 | + online_bot = None # type: ignore |
| 95 | + for bot_instance in bots.values(): |
| 96 | + if str(bot_id) != bot_instance.self_id: |
| 97 | + online_bot = bot_instance # type: ignore |
| 98 | + break |
| 99 | + |
| 100 | + if online_bot: |
| 101 | + info = await online_bot.call_api("get_stranger_info", user_id=bot_id) |
| 102 | + nickname = info.get("nickname", "Unknown Nickname") |
| 103 | + except Exception as e: |
| 104 | + logger.debug(f"Failed to get bot {bot_id} info: {e}") |
| 105 | + |
| 106 | + offline_bots[bot_id] = { |
| 107 | + "nickname": nickname, |
| 108 | + "offline_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), |
| 109 | + } |
| 110 | + |
| 111 | + job_id: str = f"bot_status_check_{bot_id}" |
| 112 | + if scheduler.get_job(job_id): |
| 113 | + scheduler.remove_job(job_id) |
| 114 | + |
| 115 | + # 计算运行时间 |
| 116 | + run_time: datetime = datetime.now() + timedelta(seconds=plugin_config.bot_status_offline_grace_time) |
| 117 | + |
| 118 | + scheduler.add_job( |
| 119 | + id=job_id, |
| 120 | + func=check_bot_still_offline, |
| 121 | + args=[ |
| 122 | + bot_id, |
| 123 | + nickname, |
| 124 | + bot.adapter.get_name() if hasattr(bot.adapter, "get_name") else "Unknown Adapter", |
| 125 | + ], |
| 126 | + misfire_grace_time=60, |
| 127 | + coalesce=True, |
| 128 | + max_instances=1, |
| 129 | + trigger="date", |
| 130 | + run_date=run_time, |
| 131 | + ) |
| 132 | + |
| 133 | + |
| 134 | +async def check_bot_still_offline(bot_id: int, nickname: str, adapter_name: str) -> None: |
| 135 | + """检查Bot是否真的离线""" |
| 136 | + bots = get_bots() |
| 137 | + if str(bot_id) not in bots: |
| 138 | + logger.warning(f"Bot {bot_id} offline, sending notification") |
| 139 | + offline_bots[bot_id]["offline_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| 140 | + await notify_bot_offline(bot_id, nickname, adapter_name) |
| 141 | + else: |
| 142 | + if bot_id in offline_bots: |
| 143 | + del offline_bots[bot_id] |
| 144 | + |
| 145 | + |
| 146 | +async def get_bot_admin_emails(bot_id: int) -> list[str]: |
| 147 | + """获取Bot的admins邮箱列表""" |
| 148 | + emails: list[str] = [] |
| 149 | + |
| 150 | + try: |
| 151 | + bot_config = BotConfig(bot_id=bot_id) |
| 152 | + admins = await bot_config._find("admins") |
| 153 | + |
| 154 | + # 为每个admin生成QQ邮箱 |
| 155 | + if admins: |
| 156 | + emails.extend(f"{admin_id}@qq.com" for admin_id in admins) |
| 157 | + except Exception as e: |
| 158 | + logger.debug(f"Failed to get admins for bot {bot_id}: {e}") |
| 159 | + |
| 160 | + return emails |
| 161 | + |
| 162 | + |
| 163 | +@test_mail_cmd.handle() |
| 164 | +async def handle_test_mail(bot: Bot, event: MessageEvent) -> None: |
| 165 | + """测试邮件""" |
| 166 | + if isinstance(event, GroupMessageEvent): |
| 167 | + config = GroupConfig(group_id=event.group_id, cooldown=10) |
| 168 | + if not await config.is_cooldown(STATUS_COOLDOWN_KEY): |
| 169 | + return |
| 170 | + await config.refresh_cooldown(STATUS_COOLDOWN_KEY) |
| 171 | + |
| 172 | + if not mail_config.check_params(): |
| 173 | + missing_params: list[str] = [] |
| 174 | + if not plugin_config.bot_status_smtp_user: |
| 175 | + missing_params.append("bot_status_smtp_user") |
| 176 | + if not plugin_config.bot_status_smtp_password: |
| 177 | + missing_params.append("bot_status_smtp_password") |
| 178 | + if not plugin_config.bot_status_smtp_server: |
| 179 | + missing_params.append("bot_status_smtp_server") |
| 180 | + if not plugin_config.bot_status_notice_email: |
| 181 | + missing_params.append("bot_status_notice_email") |
| 182 | + |
| 183 | + await test_mail_cmd.finish(f"邮箱配置缺少参数: {', '.join(missing_params)}") |
| 184 | + return |
| 185 | + |
| 186 | + title: str = "[Test] 这是一封测试邮件" |
| 187 | + content: str = f""" |
| 188 | +牛牛在吗? |
| 189 | +
|
| 190 | +发送时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} |
| 191 | +
|
| 192 | +Bot ID: {bot.self_id} |
| 193 | +
|
| 194 | +如果你收到了这封邮件,证明邮箱配置正确。 |
| 195 | + """.strip() |
| 196 | + |
| 197 | + result: str | None = await send_mail(title, content, mail_config) |
| 198 | + if result: |
| 199 | + await test_mail_cmd.finish(f"测试邮件发送失败: {result}") |
| 200 | + else: |
| 201 | + await test_mail_cmd.finish("测试邮件发送成功!") |
| 202 | + |
| 203 | + |
| 204 | +async def notify_bot_offline(bot_id: int, nickname: str, adapter_name: str = "Unknown Adapter") -> None: |
| 205 | + """通知Bot离线""" |
| 206 | + # 获取admin邮箱列表 |
| 207 | + admin_emails: list[str] = await get_bot_admin_emails(bot_id) |
| 208 | + |
| 209 | + # 发送邮件通知 |
| 210 | + if mail_config.check_params(): |
| 211 | + title: str = f"[牛牛不见啦] Bot {bot_id} is Offline" |
| 212 | + content: str = f""" |
| 213 | +掉线通知 |
| 214 | +你的牛牛:{nickname},账号:{bot_id}掉线啦,快去看看怎么回事吧 |
| 215 | +
|
| 216 | +掉线时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} |
| 217 | +
|
| 218 | + """.strip() |
| 219 | + |
| 220 | + # 发送给配置的邮箱 |
| 221 | + result: str | None = await send_mail(title, content, mail_config) |
| 222 | + if result: |
| 223 | + logger.error(f"Failed to send offline notification mail: {result}") |
| 224 | + else: |
| 225 | + logger.info(f"Offline notification mail sent for bot {bot_id}") |
| 226 | + |
| 227 | + # 发送给admin邮箱 |
| 228 | + for email in admin_emails: |
| 229 | + try: |
| 230 | + admin_mail_config: MailConfig = MailConfig( |
| 231 | + user=plugin_config.bot_status_smtp_user, |
| 232 | + password=plugin_config.bot_status_smtp_password, |
| 233 | + server=plugin_config.bot_status_smtp_server, |
| 234 | + port=plugin_config.bot_status_smtp_port, |
| 235 | + notice_email=email, |
| 236 | + ) |
| 237 | + result = await send_mail(title, content, admin_mail_config) |
| 238 | + if result: |
| 239 | + logger.error(f"Failed to send offline notification mail to admin {email}: {result}") |
| 240 | + else: |
| 241 | + logger.info(f"Offline notification mail sent to admin {email} for bot {bot_id}") |
| 242 | + except Exception as e: |
| 243 | + logger.error(f"Exception occurred while sending mail to admin {email}: {e}") |
| 244 | + else: |
| 245 | + logger.warning("Mail configuration incomplete, cannot send offline notification") |
| 246 | + |
| 247 | + |
| 248 | +@bot_status_cmd.handle() |
| 249 | +async def handle_bot_status(bot: Bot, event: MessageEvent) -> None: |
| 250 | + """处理Bot状态查询命令""" |
| 251 | + if isinstance(event, GroupMessageEvent): |
| 252 | + config = GroupConfig(group_id=event.group_id, cooldown=10) |
| 253 | + if not await config.is_cooldown(STATUS_COOLDOWN_KEY): |
| 254 | + return |
| 255 | + await config.refresh_cooldown(STATUS_COOLDOWN_KEY) |
| 256 | + |
| 257 | + # 显示在线Bot |
| 258 | + online_info: str = "" |
| 259 | + online_count: int = len(block_config.bots) if block_config.bots else 0 |
| 260 | + if block_config.bots: |
| 261 | + bot_info_list: list[str] = [] |
| 262 | + for bot_id in block_config.bots: |
| 263 | + try: |
| 264 | + info = await bot.call_api("get_stranger_info", user_id=bot_id) |
| 265 | + nickname: str = info.get("nickname", "Unknown Nickname") |
| 266 | + bot_info_list.append(f"{nickname} ({bot_id})") |
| 267 | + except Exception: |
| 268 | + # 如果无法获取信息,则只显示ID |
| 269 | + bot_info_list.append(f"Unknown Nickname ({bot_id})") |
| 270 | + |
| 271 | + online_info = f"Online bots (Total: {online_count}):\n" + "\n".join(bot_info_list) |
| 272 | + else: |
| 273 | + online_info = "No bots are currently online" |
| 274 | + |
| 275 | + # 显示离线Bot |
| 276 | + offline_info: str = "" |
| 277 | + offline_count: int = len(offline_bots) if offline_bots else 0 |
| 278 | + if offline_bots: |
| 279 | + offline_list: list[str] = [] |
| 280 | + for bot_id, info in offline_bots.items(): |
| 281 | + offline_list.append(f"{info['nickname']} ({bot_id})") |
| 282 | + offline_info = f"\n\nOffline bots (Total: {offline_count}):\n" + "\n".join(offline_list) |
| 283 | + |
| 284 | + if offline_info: |
| 285 | + message: str = online_info + offline_info |
| 286 | + elif online_info: |
| 287 | + message = online_info |
| 288 | + else: |
| 289 | + message = "No bots are currently online or offline" |
| 290 | + |
| 291 | + await bot_status_cmd.finish(message) |
0 commit comments