version 1.3: inline-кнопки в боте, исправлены вкладки и команды

- bot.py: полная переработка — inline-клавиатура вместо текстовых команд (/users, /create, /delete, /warp, /stats, /dashboard, /help через кнопки)
- bot.py: исправлен /create (secrets/string теперь в top-level импорте), escape_md для MarkdownV2
- templates/index.html: добавлен пропавший </div> в секции мониторинга (ломало все вкладки кроме дашборда)
- README: обновлена дорожная карта
This commit is contained in:
2026-06-27 00:27:47 +03:00
parent b0e3f83516
commit 0ea4b0d69e
3 changed files with 229 additions and 150 deletions
+3 -2
View File
@@ -92,8 +92,9 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s
- [x] Защита сессий: динамический Secure cookie, in-memory rate limiter - [x] Защита сессий: динамический Secure cookie, in-memory rate limiter
- [x] Bootstrap-установщик (одна команда) - [x] Bootstrap-установщик (одна команда)
- [x] Мониторинг сервера: CPU, RAM, диск на дашборде (автообновление каждые 2 сек) - [x] Мониторинг сервера: CPU, RAM, диск на дашборде (автообновление каждые 2 сек)
- [x] Telegram-бот: создание/удаление пользователей, трафик, WARP, мониторинг CPU/RAM/диск - [x] Telegram-бот: inline-кнопки, создание/удаление/WARP/дашборд/системная статистика
- [x] Установка бота через веб-панель, CLI и полный установщик - [x] Установка бота: веб-панель, CLI, полный установщик; автоопределение Telegram ID через API
- [x] Установка бота через pip в venv (обход externally-managed на новых Debian/Ubuntu)
### ⬜ Запланировано ### ⬜ Запланировано
+224 -147
View File
@@ -2,7 +2,7 @@
""" """
mita Telegram Bot — управление сервером через Telegram mita Telegram Bot — управление сервером через Telegram
""" """
import os, json, re, subprocess, psutil, logging import os, json, re, subprocess, secrets, string, psutil, logging
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -15,8 +15,8 @@ MITA_CONFIG = "/etc/mita/server_config.json"
PANEL_CONFIG = "/etc/mita/panel.json" PANEL_CONFIG = "/etc/mita/panel.json"
try: try:
from telegram import Update from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, ContextTypes from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
except ImportError: except ImportError:
_log.error("python-telegram-bot не установлен. pip install python-telegram-bot") _log.error("python-telegram-bot не установлен. pip install python-telegram-bot")
raise SystemExit(1) raise SystemExit(1)
@@ -82,6 +82,18 @@ def _mita_running():
except Exception: except Exception:
return False return False
def gen_password(length=64):
chars = string.ascii_letters + string.digits + "!@#%^*_-=+?."
return "".join(secrets.choice(chars) for _ in range(length))
def gen_username():
adj = ["swift","brave","quiet","cool","sharp","calm","bright","dark","wild","free"]
nouns = ["fox","hawk","river","storm","ember","peak","orbit","tide","frost","spark"]
return f"{secrets.choice(adj)}_{secrets.choice(nouns)}_{secrets.randbelow(9000)+1000}"
def escape_md(text):
return str(text).replace("_","\\_").replace("*","\\*").replace("[","\\[").replace("]","\\]").replace("(","\\(").replace(")","\\)").replace("~","\\~").replace(">","\\>").replace("#","\\#").replace("+","\\+").replace("-","\\-").replace("=","\\=").replace("|","\\|").replace("{","\\{").replace("}","\\}").replace(".","\\.").replace("!","\\!")
# ── auth ────────────────────────────────────────────────────────────────────── # ── auth ──────────────────────────────────────────────────────────────────────
def admin_only(func): def admin_only(func):
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE): async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -89,83 +101,76 @@ def admin_only(func):
cfg = load_bot_config() cfg = load_bot_config()
admins = cfg.get("admin_ids", []) admins = cfg.get("admin_ids", [])
if user_id not in admins: if user_id not in admins:
await update.message.reply_text("⛔ Доступ запрещён.") if update.callback_query:
await update.callback_query.answer("Доступ запрещён")
elif update.message:
await update.message.reply_text("⛔ Доступ запрещён.")
return return
return await func(update, context) return await func(update, context)
return wrapper return wrapper
# ── commands ───────────────────────────────────────────────────────────────── # ── keyboards ─────────────────────────────────────────────────────────────────
MAIN_KEYBOARD = InlineKeyboardMarkup([
[InlineKeyboardButton("👥 Пользователи", callback_data="users"),
InlineKeyboardButton("📊 Дашборд", callback_data="dashboard")],
[InlineKeyboardButton(" Создать", callback_data="create"),
InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")],
[InlineKeyboardButton("🔄 WARP", callback_data="warp_menu"),
InlineKeyboardButton("📊 Статистика", callback_data="stats")],
[InlineKeyboardButton("️ Помощь", callback_data="help")],
])
def back_button(data="main"):
return InlineKeyboardMarkup([[InlineKeyboardButton("« Назад", callback_data=data)]])
# ── main menu ─────────────────────────────────────────────────────────────────
@admin_only @admin_only
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text( await update.message.reply_text(
"🤖 *mita Bot* готов к работе\\.\n\n" "🤖 *mita Bot*\nВыберите действие:",
"/users \\- список пользователей\n" reply_markup=MAIN_KEYBOARD,
"/create \\[имя\\] \\- создать пользователя\n"
"/delete \\<имя\\> \\- удалить пользователя\n"
"/warp \\<имя\\> on\\|off \\- управление WARP\n"
"/stats \\- статистика сервера\n"
"/dashboard \\- полный дашборд\n"
"/help \\- все команды",
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
@admin_only @admin_only
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE): async def show_main(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text( query = update.callback_query
"📋 *Команды*\n\n" await query.answer()
"/users \\- список пользователей с трафиком и статусом\n" await query.edit_message_text(
"/create \\- создать пользователя \\(случайное имя\\)\n" "🤖 *mita Bot*\nВыберите действие:",
"/create `имя` \\- создать с указанным именем\n" reply_markup=MAIN_KEYBOARD,
"/delete `имя` \\- удалить пользователя\n"
"/warp `имя` on \\- включить WARP\n"
"/warp `имя` off \\- выключить WARP\n"
"/stats \\- CPU, RAM, диск\n"
"/dashboard \\- полный обзор сервера\n"
"/config `имя` \\- клиентский конфиг",
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
# ── users ─────────────────────────────────────────────────────────────────────
@admin_only @admin_only
async def cmd_users(update: Update, context: ContextTypes.DEFAULT_TYPE): async def show_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
cfg = load_mita_config() cfg = load_mita_config()
users = cfg.get("users", []) users = cfg.get("users", [])
if not users: if not users:
await update.message.reply_text("👤 Нет пользователей.") await query.edit_message_text("👤 Нет пользователей.", reply_markup=back_button())
return return
raw = mita_cmd("get", "users") raw = mita_cmd("get", "users")
pc = load_panel_config() pc = load_panel_config()
warp_users = set(pc.get("warp_users", [])) warp_users = set(pc.get("warp_users", []))
lines = []
today = datetime.now().date() today = datetime.now().date()
data_lines = [] stats_map = {}
for line in raw.splitlines(): for line in raw.splitlines():
line = line.strip() line = line.strip()
if not line or line.upper().startswith("USER"): if not line or line.upper().startswith("USER"):
continue continue
data_lines.append(line)
stats_map = {}
for line in data_lines:
parts = line.split() parts = line.split()
if len(parts) < 8: if len(parts) < 8:
continue continue
try: try:
name = parts[0] name = parts[0]
d1_bytes = _parse_traffic(parts[2]) + _parse_traffic(parts[3])
d1_down = _parse_traffic(parts[2]) d7_bytes = _parse_traffic(parts[4]) + _parse_traffic(parts[5])
d1_up = _parse_traffic(parts[3]) d30_bytes = _parse_traffic(parts[6]) + _parse_traffic(parts[7])
d7_down = _parse_traffic(parts[4])
d7_up = _parse_traffic(parts[5])
d30_down = _parse_traffic(parts[6])
d30_up = _parse_traffic(parts[7])
d1_bytes = d1_down + d1_up
d7_bytes = d7_down + d7_up
d30_bytes = d30_down + d30_up
online = False online = False
la_raw = parts[1] la_raw = parts[1]
if la_raw and la_raw.lower() not in ("never","-","n/a","никогда"): if la_raw and la_raw.lower() not in ("never","-","n/a","никогда"):
@@ -175,7 +180,6 @@ async def cmd_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
online = True online = True
except Exception: except Exception:
pass pass
stats_map[name] = {"online": online, stats_map[name] = {"online": online,
"day_mb": round(d1_bytes/1024/1024, 2), "day_mb": round(d1_bytes/1024/1024, 2),
"week_mb": round(d7_bytes/1024/1024, 2), "week_mb": round(d7_bytes/1024/1024, 2),
@@ -183,44 +187,41 @@ async def cmd_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
except Exception: except Exception:
continue continue
lines = ["👥 *Пользователи*\n"]
for u in users: for u in users:
name = u["name"] name = u["name"]
s = stats_map.get(name, {}) s = stats_map.get(name, {})
w = "🟢" if name in warp_users else "" w = "🟢" if name in warp_users else ""
o = "🟢" if s.get("online") else "🔴" o = "🟢" if s.get("online") else "🔴"
day = f"{s.get('day_mb', 0):.1f} МБ" if s.get("day_mb") else "" day = f"{s.get('day_mb', 0):.1f} МБ" if s.get("day_mb") else ""
wk = f"{s.get('week_mb', 0):.1f} МБ" if s.get("week_mb") else ""
mon = f"{s.get('month_mb', 0):.1f} МБ" if s.get("month_mb") else "" mon = f"{s.get('month_mb', 0):.1f} МБ" if s.get("month_mb") else ""
lines.append(f"{o} `{name}` {w} \\| день: {day} \\| мес: {mon}") ename = escape_md(name)
lines.append(f"{o} {ename} {w} день: {day} нед: {wk} мес: {mon}")
await update.message.reply_text( await query.edit_message_text(
"👥 *Пользователи*\n\n" + "\n".join(lines), "\n".join(lines),
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton(" Создать", callback_data="create"),
InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")],
[InlineKeyboardButton("🔄 WARP", callback_data="warp_menu")],
[InlineKeyboardButton("« Назад", callback_data="main")],
]),
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
# ── create ────────────────────────────────────────────────────────────────────
@admin_only @admin_only
async def cmd_create(update: Update, context: ContextTypes.DEFAULT_TYPE): async def create_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
args = context.args query = update.callback_query
await query.answer()
cfg = load_mita_config() cfg = load_mita_config()
existing = {u["name"] for u in cfg.get("users", [])} existing = {u["name"] for u in cfg.get("users", [])}
name = gen_username()
while name in existing:
name = gen_username()
if args: password = gen_password()
name = args[0].strip()
else:
import secrets
adj = ["swift","brave","quiet","cool","sharp","calm","bright","dark","wild","free"]
nouns = ["fox","hawk","river","storm","ember","peak","orbit","tide","frost","spark"]
name = f"{secrets.choice(adj)}_{secrets.choice(nouns)}_{secrets.randbelow(9000)+1000}"
while name in existing:
name = f"{secrets.choice(adj)}_{secrets.choice(nouns)}_{secrets.randbelow(9000)+1000}"
if name in existing:
await update.message.reply_text(f"❌ Пользователь `{name}` уже существует\\.")
return
import string
chars = string.ascii_letters + string.digits + "!@#%^*_-=+?."
password = "".join(secrets.choice(chars) for _ in range(64))
cfg.setdefault("users", []).append({"name": name, "password": password}) cfg.setdefault("users", []).append({"name": name, "password": password})
Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False)) Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
@@ -231,100 +232,137 @@ async def cmd_create(update: Update, context: ContextTypes.DEFAULT_TYPE):
bindings = cfg.get("portBindings", [{}])[0] bindings = cfg.get("portBindings", [{}])[0]
port = bindings.get("portRange", str(bindings.get("port","?"))) port = bindings.get("portRange", str(bindings.get("port","?")))
await update.message.reply_text( ename = escape_md(name)
f"✅ Пользователь *{name}* создан\\.\n\n" await query.edit_message_text(
f"Сервер: `{ip}`\n" f"✅ Пользователь *{ename}* создан\n\n"
f"Порт: `{port}`\n" f"Сервер: `{ip}`\nПорт: `{port}`\nПароль: `{password}`",
f"Пароль: `{password}`\n\n" reply_markup=InlineKeyboardMarkup([
f"Получить JSON\\-конфиг: /config `{name}`", [InlineKeyboardButton(" Ещё одного", callback_data="create")],
[InlineKeyboardButton("« Назад", callback_data="main")],
]),
parse_mode="MarkdownV2"
)
# ── delete menu ───────────────────────────────────────────────────────────────
@admin_only
async def delete_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
cfg = load_mita_config()
users = cfg.get("users", [])
if not users:
await query.edit_message_text("👤 Нет пользователей.", reply_markup=back_button())
return
buttons = []
for u in users:
buttons.append([InlineKeyboardButton(f"🗑 {u['name']}", callback_data=f"delete_{u['name']}")])
buttons.append([InlineKeyboardButton("« Назад", callback_data="main")])
await query.edit_message_text(
"🗑 *Выберите пользователя для удаления:*",
reply_markup=InlineKeyboardMarkup(buttons),
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
@admin_only @admin_only
async def cmd_delete(update: Update, context: ContextTypes.DEFAULT_TYPE): async def delete_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args: query = update.callback_query
await update.message.reply_text("❌ Укажите имя: `/delete имя`", parse_mode="MarkdownV2") await query.answer()
return name = query.data[len("delete_"):]
name = context.args[0].strip()
cfg = load_mita_config() cfg = load_mita_config()
cfg["users"] = [u for u in cfg.get("users", []) if u["name"] != name] cfg["users"] = [u for u in cfg.get("users", []) if u["name"] != name]
Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False)) Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
subprocess.run(["mita", "apply", "config", MITA_CONFIG], capture_output=True) subprocess.run(["mita", "apply", "config", MITA_CONFIG], capture_output=True)
subprocess.run(["systemctl", "restart", "mita"], capture_output=True) subprocess.run(["systemctl", "restart", "mita"], capture_output=True)
await update.message.reply_text(f"🗑 Пользователь `{name}` удалён\\.", parse_mode="MarkdownV2")
await query.edit_message_text(
f"🗑 Пользователь `{escape_md(name)}` удалён",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("« Назад", callback_data="main")],
]),
parse_mode="MarkdownV2"
)
# ── warp menu ─────────────────────────────────────────────────────────────────
@admin_only @admin_only
async def cmd_warp(update: Update, context: ContextTypes.DEFAULT_TYPE): async def warp_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
if len(context.args) < 2: query = update.callback_query
await update.message.reply_text( await query.answer()
"❌ Формат: `/warp имя on` или `/warp имя off`", parse_mode="MarkdownV2") cfg = load_mita_config()
return
name = context.args[0].strip()
action = context.args[1].lower()
if action not in ("on", "off"):
await update.message.reply_text("❌ Укажите `on` или `off`\\.", parse_mode="MarkdownV2")
return
pc = load_panel_config() pc = load_panel_config()
warp_users = set(pc.get("warp_users", [])) warp_users = set(pc.get("warp_users", []))
if action == "on": users = cfg.get("users", [])
warp_users.add(name)
else: buttons = []
warp_users.discard(name) for u in users:
pc["warp_users"] = list(warp_users) on = u["name"] in warp_users
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2)) buttons.append([InlineKeyboardButton(
await update.message.reply_text( f"{'🟢' if on else ''} {u['name']}",
f"🔄 WARP для `{name}`: *{action.upper()}* \\(сохранено в panel\\.json\\)", callback_data=f"warp_{u['name']}"
)])
buttons.append([InlineKeyboardButton("« Назад", callback_data="main")])
await query.edit_message_text(
"🔄 *WARP* — нажмите на пользователя чтобы переключить:",
reply_markup=InlineKeyboardMarkup(buttons),
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
@admin_only @admin_only
async def cmd_config(update: Update, context: ContextTypes.DEFAULT_TYPE): async def toggle_warp(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args: query = update.callback_query
await update.message.reply_text("❌ Укажите имя: `/config имя`", parse_mode="MarkdownV2") await query.answer()
return name = query.data[len("warp_"):]
name = context.args[0].strip() pc = load_panel_config()
cfg = load_mita_config() warp_users = set(pc.get("warp_users", []))
user = next((u for u in cfg.get("users", []) if u["name"] == name), None) if name in warp_users:
if not user: warp_users.discard(name)
await update.message.reply_text(f"❌ Пользователь `{name}` не найден\\.", parse_mode="MarkdownV2") status = "выключен"
return else:
ip = get_server_ip() warp_users.add(name)
bindings = cfg.get("portBindings", [{}])[0] status = "включён"
port = bindings.get("portRange", str(bindings.get("port","?"))) pc["warp_users"] = list(warp_users)
data = json.dumps({ Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
"profiles": [{"profileName": "default", await query.edit_message_text(
"user": {"name": user["name"], "password": user["password"]}, f"🔄 WARP для `{escape_md(name)}`: *{status}*",
"servers": [{"ipAddress": ip, reply_markup=InlineKeyboardMarkup([
"portBindings": [{"portRange": port, [InlineKeyboardButton("« К списку WARP", callback_data="warp_menu")],
"protocol": bindings.get("protocol","TCP")}]}]}], [InlineKeyboardButton("« На главную", callback_data="main")],
"activeProfile": "default", ]),
"rpcPort": 8964, parse_mode="MarkdownV2"
"socks5Port": 1080, )
}, indent=2, ensure_ascii=False)
await update.message.reply_text(f"```json\n{data}\n```", parse_mode=None)
# ── stats ─────────────────────────────────────────────────────────────────────
@admin_only @admin_only
async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE): async def show_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
cpu = psutil.cpu_percent(interval=1) query = update.callback_query
await query.answer()
cpu = psutil.cpu_percent(interval=0.5)
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
disk = psutil.disk_usage("/") disk = psutil.disk_usage("/")
boot = datetime.fromtimestamp(psutil.boot_time()).strftime("%d.%m.%Y %H:%M") boot = datetime.fromtimestamp(psutil.boot_time()).strftime("%d.%m.%Y %H:%M")
await update.message.reply_text( await query.edit_message_text(
f"📊 *Сервер*\n\n" f"📊 *Сервер*\n\n"
f"CPU: `{cpu:.1f}%`\n" f"CPU: `{cpu:.1f}%`\n"
f"RAM: `{mem.percent:.1f}%` \\({fmt_bytes(mem.used)} / {fmt_bytes(mem.total)}\\)\n" f"RAM: `{mem.percent:.1f}%` \\({fmt_bytes(mem.used)} / {fmt_bytes(mem.total)}\\)\n"
f"Диск: `{disk.percent:.1f}%` \\({fmt_bytes(disk.used)} / {fmt_bytes(disk.total)}\\)\n" f"Диск: `{disk.percent:.1f}%` \\({fmt_bytes(disk.used)} / {fmt_bytes(disk.total)}\\)\n"
f"Аптайм: с `{boot}`\n" f"Аптайм: с `{boot}`\n"
f"mita: {'🟢 работает' if _mita_running() else '🔴 остановлен'}", f"mita: {'🟢 работает' if _mita_running() else '🔴 остановлен'}",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("🔄 Обновить", callback_data="stats")],
[InlineKeyboardButton("« Назад", callback_data="main")],
]),
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
# ── dashboard ─────────────────────────────────────────────────────────────────
@admin_only @admin_only
async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE): async def show_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
cpu = psutil.cpu_percent(interval=1) query = update.callback_query
await query.answer()
cpu = psutil.cpu_percent(interval=0.5)
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
disk = psutil.disk_usage("/") disk = psutil.disk_usage("/")
ip = get_server_ip() ip = get_server_ip()
@@ -344,7 +382,7 @@ async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
bindings = cfg.get("portBindings", [{}])[0] bindings = cfg.get("portBindings", [{}])[0]
port = bindings.get("portRange", str(bindings.get("port","?"))) port = bindings.get("portRange", str(bindings.get("port","?")))
traffic = None traffic = (0, 0)
try: try:
raw = mita_cmd("get", "users") raw = mita_cmd("get", "users")
week_total = month_total = 0.0 week_total = month_total = 0.0
@@ -361,21 +399,67 @@ async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
except Exception: except Exception:
pass pass
w, m = traffic or (0, 0) w, m = traffic
await update.message.reply_text( await query.edit_message_text(
f"📊 *Дашборд*\n\n" f"📊 *Дашборд*\n\n"
f"IP сервера: `{ip}`\n" f"IP: `{ip}`\nWARP: `{warp_ip}`\nПорт: `{port}`\n"
f"IP WARP: `{warp_ip}`\n"
f"Порт mita: `{port}`\n"
f"Пользователей: `{user_count}`\n" f"Пользователей: `{user_count}`\n"
f"Трафик нед\\.: `{w} ГБ`\n" f"Трафик нед: `{w} ГБ` мес: `{m} ГБ`\n"
f"Трафик мес\\.: `{m} ГБ`\n"
f"mita: {'🟢' if mita_ok else '🔴'}\n" f"mita: {'🟢' if mita_ok else '🔴'}\n"
f"CPU: `{cpu:.1f}%` RAM: `{mem.percent:.1f}%` Диск: `{disk.percent:.1f}%`", f"CPU: `{cpu:.1f}%` RAM: `{mem.percent:.1f}%` Диск: `{disk.percent:.1f}%`",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("🔄 Обновить", callback_data="dashboard")],
[InlineKeyboardButton("« Назад", callback_data="main")],
]),
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
# ── help ──────────────────────────────────────────────────────────────────────
@admin_only
async def show_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
await query.edit_message_text(
"📋 *Команды*\n\n"
"Используйте кнопки меню:\n"
"👥 Пользователи — список с трафиком\n"
"➕ Создать — создать пользователя\n"
"🗑 Удалить — выбрать и удалить\n"
"🔄 WARP — включить/выключить\n"
"📊 Статистика — CPU, RAM, диск\n"
"📊 Дашборд — полный обзор",
reply_markup=back_button(),
parse_mode="MarkdownV2"
)
# ── callback router ───────────────────────────────────────────────────────────
@admin_only
async def router(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
data = query.data
if data == "main":
return await show_main(update, context)
elif data == "users":
return await show_users(update, context)
elif data == "create":
return await create_user(update, context)
elif data == "delete_menu":
return await delete_menu(update, context)
elif data.startswith("delete_"):
return await delete_user(update, context)
elif data == "warp_menu":
return await warp_menu(update, context)
elif data.startswith("warp_"):
return await toggle_warp(update, context)
elif data == "stats":
return await show_stats(update, context)
elif data == "dashboard":
return await show_dashboard(update, context)
elif data == "help":
return await show_help(update, context)
# ── main ────────────────────────────────────────────────────────────────────── # ── main ──────────────────────────────────────────────────────────────────────
def main(): def main():
cfg = load_bot_config() cfg = load_bot_config()
@@ -386,14 +470,7 @@ def main():
app = Application.builder().token(token).build() app = Application.builder().token(token).build()
app.add_handler(CommandHandler("start", cmd_start)) app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("help", cmd_help)) app.add_handler(CallbackQueryHandler(router))
app.add_handler(CommandHandler("users", cmd_users))
app.add_handler(CommandHandler("create", cmd_create))
app.add_handler(CommandHandler("delete", cmd_delete))
app.add_handler(CommandHandler("warp", cmd_warp))
app.add_handler(CommandHandler("config", cmd_config))
app.add_handler(CommandHandler("stats", cmd_stats))
app.add_handler(CommandHandler("dashboard", cmd_dashboard))
_log.info("Бот запущен") _log.info("Бот запущен")
app.run_polling() app.run_polling()
+2 -1
View File
@@ -52,8 +52,9 @@
<div class="stat-card"> <div class="stat-card">
<div class="stat-label">Аптайм</div> <div class="stat-label">Аптайм</div>
<div class="stat-value" style="font-size:1.1rem" id="s-uptime"></div> <div class="stat-value" style="font-size:1.1rem" id="s-uptime"></div>
</div>
</div>
</div> </div>
</div>
<div class="card"> <div class="card">
<div class="card-title">Быстрые действия</div> <div class="card-title">Быстрые действия</div>