first commit
This commit is contained in:
commit
df8078b380
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# .gitignore
|
||||
.env
|
||||
users.db
|
||||
.DS_Store
|
201
LICENCE
Normal file
201
LICENCE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020 naiba
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
123
README.md
Executable file
123
README.md
Executable file
@ -0,0 +1,123 @@
|
||||
**Nezha Telegram Bot - NextGen V1** 是一个基于 Nezha 监控 API,用于监控服务器状态的 Telegram 机器人。通过简单的命令,您可以实时查看服务器的运行状态、资源使用情况、执行计划任务以及监控服务可用性。
|
||||
|
||||
## 📋 特性
|
||||
|
||||
- **账号绑定**:安全绑定您的 Nezha 账户,确保数据隐私。
|
||||
- **服务器概览**:查看所有服务器的在线状态、内存使用、交换空间、磁盘使用、网络流量等统计信息。
|
||||
- **单台服务器状态**:获取单个服务器的详细状态信息,包括负载、CPU 使用率、内存、磁盘、网络流量等。
|
||||
- **计划任务管理**:查看并执行预设的计划任务,自动化管理服务器。
|
||||
- **服务可用性监测**:监控服务的可用性和平均延迟,确保服务稳定运行。
|
||||
- **数据刷新**:实时刷新数据,确保您获取到最新的服务器状态。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 📦 前提条件
|
||||
|
||||
在开始之前,请确保您已经具备以下条件:
|
||||
|
||||
- Python 3.7 或更高版本
|
||||
- Telegram 账号
|
||||
- 已安装哪吒监控 Dashboard 并完成配置
|
||||
- Telegram 机器人 Token(通过 [BotFather](https://t.me/BotFather) 获取)
|
||||
|
||||
### 🔧 安装步骤
|
||||
|
||||
1. **克隆仓库**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/nezha-telegram-bot.git
|
||||
cd nezha-telegram-bot
|
||||
```
|
||||
|
||||
2. **创建虚拟环境**
|
||||
|
||||
推荐使用 `venv` 创建虚拟环境:
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # 对于 Windows 用户使用 venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. **安装依赖**
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. **配置环境变量**
|
||||
|
||||
为了安全起见,建议使用环境变量存储敏感信息。创建一个 `.env` 文件并添加以下内容:
|
||||
|
||||
```env
|
||||
TELEGRAM_TOKEN=your_telegram_bot_token
|
||||
```
|
||||
|
||||
5. **初始化数据库**
|
||||
|
||||
数据库会在首次运行时自动创建。
|
||||
|
||||
6. **运行机器人**
|
||||
|
||||
```bash
|
||||
python bot.py
|
||||
```
|
||||
|
||||
您应该会看到类似以下的日志输出,表示机器人已成功启动:
|
||||
|
||||
```
|
||||
2024-12-08 17:50:39,139 - telegram.ext.Application - INFO - Application started
|
||||
```
|
||||
|
||||
## 🛠️ 使用指南
|
||||
|
||||
### 📌 绑定账号
|
||||
|
||||
为了确保您的数据安全,绑定账号仅支持私聊中进行操作。如果在群组中尝试绑定,机器人会提示您需在私聊中执行。
|
||||
|
||||
1. **私聊中发送 `/bind` 命令**。
|
||||
|
||||
2. **按照提示依次输入**:
|
||||
- 用户名
|
||||
- 密码
|
||||
- Dashboard 地址(例如:https://dashboard.example.com)
|
||||
|
||||
3. **绑定成功后**,您可以开始使用机器人的各项功能。
|
||||
|
||||
### 📜 可用命令
|
||||
|
||||
- `/start` - 启动机器人并显示欢迎信息。
|
||||
- `/help` - 获取可用命令列表和简要说明。
|
||||
- `/bind` - 绑定您的 Nezha 账户。
|
||||
- `/unbind` - 解绑您的 Nezha 账户。
|
||||
- `/overview` - 查看所有服务器的状态总览。
|
||||
- `/server` - 查看单台服务器的详细状态。
|
||||
- `/cron` - 执行计划任务。
|
||||
- `/services` - 查看服务状态总览。
|
||||
|
||||
### 📊 服务器概览
|
||||
|
||||
使用 `/overview` 命令,可以查看所有绑定服务器的统计信息,包括在线状态、内存使用、交换空间、磁盘使用、网络流量等。您还可以通过点击“刷新”按钮实时更新数据。
|
||||
|
||||
### 🖥️ 单台服务器状态
|
||||
|
||||
使用 `/server` 命令,输入服务器名称进行搜索,并选择相应的服务器查看详细状态信息。包括负载、CPU 使用率、内存、磁盘、网络流量等数据。
|
||||
|
||||
### ⏰ 计划任务管理
|
||||
|
||||
使用 `/cron` 命令,可以查看并执行预设的计划任务。点击相应任务名称进行确认执行或取消操作。
|
||||
|
||||
### 🌐 服务可用性监测
|
||||
|
||||
使用 `/services` 命令,可以查看服务的可用性信息,包括可用率、当前状态、平均延迟和剩余流量等。
|
||||
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
- [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) - 用于 Telegram 机器人的开发。
|
||||
- [aiohttp](https://github.com/aio-libs/aiohttp) - 异步 HTTP 客户端/服务器框架。
|
||||
- [aiosqlite](https://github.com/jreese/aiosqlite) - 异步 SQLite 连接库。
|
||||
- [哪吒监控](https://nezha.wiki) - 哪吒服务器监控。
|
||||
- [ChatGPT](https://chat.openai.com) - 本项目采用“面向 ChatGPT 编程”的理念,完成了包括本文档在内的 90% 的代码。
|
||||
---
|
||||
|
||||
**免责声明**:使用本机器人时,请确保遵守相关法律法规。开发者不对因使用本机器人导致的任何损失承担责任。
|
672
bot.py
Executable file
672
bot.py
Executable file
@ -0,0 +1,672 @@
|
||||
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()
|
41
database.py
Executable file
41
database.py
Executable file
@ -0,0 +1,41 @@
|
||||
# database.py
|
||||
|
||||
import aiosqlite
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path):
|
||||
self.db_path = db_path
|
||||
|
||||
async def initialize(self):
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
telegram_id INTEGER PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
dashboard_url TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
await db.commit()
|
||||
|
||||
async def add_user(self, telegram_id, username, password, dashboard_url):
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute('''
|
||||
INSERT OR REPLACE INTO users (telegram_id, username, password, dashboard_url)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (telegram_id, username, password, dashboard_url))
|
||||
await db.commit()
|
||||
|
||||
async def get_user(self, telegram_id):
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute('SELECT username, password, dashboard_url FROM users WHERE telegram_id = ?', (telegram_id,)) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
return {'username': row[0], 'password': row[1], 'dashboard_url': row[2]}
|
||||
else:
|
||||
return None
|
||||
|
||||
async def delete_user(self, telegram_id):
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute('DELETE FROM users WHERE telegram_id = ?', (telegram_id,))
|
||||
await db.commit()
|
100
nezha_api.py
Executable file
100
nezha_api.py
Executable file
@ -0,0 +1,100 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
class NezhaAPI:
|
||||
def __init__(self, dashboard_url, username, password):
|
||||
self.base_url = dashboard_url.rstrip('/') + '/api/v1'
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.token = None
|
||||
self.session = aiohttp.ClientSession()
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
async def close(self):
|
||||
await self.session.close()
|
||||
|
||||
async def authenticate(self):
|
||||
async with self.lock:
|
||||
if self.token is not None:
|
||||
return
|
||||
login_url = f'{self.base_url}/login'
|
||||
payload = {
|
||||
'username': self.username,
|
||||
'password': self.password
|
||||
}
|
||||
async with self.session.post(login_url, json=payload) as resp:
|
||||
data = await resp.json()
|
||||
if data.get('success'):
|
||||
self.token = data['data']['token']
|
||||
else:
|
||||
raise Exception('认证失败,请检查用户名和密码。')
|
||||
|
||||
async def request(self, method, endpoint, **kwargs):
|
||||
await self.authenticate()
|
||||
url = f'{self.base_url}{endpoint}'
|
||||
headers = kwargs.get('headers', {})
|
||||
headers['Authorization'] = f'Bearer {self.token}'
|
||||
kwargs['headers'] = headers
|
||||
|
||||
async with self.session.request(method, url, **kwargs) as resp:
|
||||
if resp.status == 401:
|
||||
self.token = None
|
||||
return await self.request(method, endpoint, **kwargs)
|
||||
elif resp.status == 200:
|
||||
return await resp.json()
|
||||
else:
|
||||
logging.error(f'API 请求失败:{resp.status}')
|
||||
return None
|
||||
|
||||
async def get_overview(self):
|
||||
data = await self.request('GET', '/server')
|
||||
return data
|
||||
|
||||
async def get_services(self):
|
||||
data = await self.request('GET', '/service')
|
||||
return data
|
||||
|
||||
async def get_servers(self):
|
||||
data = await self.request('GET', '/server')
|
||||
return data
|
||||
|
||||
async def get_cron_jobs(self):
|
||||
data = await self.request('GET', '/cron')
|
||||
return data
|
||||
|
||||
async def run_cron_job(self, cron_id):
|
||||
endpoint = f'/cron/{cron_id}/manual'
|
||||
data = await self.request('GET', endpoint)
|
||||
return data
|
||||
|
||||
async def search_servers(self, query):
|
||||
servers = await self.get_servers()
|
||||
if servers and servers.get('success'):
|
||||
result = []
|
||||
for server in servers['data']:
|
||||
if query.lower() in server['name'].lower():
|
||||
result.append(server)
|
||||
return result
|
||||
return []
|
||||
|
||||
async def get_server_detail(self, server_id):
|
||||
servers = await self.get_servers()
|
||||
if servers and servers.get('success'):
|
||||
for server in servers['data']:
|
||||
if server['id'] == server_id:
|
||||
return server
|
||||
return None
|
||||
|
||||
async def get_services_status(self):
|
||||
data = await self.request('GET', '/service')
|
||||
return data
|
||||
|
||||
async def get_service_histories(self, server_id):
|
||||
endpoint = f'/service/{server_id}'
|
||||
data = await self.request('GET', endpoint)
|
||||
return data
|
||||
|
||||
async def get_alert_rules(self):
|
||||
data = await self.request('GET', '/alert-rule')
|
||||
return data
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
python-telegram-bot==20.3
|
||||
aiohttp==3.8.1
|
||||
aiosqlite==0.19.0
|
||||
python-dateutil==2.8.2
|
||||
httpx==0.24.0
|
||||
python-dotenv==0.21.0
|
Loading…
Reference in New Issue
Block a user