Skip to content

Commit 24a7ca7

Browse files
committed
feat: 牛牛在吗?统计牛牛在线状况
- 超管可用,统计牛牛在线与离线情况 - 牛牛掉线通知,可邮件通知到各牛号主,详情配置见readme
1 parent 904512d commit 24a7ca7

5 files changed

Lines changed: 395 additions & 3 deletions

File tree

.env

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,16 @@ TTS_ENABLE=false
125125
# 重试次数
126126
#AI_SERVER_RETRY=3
127127

128-
129-
128+
#bot_status 配置
129+
# bot_status_smtp_user=your_email@example.com
130+
# bot_status_smtp_password=your_email_password
131+
# bot_status_smtp_server=smtp.example.com
132+
# bot_status_smtp_port=465
133+
# bot_status_notice_email=admin@example.com
134+
#离线等待时长
135+
# bot_status_offline_grace_time=30
136+
137+
#help 配置
130138
# 默认使用的样式名称
131139
# 可选值包括:
132140
# - "default" - 库自带默认样式
@@ -144,4 +152,4 @@ TTS_ENABLE=false
144152
#CUSTOM_STYLES=[]
145153

146154
# 忽略的插件列表
147-
#IGNORED_PLUGINS=["nonebot-plugin-alconna","nonebot_plugin_apscheduler","auto_accept","callback","block","greeting"]
155+
#IGNORED_PLUGINS=["nonebot-plugin-alconna","nonebot_plugin_apscheduler","auto_accept","callback","block","greeting","bot_status"]

src/plugins/bot_status/__init__.py

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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)

src/plugins/bot_status/config.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from nonebot import get_driver, get_plugin_config
2+
from pydantic import BaseModel
3+
4+
5+
class Config(BaseModel):
6+
# 邮件推送配置
7+
bot_status_smtp_user: str = ""
8+
bot_status_smtp_password: str = ""
9+
bot_status_smtp_server: str = ""
10+
bot_status_smtp_port: int = 465
11+
bot_status_notice_email: str = ""
12+
# 离线等待时长
13+
bot_status_offline_grace_time: int = 30
14+
15+
16+
class MailConfig:
17+
def __init__(self, user: str, password: str, server: str, port: int, notice_email: str):
18+
self.user = user
19+
self.password = password
20+
self.server = server
21+
self.port = port
22+
self.notice_email = notice_email
23+
24+
def check_params(self) -> bool:
25+
"""检查参数是否填写完整"""
26+
if self.user and self.password and self.server and self.port and self.notice_email:
27+
return True
28+
else:
29+
return False
30+
31+
32+
driver = get_driver()
33+
global_config = driver.config
34+
plugin_config = get_plugin_config(Config)

src/plugins/bot_status/readme.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
离线邮箱通知会向BotConfig中设置好的admin与.env中设置的邮箱发送邮件,因此想给号主发邮件请配置好admins
2+
没有给Bot配置admins的请使用/tools/config.mongodb来添加
3+
邮箱配置请参考各邮箱的smtp配置

0 commit comments

Comments
 (0)