673 lines
29 KiB
Python
Executable File
673 lines
29 KiB
Python
Executable File
import asyncio
|
||
import logging
|
||
import math
|
||
import time
|
||
from datetime import datetime, timezone
|
||
from dateutil import parser
|
||
from dotenv import load_dotenv
|
||
import os
|
||
|
||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||
from telegram.ext import (
|
||
ApplicationBuilder, CommandHandler, MessageHandler, CallbackQueryHandler,
|
||
ConversationHandler, ContextTypes, filters
|
||
)
|
||
|
||
from nezha_api import NezhaAPI
|
||
from database import Database
|
||
|
||
# 配置日志
|
||
logging.basicConfig(
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||
level=logging.INFO
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
load_dotenv()
|
||
|
||
# 定义常量和配置
|
||
TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN')
|
||
DATABASE_PATH = 'users.db'
|
||
|
||
# 定义阶段
|
||
BIND_USERNAME, BIND_PASSWORD, BIND_DASHBOARD = range(3)
|
||
SEARCH_SERVER = range(1)
|
||
|
||
# 初始化数据库
|
||
db = Database(DATABASE_PATH)
|
||
|
||
# 添加 format_bytes 函数
|
||
def format_bytes(size_in_bytes):
|
||
if size_in_bytes == 0:
|
||
return "0B"
|
||
units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||
power = int(math.floor(math.log(size_in_bytes, 1024)))
|
||
power = min(power, len(units) - 1) # 防止超过单位列表的范围
|
||
size = size_in_bytes / (1024 ** power)
|
||
formatted_size = f"{size:.2f}{units[power]}"
|
||
return formatted_size
|
||
|
||
def is_online(server):
|
||
"""根据last_active判断服务器是否在线,如果最后活跃时间在10秒内则为在线。"""
|
||
now_utc = datetime.now(timezone.utc)
|
||
last_active_str = server.get('last_active')
|
||
if not last_active_str:
|
||
return False
|
||
try:
|
||
last_active_dt = parser.isoparse(last_active_str)
|
||
except ValueError:
|
||
return False
|
||
last_active_utc = last_active_dt.astimezone(timezone.utc)
|
||
diff = now_utc - last_active_utc
|
||
is_on = diff.total_seconds() < 10
|
||
logger.info("Checking online: diff=%s now=%s last=%s is_online=%s",
|
||
diff, now_utc, last_active_utc, is_on)
|
||
return is_on
|
||
|
||
# 添加 IP 地址掩码函数
|
||
def mask_ipv4(ipv4_address):
|
||
if ipv4_address == '未知' or ipv4_address == '❌':
|
||
return ipv4_address
|
||
parts = ipv4_address.split('.')
|
||
if len(parts) != 4:
|
||
return ipv4_address # 非法的 IPv4 地址,直接返回
|
||
# 将后两部分替换为 'xx'
|
||
masked_ip = f"{parts[0]}.{parts[1]}.xx.xx"
|
||
return masked_ip
|
||
|
||
def mask_ipv6(ipv6_address):
|
||
if ipv6_address == '未知' or ipv6_address == '❌':
|
||
return ipv6_address
|
||
parts = ipv6_address.split(':')
|
||
if len(parts) < 3:
|
||
return ipv6_address # 非法的 IPv6 地址,直接返回
|
||
# 只显示前两个部分,后面用 'xx' 替代
|
||
masked_ip = ':'.join(parts[:2]) + ':xx:xx:xx:xx'
|
||
return masked_ip
|
||
|
||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text(
|
||
"欢迎使用 Nezha 监控机器人!\n请使用 /bind 命令绑定您的账号。\n请注意,使用公共机器人有安全风险,用户名密码将会被记录用以鉴权,解绑删除。"
|
||
)
|
||
|
||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text("""
|
||
可用命令:
|
||
/bind - 绑定账号
|
||
/unbind - 解绑账号
|
||
/overview - 查看服务器状态总览
|
||
/server - 查看单台服务器状态
|
||
/cron - 执行计划任务
|
||
/services - 查看服务状态总览
|
||
/help - 获取帮助
|
||
""")
|
||
|
||
async def bind_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
# 检查当前对话类型
|
||
if update.effective_chat.type != "private":
|
||
await update.message.reply_text("请与机器人私聊进行绑定操作,\n避免机密信息泄露。")
|
||
return ConversationHandler.END
|
||
|
||
user = await db.get_user(update.effective_user.id)
|
||
if user:
|
||
await update.message.reply_text("您已绑定账号,如需重新绑定,请先使用 /unbind 命令解绑。")
|
||
return ConversationHandler.END
|
||
else:
|
||
await update.message.reply_text("请输入您的用户名:")
|
||
return BIND_USERNAME
|
||
|
||
async def bind_username(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
context.user_data['username'] = update.message.text.strip()
|
||
await update.message.reply_text("请输入您的密码:")
|
||
return BIND_PASSWORD
|
||
|
||
async def bind_password(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
context.user_data['password'] = update.message.text.strip()
|
||
await update.message.reply_text("请输入您的 Dashboard 地址(例如:https://nezha.example.com):")
|
||
return BIND_DASHBOARD
|
||
|
||
async def bind_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
dashboard_url = update.message.text.strip()
|
||
context.user_data['dashboard_url'] = dashboard_url
|
||
telegram_id = update.effective_user.id
|
||
username = context.user_data['username']
|
||
password = context.user_data['password']
|
||
|
||
# 测试连接
|
||
try:
|
||
api = NezhaAPI(dashboard_url, username, password)
|
||
await api.authenticate()
|
||
await api.close()
|
||
except Exception as e:
|
||
await update.message.reply_text(f"绑定失败:{e}\n请检查您的信息并重新绑定。")
|
||
return ConversationHandler.END
|
||
|
||
# 保存到数据库
|
||
await db.add_user(telegram_id, username, password, dashboard_url)
|
||
await update.message.reply_text("绑定成功!您现在可以使用机器人的功能了。")
|
||
return ConversationHandler.END
|
||
|
||
async def unbind(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user = await db.get_user(update.effective_user.id)
|
||
if user:
|
||
await db.delete_user(update.effective_user.id)
|
||
await update.message.reply_text("已解绑。")
|
||
else:
|
||
await update.message.reply_text("您尚未绑定账号。")
|
||
|
||
async def overview(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user = await db.get_user(update.effective_user.id)
|
||
if not user:
|
||
await update.message.reply_text("请先使用 /bind 命令绑定您的账号。")
|
||
return
|
||
|
||
api = NezhaAPI(user['dashboard_url'], user['username'], user['password'])
|
||
try:
|
||
data = await api.get_overview()
|
||
except Exception as e:
|
||
await update.message.reply_text(f"获取数据失败:{e}")
|
||
await api.close()
|
||
return
|
||
# print("返回的服务数据:", data)
|
||
|
||
if data and data.get('success'):
|
||
servers = data['data']
|
||
online_servers = sum(1 for s in servers if is_online(s))
|
||
total_servers = len(servers)
|
||
total_mem = sum(s['host'].get('mem_total', 0) for s in servers if s.get('host'))
|
||
used_mem = sum(s['state'].get('mem_used', 0) for s in servers if s.get('state'))
|
||
total_swap = sum(s['host'].get('swap_total', 0) for s in servers if s.get('host'))
|
||
used_swap = sum(s['state'].get('swap_used', 0) for s in servers if s.get('state'))
|
||
total_disk = sum(s['host'].get('disk_total', 0) for s in servers if s.get('host'))
|
||
used_disk = sum(s['state'].get('disk_used', 0) for s in servers if s.get('state'))
|
||
net_in_speed = sum(s['state'].get('net_in_speed', 0) for s in servers if s.get('state'))
|
||
net_out_speed = sum(s['state'].get('net_out_speed', 0) for s in servers if s.get('state'))
|
||
net_in_transfer = sum(s['state'].get('net_in_transfer', 0) for s in servers if s.get('state'))
|
||
net_out_transfer = sum(s['state'].get('net_out_transfer', 0) for s in servers if s.get('state'))
|
||
transfer_ratio = (net_out_transfer / net_in_transfer * 100) if net_in_transfer else 0
|
||
|
||
response = f"""📊 **统计信息**
|
||
===========================
|
||
**服务器数量**: {total_servers}
|
||
**在线服务器**: {online_servers}
|
||
**内存**: {used_mem / total_mem * 100 if total_mem else 0:.1f}% [{format_bytes(used_mem)}/{format_bytes(total_mem)}]
|
||
**交换**: {used_swap / total_swap * 100 if total_swap else 0:.1f}% [{format_bytes(used_swap)}/{format_bytes(total_swap)}]
|
||
**磁盘**: {used_disk / total_disk * 100 if total_disk else 0:.1f}% [{format_bytes(used_disk)}/{format_bytes(total_disk)}]
|
||
**下行速度**: ↓{format_bytes(net_in_speed)}/s
|
||
**上行速度**: ↑{format_bytes(net_out_speed)}/s
|
||
**下行流量**: ↓{format_bytes(net_in_transfer)}
|
||
**上行流量**: ↑{format_bytes(net_out_transfer)}
|
||
**流量对等性**: {transfer_ratio:.1f}%
|
||
|
||
**更新于**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC
|
||
"""
|
||
keyboard = [[InlineKeyboardButton("刷新", callback_data="refresh_overview")]]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await update.message.reply_text(response, parse_mode='Markdown', reply_markup=reply_markup)
|
||
else:
|
||
await update.message.reply_text("获取服务器信息失败。")
|
||
await api.close()
|
||
|
||
async def server_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user = await db.get_user(update.effective_user.id)
|
||
if not user:
|
||
await update.message.reply_text("请先使用 /bind 命令绑定您的账号。")
|
||
return
|
||
|
||
await update.message.reply_text("请输入要查询的服务器名称(支持模糊搜索):")
|
||
return SEARCH_SERVER
|
||
|
||
async def search_server(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
query_text = update.message.text.strip()
|
||
user = await db.get_user(update.effective_user.id)
|
||
api = NezhaAPI(user['dashboard_url'], user['username'], user['password'])
|
||
try:
|
||
results = await api.search_servers(query_text)
|
||
except Exception as e:
|
||
await update.message.reply_text(f"搜索失败:{e}")
|
||
await api.close()
|
||
return ConversationHandler.END
|
||
|
||
if not results:
|
||
await update.message.reply_text("未找到匹配的服务器。")
|
||
await api.close()
|
||
return ConversationHandler.END
|
||
|
||
keyboard = [
|
||
[InlineKeyboardButton(s['name'], callback_data=f"server_detail_{s['id']}")]
|
||
for s in results
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await update.message.reply_text("请选择服务器:", reply_markup=reply_markup)
|
||
await api.close()
|
||
return ConversationHandler.END
|
||
|
||
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
query = update.callback_query
|
||
data = query.data
|
||
|
||
user = await db.get_user(query.from_user.id)
|
||
if not user:
|
||
await query.answer("请先使用 /bind 命令绑定您的账号。", show_alert=True)
|
||
return
|
||
|
||
# 实现刷新频率限制
|
||
last_refresh_time = context.user_data.get('last_refresh_time', 0)
|
||
current_time = time.time()
|
||
if data.startswith('refresh_'):
|
||
if current_time - last_refresh_time < 1:
|
||
await query.answer("刷新太频繁,请稍后再试。", show_alert=True)
|
||
return
|
||
else:
|
||
context.user_data['last_refresh_time'] = current_time
|
||
|
||
await query.answer()
|
||
|
||
api = NezhaAPI(user['dashboard_url'], user['username'], user['password'])
|
||
|
||
if data.startswith('server_detail_'):
|
||
server_id = int(data.split('_')[-1])
|
||
try:
|
||
server = await api.get_server_detail(server_id)
|
||
except Exception as e:
|
||
await query.edit_message_text(f"获取服务器详情失败:{e}")
|
||
await api.close()
|
||
return
|
||
|
||
await api.close()
|
||
|
||
if not server:
|
||
await query.edit_message_text("未找到该服务器。")
|
||
return
|
||
|
||
name = server.get('name', '未知')
|
||
online_status = is_online(server)
|
||
status = "❇️在线" if online_status else "❌离线"
|
||
ipv4 = server.get('geoip', {}).get('ip', {}).get('ipv4_addr', '未知')
|
||
ipv6 = server.get('geoip', {}).get('ip', {}).get('ipv6_addr', '❌')
|
||
|
||
# 对 IP 地址进行掩码处理
|
||
ipv4 = mask_ipv4(ipv4)
|
||
ipv6 = mask_ipv6(ipv6)
|
||
|
||
platform = server.get('host', {}).get('platform', '未知')
|
||
cpu_info = ', '.join(server.get('host', {}).get('cpu', [])) if server.get('host') else '未知'
|
||
uptime_seconds = server.get('state', {}).get('uptime', 0)
|
||
uptime_days = uptime_seconds // 86400
|
||
uptime_hours = (uptime_seconds % 86400) // 3600
|
||
load_1 = server.get('state', {}).get('load_1', 0)
|
||
load_5 = server.get('state', {}).get('load_5', 0)
|
||
load_15 = server.get('state', {}).get('load_15', 0)
|
||
cpu_usage = server.get('state', {}).get('cpu', 0)
|
||
mem_used = server.get('state', {}).get('mem_used', 0)
|
||
mem_total = server.get('host', {}).get('mem_total', 1)
|
||
swap_used = server.get('state', {}).get('swap_used', 0)
|
||
swap_total = server.get('host', {}).get('swap_total', 1)
|
||
disk_used = server.get('state', {}).get('disk_used', 0)
|
||
disk_total = server.get('host', {}).get('disk_total', 1)
|
||
net_in_transfer = server.get('state', {}).get('net_in_transfer', 0)
|
||
net_out_transfer = server.get('state', {}).get('net_out_transfer', 0)
|
||
net_in_speed = server.get('state', {}).get('net_in_speed', 0)
|
||
net_out_speed = server.get('state', {}).get('net_out_speed', 0)
|
||
arch = server.get('host', {}).get('arch', '')
|
||
|
||
response = f"""**{name}** {status}
|
||
==========================
|
||
**ID**: {server.get('id', '未知')}
|
||
**IPv4**: {ipv4}
|
||
**IPv6**: {ipv6}
|
||
**平台**: {platform}
|
||
**CPU 信息**: {cpu_info}
|
||
**运行时间**: {uptime_days} 天 {uptime_hours} 小时
|
||
**负载**: {load_1:.2f} {load_5:.2f} {load_15:.2f}
|
||
**CPU**: {cpu_usage:.2f}% [{arch}]
|
||
**内存**: {mem_used / mem_total * 100 if mem_total else 0:.1f}% [{format_bytes(mem_used)}/{format_bytes(mem_total)}]
|
||
**交换**: {swap_used / swap_total * 100 if swap_total else 0:.1f}% [{format_bytes(swap_used)}/{format_bytes(swap_total)}]
|
||
**磁盘**: {disk_used / disk_total * 100 if disk_total else 0:.1f}% [{format_bytes(disk_used)}/{format_bytes(disk_total)}]
|
||
**流量**: ↓{format_bytes(net_in_transfer)} ↑{format_bytes(net_out_transfer)}
|
||
**网速**: ↓{format_bytes(net_in_speed)}/s ↑{format_bytes(net_out_speed)}/s
|
||
|
||
**更新于**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC
|
||
"""
|
||
# 添加刷新按钮
|
||
keyboard = [[InlineKeyboardButton("刷新", callback_data=f"refresh_server_{server_id}")]]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(response, parse_mode='Markdown', reply_markup=reply_markup)
|
||
|
||
elif data.startswith('refresh_server_'):
|
||
server_id = int(data.split('_')[-1])
|
||
# 重新获取服务器详情,与上面相同的代码
|
||
try:
|
||
server = await api.get_server_detail(server_id)
|
||
except Exception as e:
|
||
await query.edit_message_text(f"获取服务器详情失败:{e}")
|
||
await api.close()
|
||
return
|
||
|
||
await api.close()
|
||
|
||
if not server:
|
||
await query.edit_message_text("未找到该服务器。")
|
||
return
|
||
|
||
# 同上,构建响应和刷新按钮
|
||
name = server.get('name', '未知')
|
||
online_status = is_online(server)
|
||
status = "❇️在线" if online_status else "❌离线"
|
||
ipv4 = server.get('geoip', {}).get('ip', {}).get('ipv4_addr', '未知')
|
||
ipv6 = server.get('geoip', {}).get('ip', {}).get('ipv6_addr', '❌')
|
||
|
||
# 对 IP 地址进行掩码处理
|
||
ipv4 = mask_ipv4(ipv4)
|
||
ipv6 = mask_ipv6(ipv6)
|
||
|
||
platform = server.get('host', {}).get('platform', '未知')
|
||
cpu_info = ', '.join(server.get('host', {}).get('cpu', [])) if server.get('host') else '未知'
|
||
uptime_seconds = server.get('state', {}).get('uptime', 0)
|
||
uptime_days = uptime_seconds // 86400
|
||
uptime_hours = (uptime_seconds % 86400) // 3600
|
||
load_1 = server.get('state', {}).get('load_1', 0)
|
||
load_5 = server.get('state', {}).get('load_5', 0)
|
||
load_15 = server.get('state', {}).get('load_15', 0)
|
||
cpu_usage = server.get('state', {}).get('cpu', 0)
|
||
mem_used = server.get('state', {}).get('mem_used', 0)
|
||
mem_total = server.get('host', {}).get('mem_total', 1)
|
||
swap_used = server.get('state', {}).get('swap_used', 0)
|
||
swap_total = server.get('host', {}).get('swap_total', 1)
|
||
disk_used = server.get('state', {}).get('disk_used', 0)
|
||
disk_total = server.get('host', {}).get('disk_total', 1)
|
||
net_in_transfer = server.get('state', {}).get('net_in_transfer', 0)
|
||
net_out_transfer = server.get('state', {}).get('net_out_transfer', 0)
|
||
net_in_speed = server.get('state', {}).get('net_in_speed', 0)
|
||
net_out_speed = server.get('state', {}).get('net_out_speed', 0)
|
||
arch = server.get('host', {}).get('arch', '')
|
||
|
||
response = f"""**{name}** {status}
|
||
==========================
|
||
**ID**: {server.get('id', '未知')}
|
||
**IPv4**: {ipv4}
|
||
**IPv6**: {ipv6}
|
||
**平台**: {platform}
|
||
**CPU 信息**: {cpu_info}
|
||
**运行时间**: {uptime_days} 天 {uptime_hours} 小时
|
||
**负载**: {load_1:.2f} {load_5:.2f} {load_15:.2f}
|
||
**CPU**: {cpu_usage:.2f}% [{arch}]
|
||
**内存**: {mem_used / mem_total * 100 if mem_total else 0:.1f}% [{format_bytes(mem_used)}/{format_bytes(mem_total)}]
|
||
**交换**: {swap_used / swap_total * 100 if swap_total else 0:.1f}% [{format_bytes(swap_used)}/{format_bytes(swap_total)}]
|
||
**磁盘**: {disk_used / disk_total * 100 if disk_total else 0:.1f}% [{format_bytes(disk_used)}/{format_bytes(disk_total)}]
|
||
**流量**: ↓{format_bytes(net_in_transfer)} ↑{format_bytes(net_out_transfer)}
|
||
**网速**: ↓{format_bytes(net_in_speed)}/s ↑{format_bytes(net_out_speed)}/s
|
||
|
||
**更新于**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC
|
||
"""
|
||
keyboard = [[InlineKeyboardButton("刷新", callback_data=f"refresh_server_{server_id}")]]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(response, parse_mode='Markdown', reply_markup=reply_markup)
|
||
|
||
elif data == 'refresh_overview':
|
||
# 重新获取概览数据,与 overview 函数类似
|
||
try:
|
||
data = await api.get_overview()
|
||
except Exception as e:
|
||
await query.edit_message_text(f"获取数据失败:{e}")
|
||
await api.close()
|
||
return
|
||
|
||
if data and data.get('success'):
|
||
servers = data['data']
|
||
total_servers = len(servers)
|
||
online_servers = sum(1 for s in servers if is_online(s))
|
||
total_mem = sum(s['host'].get('mem_total', 0) for s in servers if s.get('host'))
|
||
used_mem = sum(s['state'].get('mem_used', 0) for s in servers if s.get('state'))
|
||
total_swap = sum(s['host'].get('swap_total', 0) for s in servers if s.get('host'))
|
||
used_swap = sum(s['state'].get('swap_used', 0) for s in servers if s.get('state'))
|
||
total_disk = sum(s['host'].get('disk_total', 0) for s in servers if s.get('host'))
|
||
used_disk = sum(s['state'].get('disk_used', 0) for s in servers if s.get('state'))
|
||
net_in_speed = sum(s['state'].get('net_in_speed', 0) for s in servers if s.get('state'))
|
||
net_out_speed = sum(s['state'].get('net_out_speed', 0) for s in servers if s.get('state'))
|
||
net_in_transfer = sum(s['state'].get('net_in_transfer', 0) for s in servers if s.get('state'))
|
||
net_out_transfer = sum(s['state'].get('net_out_transfer', 0) for s in servers if s.get('state'))
|
||
transfer_ratio = (net_out_transfer / net_in_transfer * 100) if net_in_transfer else 0
|
||
|
||
response = f"""📊 **统计信息**
|
||
===========================
|
||
**服务器数量**: {total_servers}
|
||
**在线服务器**: {online_servers}
|
||
**内存**: {used_mem / total_mem * 100 if total_mem else 0:.1f}% [{format_bytes(used_mem)}/{format_bytes(total_mem)}]
|
||
**交换**: {used_swap / total_swap * 100 if total_swap else 0:.1f}% [{format_bytes(used_swap)}/{format_bytes(total_swap)}]
|
||
**磁盘**: {used_disk / total_disk * 100 if total_disk else 0:.1f}% [{format_bytes(used_disk)}/{format_bytes(total_disk)}]
|
||
**下行速度**: ↓{format_bytes(net_in_speed)}/s
|
||
**上行速度**: ↑{format_bytes(net_out_speed)}/s
|
||
**下行流量**: ↓{format_bytes(net_in_transfer)}
|
||
**上行流量**: ↑{format_bytes(net_out_transfer)}
|
||
**流量对等性**: {transfer_ratio:.1f}%
|
||
|
||
**更新于**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC
|
||
"""
|
||
keyboard = [[InlineKeyboardButton("刷新", callback_data="refresh_overview")]]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(response, parse_mode='Markdown', reply_markup=reply_markup)
|
||
else:
|
||
await query.edit_message_text("获取服务器信息失败。")
|
||
await api.close()
|
||
|
||
elif data.startswith('cron_job_'):
|
||
cron_id = int(data.split('_')[-1])
|
||
keyboard = [
|
||
[InlineKeyboardButton("确认执行", callback_data=f"confirm_cron_{cron_id}")],
|
||
[InlineKeyboardButton("取消", callback_data="cancel")]
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text("您确定要执行此计划任务吗?", reply_markup=reply_markup)
|
||
|
||
elif data.startswith('confirm_cron_'):
|
||
cron_id = int(data.split('_')[-1])
|
||
try:
|
||
result = await api.run_cron_job(cron_id)
|
||
except Exception as e:
|
||
await query.edit_message_text(f"执行失败:{e}")
|
||
await api.close()
|
||
return
|
||
|
||
await api.close()
|
||
|
||
if result and result.get('success'):
|
||
await query.edit_message_text("计划任务已执行。")
|
||
else:
|
||
await query.edit_message_text("执行失败。")
|
||
|
||
elif data == 'cancel':
|
||
await query.edit_message_text("操作已取消。")
|
||
|
||
elif data == 'view_loop_traffic':
|
||
await view_loop_traffic(query, context, api)
|
||
|
||
elif data == 'refresh_loop_traffic':
|
||
await view_loop_traffic(query, context, api)
|
||
|
||
elif data == 'view_availability':
|
||
await view_availability(query, context, api)
|
||
|
||
elif data == 'refresh_availability':
|
||
await view_availability(query, context, api)
|
||
|
||
async def view_loop_traffic(query, context, api):
|
||
# 获取服务状态
|
||
try:
|
||
services_data = await api.get_services_status()
|
||
except Exception as e:
|
||
await query.edit_message_text(f"获取服务信息失败:{e}")
|
||
await api.close()
|
||
return
|
||
|
||
if services_data and services_data.get('success'):
|
||
cycle_stats = services_data['data'].get('cycle_transfer_stats', {})
|
||
if not cycle_stats:
|
||
await query.edit_message_text("暂无循环流量信息。")
|
||
await api.close()
|
||
return
|
||
|
||
response = "**循环流量信息总览**\n==========================\n"
|
||
for stat_name, stats in cycle_stats.items():
|
||
rule_name = stats.get('name', '未知规则')
|
||
server_names = stats.get('server_name', {})
|
||
transfers = stats.get('transfer', {})
|
||
max_transfer = stats.get('max', 1) # 最大流量(字节)
|
||
|
||
response += f"**规则:{rule_name}**\n"
|
||
for server_id_str, transfer_value in transfers.items():
|
||
server_id = str(server_id_str)
|
||
server_name = server_names.get(server_id, f"服务器ID {server_id}")
|
||
transfer_formatted = format_bytes(transfer_value)
|
||
max_transfer_formatted = format_bytes(max_transfer)
|
||
percentage = (transfer_value / max_transfer * 100) if max_transfer else 0
|
||
response += f"服务器 **{server_name}**:已使用 {transfer_formatted} / {max_transfer_formatted},已使用 {percentage:.2f}%\n"
|
||
response += "--------------------------\n"
|
||
|
||
response += f"**更新于**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC"
|
||
|
||
# 添加刷新按钮
|
||
keyboard = [[InlineKeyboardButton("刷新", callback_data="refresh_loop_traffic")]]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(response, parse_mode='Markdown', reply_markup=reply_markup)
|
||
else:
|
||
await query.edit_message_text("获取循环流量信息失败。")
|
||
await api.close()
|
||
|
||
async def view_availability(query, context, api):
|
||
# 获取服务状态
|
||
try:
|
||
services_data = await api.get_services_status()
|
||
except Exception as e:
|
||
await query.edit_message_text(f"获取服务信息失败:{e}")
|
||
await api.close()
|
||
return
|
||
# print("返回的服务数据:", services_data)
|
||
|
||
if services_data and services_data.get('success'):
|
||
services = services_data['data'].get('services', {})
|
||
if not services:
|
||
await query.edit_message_text("暂无可用性监测信息。")
|
||
await api.close()
|
||
return
|
||
|
||
response = "**可用性监测信息总览**\n==========================\n"
|
||
for service_id, service_info in services.items():
|
||
service = service_info.get('service', {})
|
||
name = service_info.get('service_name', '未知')
|
||
total_up = service_info.get('total_up', 0)
|
||
total_down = service_info.get('total_down', 0)
|
||
total = total_up + total_down
|
||
availability = (total_up / total * 100) if total else 0
|
||
status = "🟢 UP" if service_info.get('current_up', 0) else "🔴 DOWN"
|
||
# 计算平均延迟
|
||
delays = service_info.get('delay', [])
|
||
if delays:
|
||
avg_delay = sum(delays) / len(delays)
|
||
else:
|
||
avg_delay = None
|
||
if avg_delay is not None:
|
||
delay_text = f",平均延迟 {avg_delay:.2f}ms"
|
||
else:
|
||
delay_text = ""
|
||
response += f"**{name}**:可用率 {availability:.2f}%,状态 {status}{delay_text}\n------------------\n"
|
||
response += f"\n**更新于**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC"
|
||
|
||
# 添加刷新按钮
|
||
keyboard = [[InlineKeyboardButton("刷新", callback_data="refresh_availability")]]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(response, parse_mode='Markdown', reply_markup=reply_markup)
|
||
else:
|
||
await query.edit_message_text("获取可用性监测信息失败。")
|
||
await api.close()
|
||
|
||
async def cron_jobs(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user = await db.get_user(update.effective_user.id)
|
||
if not user:
|
||
await update.message.reply_text("请先使用 /bind 命令绑定您的账号。")
|
||
return
|
||
|
||
api = NezhaAPI(user['dashboard_url'], user['username'], user['password'])
|
||
try:
|
||
data = await api.get_cron_jobs()
|
||
except Exception as e:
|
||
await update.message.reply_text(f"获取计划任务失败:{e}")
|
||
await api.close()
|
||
return
|
||
|
||
if data and data.get('success'):
|
||
cron_jobs = data['data']
|
||
if not cron_jobs:
|
||
await update.message.reply_text("暂无计划任务。")
|
||
await api.close()
|
||
return
|
||
|
||
keyboard = [
|
||
[InlineKeyboardButton(job['name'], callback_data=f"cron_job_{job['id']}")]
|
||
for job in cron_jobs
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await update.message.reply_text("请选择要执行的计划任务:", reply_markup=reply_markup)
|
||
else:
|
||
await update.message.reply_text("获取计划任务失败。")
|
||
await api.close()
|
||
|
||
async def services_overview(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user = await db.get_user(update.effective_user.id)
|
||
if not user:
|
||
await update.message.reply_text("请先使用 /bind 命令绑定您的账号。")
|
||
return
|
||
|
||
keyboard = [
|
||
[InlineKeyboardButton("查看循环流量信息", callback_data="view_loop_traffic")],
|
||
[InlineKeyboardButton("查看可用性监测信息", callback_data="view_availability")]
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await update.message.reply_text("请选择要查看的服务信息:", reply_markup=reply_markup)
|
||
|
||
def main():
|
||
application = ApplicationBuilder().token(TELEGRAM_TOKEN).build()
|
||
|
||
# 初始化数据库
|
||
loop = asyncio.get_event_loop()
|
||
loop.run_until_complete(db.initialize())
|
||
|
||
# 回调查询处理(放在最前面)
|
||
application.add_handler(CallbackQueryHandler(button_handler))
|
||
|
||
# 命令处理
|
||
application.add_handler(CommandHandler('start', start))
|
||
application.add_handler(CommandHandler('help', help_command))
|
||
application.add_handler(CommandHandler('unbind', unbind))
|
||
application.add_handler(CommandHandler('overview', overview))
|
||
application.add_handler(CommandHandler('cron', cron_jobs))
|
||
application.add_handler(CommandHandler('services', services_overview))
|
||
|
||
# 绑定命令的会话处理
|
||
bind_handler = ConversationHandler(
|
||
entry_points=[CommandHandler('bind', bind_start)],
|
||
states={
|
||
BIND_USERNAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, bind_username)],
|
||
BIND_PASSWORD: [MessageHandler(filters.TEXT & ~filters.COMMAND, bind_password)],
|
||
BIND_DASHBOARD: [MessageHandler(filters.TEXT & ~filters.COMMAND, bind_dashboard)],
|
||
},
|
||
fallbacks=[]
|
||
)
|
||
application.add_handler(bind_handler)
|
||
|
||
# 查看单台服务器状态的会话处理
|
||
server_handler = ConversationHandler(
|
||
entry_points=[CommandHandler('server', server_status)],
|
||
states={
|
||
SEARCH_SERVER: [MessageHandler(filters.TEXT & ~filters.COMMAND, search_server)],
|
||
},
|
||
fallbacks=[]
|
||
)
|
||
application.add_handler(server_handler)
|
||
|
||
# 在 run_polling 中指定 allowed_updates
|
||
application.run_polling(allowed_updates=['message', 'callback_query'])
|
||
|
||
if __name__ == '__main__':
|
||
main()
|