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:
@@ -92,8 +92,9 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s
|
||||
- [x] Защита сессий: динамический Secure cookie, in-memory rate limiter
|
||||
- [x] Bootstrap-установщик (одна команда)
|
||||
- [x] Мониторинг сервера: CPU, RAM, диск на дашборде (автообновление каждые 2 сек)
|
||||
- [x] Telegram-бот: создание/удаление пользователей, трафик, WARP, мониторинг CPU/RAM/диск
|
||||
- [x] Установка бота через веб-панель, CLI и полный установщик
|
||||
- [x] Telegram-бот: inline-кнопки, создание/удаление/WARP/дашборд/системная статистика
|
||||
- [x] Установка бота: веб-панель, CLI, полный установщик; автоопределение Telegram ID через API
|
||||
- [x] Установка бота через pip в venv (обход externally-managed на новых Debian/Ubuntu)
|
||||
|
||||
### ⬜ Запланировано
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""
|
||||
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 pathlib import Path
|
||||
|
||||
@@ -15,8 +15,8 @@ MITA_CONFIG = "/etc/mita/server_config.json"
|
||||
PANEL_CONFIG = "/etc/mita/panel.json"
|
||||
|
||||
try:
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, ContextTypes
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
|
||||
except ImportError:
|
||||
_log.error("python-telegram-bot не установлен. pip install python-telegram-bot")
|
||||
raise SystemExit(1)
|
||||
@@ -82,6 +82,18 @@ def _mita_running():
|
||||
except Exception:
|
||||
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 ──────────────────────────────────────────────────────────────────────
|
||||
def admin_only(func):
|
||||
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
@@ -89,83 +101,76 @@ def admin_only(func):
|
||||
cfg = load_bot_config()
|
||||
admins = cfg.get("admin_ids", [])
|
||||
if user_id not in admins:
|
||||
if update.callback_query:
|
||||
await update.callback_query.answer("Доступ запрещён")
|
||||
elif update.message:
|
||||
await update.message.reply_text("⛔ Доступ запрещён.")
|
||||
return
|
||||
return await func(update, context)
|
||||
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
|
||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text(
|
||||
"🤖 *mita Bot* готов к работе\\.\n\n"
|
||||
"/users \\- список пользователей\n"
|
||||
"/create \\[имя\\] \\- создать пользователя\n"
|
||||
"/delete \\<имя\\> \\- удалить пользователя\n"
|
||||
"/warp \\<имя\\> on\\|off \\- управление WARP\n"
|
||||
"/stats \\- статистика сервера\n"
|
||||
"/dashboard \\- полный дашборд\n"
|
||||
"/help \\- все команды",
|
||||
"🤖 *mita Bot*\nВыберите действие:",
|
||||
reply_markup=MAIN_KEYBOARD,
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text(
|
||||
"📋 *Команды*\n\n"
|
||||
"/users \\- список пользователей с трафиком и статусом\n"
|
||||
"/create \\- создать пользователя \\(случайное имя\\)\n"
|
||||
"/create `имя` \\- создать с указанным именем\n"
|
||||
"/delete `имя` \\- удалить пользователя\n"
|
||||
"/warp `имя` on \\- включить WARP\n"
|
||||
"/warp `имя` off \\- выключить WARP\n"
|
||||
"/stats \\- CPU, RAM, диск\n"
|
||||
"/dashboard \\- полный обзор сервера\n"
|
||||
"/config `имя` \\- клиентский конфиг",
|
||||
async def show_main(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
await query.edit_message_text(
|
||||
"🤖 *mita Bot*\nВыберите действие:",
|
||||
reply_markup=MAIN_KEYBOARD,
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
# ── users ─────────────────────────────────────────────────────────────────────
|
||||
@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()
|
||||
users = cfg.get("users", [])
|
||||
if not users:
|
||||
await update.message.reply_text("👤 Нет пользователей.")
|
||||
await query.edit_message_text("👤 Нет пользователей.", reply_markup=back_button())
|
||||
return
|
||||
|
||||
raw = mita_cmd("get", "users")
|
||||
pc = load_panel_config()
|
||||
warp_users = set(pc.get("warp_users", []))
|
||||
|
||||
lines = []
|
||||
today = datetime.now().date()
|
||||
|
||||
data_lines = []
|
||||
stats_map = {}
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.upper().startswith("USER"):
|
||||
continue
|
||||
data_lines.append(line)
|
||||
|
||||
stats_map = {}
|
||||
for line in data_lines:
|
||||
parts = line.split()
|
||||
if len(parts) < 8:
|
||||
continue
|
||||
try:
|
||||
name = parts[0]
|
||||
|
||||
d1_down = _parse_traffic(parts[2])
|
||||
d1_up = _parse_traffic(parts[3])
|
||||
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
|
||||
|
||||
d1_bytes = _parse_traffic(parts[2]) + _parse_traffic(parts[3])
|
||||
d7_bytes = _parse_traffic(parts[4]) + _parse_traffic(parts[5])
|
||||
d30_bytes = _parse_traffic(parts[6]) + _parse_traffic(parts[7])
|
||||
online = False
|
||||
la_raw = parts[1]
|
||||
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
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
stats_map[name] = {"online": online,
|
||||
"day_mb": round(d1_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:
|
||||
continue
|
||||
|
||||
lines = ["👥 *Пользователи*\n"]
|
||||
for u in users:
|
||||
name = u["name"]
|
||||
s = stats_map.get(name, {})
|
||||
w = "🟢" if name in warp_users else "⚪"
|
||||
o = "🟢" if s.get("online") 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 "—"
|
||||
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(
|
||||
"👥 *Пользователи*\n\n" + "\n".join(lines),
|
||||
await query.edit_message_text(
|
||||
"\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"
|
||||
)
|
||||
|
||||
# ── create ────────────────────────────────────────────────────────────────────
|
||||
@admin_only
|
||||
async def cmd_create(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
args = context.args
|
||||
async def create_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
cfg = load_mita_config()
|
||||
existing = {u["name"] for u in cfg.get("users", [])}
|
||||
|
||||
if args:
|
||||
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}"
|
||||
name = gen_username()
|
||||
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))
|
||||
name = gen_username()
|
||||
|
||||
password = gen_password()
|
||||
cfg.setdefault("users", []).append({"name": name, "password": password})
|
||||
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]
|
||||
port = bindings.get("portRange", str(bindings.get("port","?")))
|
||||
|
||||
await update.message.reply_text(
|
||||
f"✅ Пользователь *{name}* создан\\.\n\n"
|
||||
f"Сервер: `{ip}`\n"
|
||||
f"Порт: `{port}`\n"
|
||||
f"Пароль: `{password}`\n\n"
|
||||
f"Получить JSON\\-конфиг: /config `{name}`",
|
||||
ename = escape_md(name)
|
||||
await query.edit_message_text(
|
||||
f"✅ Пользователь *{ename}* создан\n\n"
|
||||
f"Сервер: `{ip}`\nПорт: `{port}`\nПароль: `{password}`",
|
||||
reply_markup=InlineKeyboardMarkup([
|
||||
[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"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_delete(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not context.args:
|
||||
await update.message.reply_text("❌ Укажите имя: `/delete имя`", parse_mode="MarkdownV2")
|
||||
return
|
||||
name = context.args[0].strip()
|
||||
async def delete_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
name = query.data[len("delete_"):]
|
||||
cfg = load_mita_config()
|
||||
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))
|
||||
subprocess.run(["mita", "apply", "config", MITA_CONFIG], 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
|
||||
async def cmd_warp(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if len(context.args) < 2:
|
||||
await update.message.reply_text(
|
||||
"❌ Формат: `/warp имя on` или `/warp имя off`", parse_mode="MarkdownV2")
|
||||
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
|
||||
|
||||
async def warp_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
cfg = load_mita_config()
|
||||
pc = load_panel_config()
|
||||
warp_users = set(pc.get("warp_users", []))
|
||||
if action == "on":
|
||||
warp_users.add(name)
|
||||
else:
|
||||
warp_users.discard(name)
|
||||
pc["warp_users"] = list(warp_users)
|
||||
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
||||
await update.message.reply_text(
|
||||
f"🔄 WARP для `{name}`: *{action.upper()}* \\(сохранено в panel\\.json\\)",
|
||||
users = cfg.get("users", [])
|
||||
|
||||
buttons = []
|
||||
for u in users:
|
||||
on = u["name"] in warp_users
|
||||
buttons.append([InlineKeyboardButton(
|
||||
f"{'🟢' if on else '⚪'} {u['name']}",
|
||||
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"
|
||||
)
|
||||
|
||||
@admin_only
|
||||
async def cmd_config(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not context.args:
|
||||
await update.message.reply_text("❌ Укажите имя: `/config имя`", parse_mode="MarkdownV2")
|
||||
return
|
||||
name = context.args[0].strip()
|
||||
cfg = load_mita_config()
|
||||
user = next((u for u in cfg.get("users", []) if u["name"] == name), None)
|
||||
if not user:
|
||||
await update.message.reply_text(f"❌ Пользователь `{name}` не найден\\.", parse_mode="MarkdownV2")
|
||||
return
|
||||
ip = get_server_ip()
|
||||
bindings = cfg.get("portBindings", [{}])[0]
|
||||
port = bindings.get("portRange", str(bindings.get("port","?")))
|
||||
data = json.dumps({
|
||||
"profiles": [{"profileName": "default",
|
||||
"user": {"name": user["name"], "password": user["password"]},
|
||||
"servers": [{"ipAddress": ip,
|
||||
"portBindings": [{"portRange": port,
|
||||
"protocol": bindings.get("protocol","TCP")}]}]}],
|
||||
"activeProfile": "default",
|
||||
"rpcPort": 8964,
|
||||
"socks5Port": 1080,
|
||||
}, indent=2, ensure_ascii=False)
|
||||
|
||||
await update.message.reply_text(f"```json\n{data}\n```", parse_mode=None)
|
||||
async def toggle_warp(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
name = query.data[len("warp_"):]
|
||||
pc = load_panel_config()
|
||||
warp_users = set(pc.get("warp_users", []))
|
||||
if name in warp_users:
|
||||
warp_users.discard(name)
|
||||
status = "выключен"
|
||||
else:
|
||||
warp_users.add(name)
|
||||
status = "включён"
|
||||
pc["warp_users"] = list(warp_users)
|
||||
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
||||
await query.edit_message_text(
|
||||
f"🔄 WARP для `{escape_md(name)}`: *{status}*",
|
||||
reply_markup=InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("« К списку WARP", callback_data="warp_menu")],
|
||||
[InlineKeyboardButton("« На главную", callback_data="main")],
|
||||
]),
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
# ── stats ─────────────────────────────────────────────────────────────────────
|
||||
@admin_only
|
||||
async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
cpu = psutil.cpu_percent(interval=1)
|
||||
async def show_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
cpu = psutil.cpu_percent(interval=0.5)
|
||||
mem = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage("/")
|
||||
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"CPU: `{cpu:.1f}%`\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"Аптайм: с `{boot}`\n"
|
||||
f"mita: {'🟢 работает' if _mita_running() else '🔴 остановлен'}",
|
||||
reply_markup=InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("🔄 Обновить", callback_data="stats")],
|
||||
[InlineKeyboardButton("« Назад", callback_data="main")],
|
||||
]),
|
||||
parse_mode="MarkdownV2"
|
||||
)
|
||||
|
||||
# ── dashboard ─────────────────────────────────────────────────────────────────
|
||||
@admin_only
|
||||
async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
cpu = psutil.cpu_percent(interval=1)
|
||||
async def show_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
cpu = psutil.cpu_percent(interval=0.5)
|
||||
mem = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage("/")
|
||||
ip = get_server_ip()
|
||||
@@ -344,7 +382,7 @@ async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
bindings = cfg.get("portBindings", [{}])[0]
|
||||
port = bindings.get("portRange", str(bindings.get("port","?")))
|
||||
|
||||
traffic = None
|
||||
traffic = (0, 0)
|
||||
try:
|
||||
raw = mita_cmd("get", "users")
|
||||
week_total = month_total = 0.0
|
||||
@@ -361,21 +399,67 @@ async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
w, m = traffic or (0, 0)
|
||||
w, m = traffic
|
||||
|
||||
await update.message.reply_text(
|
||||
await query.edit_message_text(
|
||||
f"📊 *Дашборд*\n\n"
|
||||
f"IP сервера: `{ip}`\n"
|
||||
f"IP WARP: `{warp_ip}`\n"
|
||||
f"Порт mita: `{port}`\n"
|
||||
f"IP: `{ip}`\nWARP: `{warp_ip}`\nПорт: `{port}`\n"
|
||||
f"Пользователей: `{user_count}`\n"
|
||||
f"Трафик нед\\.: `{w} ГБ`\n"
|
||||
f"Трафик мес\\.: `{m} ГБ`\n"
|
||||
f"Трафик нед: `{w} ГБ` мес: `{m} ГБ`\n"
|
||||
f"mita: {'🟢' if mita_ok else '🔴'}\n"
|
||||
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"
|
||||
)
|
||||
|
||||
# ── 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 ──────────────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
cfg = load_bot_config()
|
||||
@@ -386,14 +470,7 @@ def main():
|
||||
|
||||
app = Application.builder().token(token).build()
|
||||
app.add_handler(CommandHandler("start", cmd_start))
|
||||
app.add_handler(CommandHandler("help", cmd_help))
|
||||
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))
|
||||
app.add_handler(CallbackQueryHandler(router))
|
||||
|
||||
_log.info("Бот запущен")
|
||||
app.run_polling()
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
<div class="stat-label">Аптайм</div>
|
||||
<div class="stat-value" style="font-size:1.1rem" id="s-uptime">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Быстрые действия</div>
|
||||
|
||||
Reference in New Issue
Block a user