2026-06-26 23:37:05 +03:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
mita Telegram Bot — управление сервером через Telegram
|
|
|
|
|
|
"""
|
2026-06-27 00:27:47 +03:00
|
|
|
|
import os, json, re, subprocess, secrets, string, psutil, logging
|
2026-06-26 23:37:05 +03:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO,
|
|
|
|
|
|
format="%(asctime)s %(levelname)s [bot] %(message)s")
|
|
|
|
|
|
_log = logging.getLogger("mita_bot")
|
|
|
|
|
|
|
|
|
|
|
|
BOT_CONFIG = os.environ.get("BOT_CONFIG", "/etc/mita/bot.json")
|
|
|
|
|
|
MITA_CONFIG = "/etc/mita/server_config.json"
|
|
|
|
|
|
PANEL_CONFIG = "/etc/mita/panel.json"
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-06-27 00:27:47 +03:00
|
|
|
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
|
|
|
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
|
2026-06-26 23:37:05 +03:00
|
|
|
|
except ImportError:
|
|
|
|
|
|
_log.error("python-telegram-bot не установлен. pip install python-telegram-bot")
|
|
|
|
|
|
raise SystemExit(1)
|
|
|
|
|
|
|
|
|
|
|
|
# ── config ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def load_bot_config():
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(Path(BOT_CONFIG).read_text())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
def load_mita_config():
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(Path(MITA_CONFIG).read_text())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
def load_panel_config():
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(Path(PANEL_CONFIG).read_text())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def mita_cmd(*args):
|
|
|
|
|
|
r = subprocess.run(["mita", *args], capture_output=True, text=True)
|
|
|
|
|
|
return r.stdout.strip()
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_traffic(s: str) -> float:
|
|
|
|
|
|
s = s.strip()
|
|
|
|
|
|
mul = {"TiB":1024**4,"GiB":1024**3,"MiB":1024**2,"KiB":1024,"B":1}
|
|
|
|
|
|
for suffix, factor in mul.items():
|
|
|
|
|
|
if s.endswith(suffix):
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(s[:-len(suffix)]) * factor
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return 0.0
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(s)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
def fmt_bytes(b):
|
|
|
|
|
|
if b >= 1024**4: return f"{b/1024**4:.2f} TiB"
|
|
|
|
|
|
if b >= 1024**3: return f"{b/1024**3:.2f} GiB"
|
|
|
|
|
|
if b >= 1024**2: return f"{b/1024**2:.1f} MiB"
|
|
|
|
|
|
if b >= 1024: return f"{b/1024:.0f} KiB"
|
|
|
|
|
|
return f"{b:.0f} B"
|
|
|
|
|
|
|
|
|
|
|
|
def get_server_ip():
|
|
|
|
|
|
try:
|
|
|
|
|
|
return subprocess.run(
|
|
|
|
|
|
["curl","-s","--max-time","5","ifconfig.me"],
|
|
|
|
|
|
capture_output=True, text=True).stdout.strip() or "?"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return "?"
|
|
|
|
|
|
|
|
|
|
|
|
def _mita_running():
|
|
|
|
|
|
try:
|
|
|
|
|
|
r = subprocess.run(["systemctl","is-active","mita"],
|
|
|
|
|
|
capture_output=True, text=True, timeout=3)
|
|
|
|
|
|
return r.stdout.strip() == "active"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-06-28 15:11:17 +03:00
|
|
|
|
def gen_password(length=64, mode="hard"):
|
|
|
|
|
|
if mode == "easy":
|
|
|
|
|
|
chars = string.ascii_letters + string.digits + "-._~*+"
|
|
|
|
|
|
else:
|
|
|
|
|
|
chars = string.ascii_letters + string.digits + "!@#%^*_-=+?."
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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("!","\\!")
|
|
|
|
|
|
|
2026-06-26 23:37:05 +03:00
|
|
|
|
# ── auth ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def admin_only(func):
|
|
|
|
|
|
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
user_id = str(update.effective_user.id)
|
|
|
|
|
|
cfg = load_bot_config()
|
|
|
|
|
|
admins = cfg.get("admin_ids", [])
|
|
|
|
|
|
if user_id not in admins:
|
2026-06-27 00:27:47 +03:00
|
|
|
|
if update.callback_query:
|
|
|
|
|
|
await update.callback_query.answer("Доступ запрещён")
|
|
|
|
|
|
elif update.message:
|
|
|
|
|
|
await update.message.reply_text("⛔ Доступ запрещён.")
|
2026-06-26 23:37:05 +03:00
|
|
|
|
return
|
|
|
|
|
|
return await func(update, context)
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
# ── 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"),
|
2026-06-27 00:37:32 +03:00
|
|
|
|
InlineKeyboardButton("🛡 Безопасность", callback_data="security")],
|
2026-06-27 00:27:47 +03:00
|
|
|
|
[InlineKeyboardButton("ℹ️ Помощь", callback_data="help")],
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
def back_button(data="main"):
|
|
|
|
|
|
return InlineKeyboardMarkup([[InlineKeyboardButton("« Назад", callback_data=data)]])
|
|
|
|
|
|
|
|
|
|
|
|
# ── main menu ─────────────────────────────────────────────────────────────────
|
2026-06-26 23:37:05 +03:00
|
|
|
|
@admin_only
|
|
|
|
|
|
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
await update.message.reply_text(
|
2026-06-27 00:27:47 +03:00
|
|
|
|
"🤖 *mita Bot*\nВыберите действие:",
|
|
|
|
|
|
reply_markup=MAIN_KEYBOARD,
|
2026-06-26 23:37:05 +03:00
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin_only
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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,
|
2026-06-26 23:37:05 +03:00
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
# ── users ─────────────────────────────────────────────────────────────────────
|
2026-06-26 23:37:05 +03:00
|
|
|
|
@admin_only
|
2026-06-27 00:27:47 +03:00
|
|
|
|
async def show_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
2026-06-29 19:25:43 +03:00
|
|
|
|
try:
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
users = cfg.get("users", [])
|
|
|
|
|
|
if not users:
|
|
|
|
|
|
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", []))
|
|
|
|
|
|
today = datetime.now().date()
|
|
|
|
|
|
|
|
|
|
|
|
stats_map = {}
|
|
|
|
|
|
for line in raw.splitlines():
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if not line or line.upper().startswith("USER"):
|
|
|
|
|
|
continue
|
|
|
|
|
|
parts = line.split()
|
|
|
|
|
|
if len(parts) < 8:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
name = parts[0]
|
|
|
|
|
|
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","никогда"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
la_date = datetime.strptime(la_raw[:10], "%Y-%m-%d").date()
|
|
|
|
|
|
if (today - la_date).days == 0:
|
|
|
|
|
|
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),
|
|
|
|
|
|
"month_mb": round(d30_bytes/1024/1024, 2)}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
lines = ["👥 *Пользователи*\n"]
|
|
|
|
|
|
user_buttons = []
|
|
|
|
|
|
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 "—"
|
|
|
|
|
|
mon = f"`{s.get('month_mb', 0):.1f} МБ`" if s.get("month_mb") else "—"
|
|
|
|
|
|
ename = escape_md(name)
|
|
|
|
|
|
lines.append(f"{o} {ename} {w} д:{day} м:{mon}")
|
|
|
|
|
|
user_buttons.append([InlineKeyboardButton(
|
|
|
|
|
|
f"{'🟢 ' if s.get('online') else ''}{name}",
|
|
|
|
|
|
callback_data=f"user_{name}"
|
|
|
|
|
|
)])
|
|
|
|
|
|
|
|
|
|
|
|
user_buttons.extend([
|
|
|
|
|
|
[InlineKeyboardButton("➕ Создать", callback_data="create"),
|
|
|
|
|
|
InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")],
|
|
|
|
|
|
[InlineKeyboardButton("🔄 WARP", callback_data="warp_menu")],
|
|
|
|
|
|
[InlineKeyboardButton("« Назад", callback_data="main")],
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
text = "\n".join(lines)
|
|
|
|
|
|
if len(text) > 4000:
|
|
|
|
|
|
text = text[:3997] + "…"
|
|
|
|
|
|
if len(user_buttons) > 96:
|
|
|
|
|
|
user_buttons = user_buttons[:96]
|
|
|
|
|
|
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup(user_buttons),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
_log.error("show_users failed: %s", e, exc_info=True)
|
2026-06-26 23:37:05 +03:00
|
|
|
|
try:
|
2026-06-29 19:25:43 +03:00
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"⚠ Ошибка загрузки пользователей:\n`{e}`",
|
|
|
|
|
|
reply_markup=back_button(),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
2026-06-26 23:37:05 +03:00
|
|
|
|
except Exception:
|
2026-06-29 19:25:43 +03:00
|
|
|
|
pass
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
_log.error("show_users failed: %s", e)
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"⚠ Ошибка: {e}",
|
|
|
|
|
|
reply_markup=back_button()
|
|
|
|
|
|
)
|
2026-06-26 23:37:05 +03:00
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
# ── create ────────────────────────────────────────────────────────────────────
|
2026-06-26 23:37:05 +03:00
|
|
|
|
@admin_only
|
2026-06-27 00:27:47 +03:00
|
|
|
|
async def create_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
2026-06-28 15:11:17 +03:00
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
"➕ *Создать пользователя*\n\nВыберите сложность пароля:",
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("🔓 Лёгкий (A-Z, 0-9, -._~*+)", callback_data="create_easy")],
|
|
|
|
|
|
[InlineKeyboardButton("🔐 Сложный (со спецсимволами)", callback_data="create_hard")],
|
|
|
|
|
|
[InlineKeyboardButton("« Назад", callback_data="main")],
|
|
|
|
|
|
]),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin_only
|
|
|
|
|
|
async def create_exec(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
pwd_mode = "easy" if query.data == "create_easy" else "hard"
|
2026-06-26 23:37:05 +03:00
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
existing = {u["name"] for u in cfg.get("users", [])}
|
2026-06-27 00:27:47 +03:00
|
|
|
|
name = gen_username()
|
|
|
|
|
|
while name in existing:
|
|
|
|
|
|
name = gen_username()
|
2026-06-26 23:37:05 +03:00
|
|
|
|
|
2026-06-28 15:11:17 +03:00
|
|
|
|
password = gen_password(mode=pwd_mode)
|
2026-06-26 23:37:05 +03:00
|
|
|
|
cfg.setdefault("users", []).append({"name": name, "password": password})
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
ip = get_server_ip()
|
|
|
|
|
|
bindings = cfg.get("portBindings", [{}])[0]
|
|
|
|
|
|
port = bindings.get("portRange", str(bindings.get("port","?")))
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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")],
|
|
|
|
|
|
]),
|
2026-06-26 23:37:05 +03:00
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
# ── delete menu ───────────────────────────────────────────────────────────────
|
2026-06-26 23:37:05 +03:00
|
|
|
|
@admin_only
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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())
|
2026-06-26 23:37:05 +03:00
|
|
|
|
return
|
2026-06-27 00:27:47 +03:00
|
|
|
|
|
|
|
|
|
|
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 delete_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
name = query.data[len("delete_"):]
|
2026-06-26 23:37:05 +03:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"🗑 Пользователь `{escape_md(name)}` удалён",
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("« Назад", callback_data="main")],
|
|
|
|
|
|
]),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── warp menu ─────────────────────────────────────────────────────────────────
|
2026-06-26 23:37:05 +03:00
|
|
|
|
@admin_only
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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", []))
|
|
|
|
|
|
users = cfg.get("users", [])
|
2026-06-26 23:37:05 +03:00
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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 toggle_warp(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
name = query.data[len("warp_"):]
|
2026-06-26 23:37:05 +03:00
|
|
|
|
pc = load_panel_config()
|
|
|
|
|
|
warp_users = set(pc.get("warp_users", []))
|
2026-06-27 00:27:47 +03:00
|
|
|
|
if name in warp_users:
|
2026-06-26 23:37:05 +03:00
|
|
|
|
warp_users.discard(name)
|
2026-06-27 00:27:47 +03:00
|
|
|
|
status = "выключен"
|
|
|
|
|
|
else:
|
|
|
|
|
|
warp_users.add(name)
|
|
|
|
|
|
status = "включён"
|
2026-06-26 23:37:05 +03:00
|
|
|
|
pc["warp_users"] = list(warp_users)
|
|
|
|
|
|
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
2026-06-28 23:59:47 +03:00
|
|
|
|
|
|
|
|
|
|
# Перестроить egress
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
warp_users_after = set(pc.get("warp_users", []))
|
|
|
|
|
|
if not warp_users_after:
|
|
|
|
|
|
cfg.pop("egress", None)
|
|
|
|
|
|
else:
|
|
|
|
|
|
wp = {"name": "warp", "protocol": "SOCKS5_PROXY_PROTOCOL",
|
|
|
|
|
|
"host": "127.0.0.1", "port": 40000}
|
|
|
|
|
|
has_full = any(pc.get("warp_rules", {}).get(u, {}).get("full_warp") for u in warp_users_after)
|
|
|
|
|
|
if has_full:
|
|
|
|
|
|
cfg["egress"] = {
|
|
|
|
|
|
"proxies": [wp],
|
|
|
|
|
|
"rules": [{"ipRanges": ["*"], "domainNames": ["*"],
|
|
|
|
|
|
"action": "PROXY", "proxyNames": ["warp"]}],
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
all_dom = set()
|
|
|
|
|
|
all_ip = set()
|
|
|
|
|
|
for uname in warp_users_after:
|
|
|
|
|
|
r = pc.get("warp_rules", {}).get(uname, {})
|
|
|
|
|
|
all_dom.update(d for d in r.get("domains", []) if d)
|
|
|
|
|
|
all_ip.update(i for i in r.get("ips", []) if i)
|
|
|
|
|
|
if all_dom or all_ip:
|
|
|
|
|
|
wr = {"action": "PROXY", "proxyNames": ["warp"]}
|
|
|
|
|
|
if all_dom: wr["domainNames"] = sorted(all_dom)
|
|
|
|
|
|
if all_ip: wr["ipRanges"] = sorted(all_ip)
|
|
|
|
|
|
cfg["egress"] = {
|
|
|
|
|
|
"proxies": [wp],
|
|
|
|
|
|
"rules": [wr, {"ipRanges": ["*"], "domainNames": ["*"], "action": "DIRECT"}],
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
cfg.pop("egress", None)
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"🔄 WARP для `{escape_md(name)}`: *{status}*",
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("« К списку WARP", callback_data="warp_menu")],
|
|
|
|
|
|
[InlineKeyboardButton("« На главную", callback_data="main")],
|
|
|
|
|
|
]),
|
2026-06-26 23:37:05 +03:00
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-27 00:37:32 +03:00
|
|
|
|
def _build_client_config(name, password):
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
bindings = cfg.get("portBindings", [{}])[0]
|
|
|
|
|
|
proto = bindings.get("protocol", "TCP")
|
|
|
|
|
|
port_range = bindings.get("portRange", str(bindings.get("port", "?")))
|
|
|
|
|
|
ip = get_server_ip()
|
|
|
|
|
|
return {
|
|
|
|
|
|
"profiles": [{"profileName": "default",
|
|
|
|
|
|
"user": {"name": name, "password": password},
|
|
|
|
|
|
"servers": [{"ipAddress": ip,
|
|
|
|
|
|
"portBindings": [{"portRange": port_range,
|
|
|
|
|
|
"protocol": proto}]}]}],
|
|
|
|
|
|
"activeProfile": "default",
|
|
|
|
|
|
"rpcPort": 8964,
|
|
|
|
|
|
"socks5Port": 1080,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── config / Karing ──────────────────────────────────────────────────────────
|
2026-06-29 00:36:32 +03:00
|
|
|
|
@admin_only
|
|
|
|
|
|
async def show_user_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
name = query.data[len("user_"):]
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
user = next((u for u in cfg.get("users", []) if u["name"] == name), None)
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
await query.answer("Пользователь не найден")
|
|
|
|
|
|
return
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"👤 *{escape_md(name)}*",
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("📋 JSON-конфиг", callback_data=f"config_{name}"),
|
|
|
|
|
|
InlineKeyboardButton("📱 Karing", callback_data=f"karing_{name}")],
|
|
|
|
|
|
[InlineKeyboardButton("« К пользователям", callback_data="users")],
|
|
|
|
|
|
[InlineKeyboardButton("« На главную", callback_data="main")],
|
|
|
|
|
|
]),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-26 23:37:05 +03:00
|
|
|
|
@admin_only
|
2026-06-27 00:37:32 +03:00
|
|
|
|
async def show_config(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
2026-06-27 00:27:47 +03:00
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
2026-06-27 00:37:32 +03:00
|
|
|
|
name = query.data[len("config_"):]
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
user = next((u for u in cfg.get("users", []) if u["name"] == name), None)
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
await query.answer("Пользователь не найден")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
data = json.dumps(_build_client_config(name, user["password"]), indent=2, ensure_ascii=False)
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"📋 Конфиг `{escape_md(name)}`:\n```json\n{data[:3500]}\n```",
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("📱 Karing", callback_data=f"karing_{name}")],
|
2026-06-29 00:36:32 +03:00
|
|
|
|
[InlineKeyboardButton("« К пользователю", callback_data=f"user_{name}")],
|
|
|
|
|
|
[InlineKeyboardButton("« К списку", callback_data="users")],
|
2026-06-27 00:37:32 +03:00
|
|
|
|
]),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin_only
|
|
|
|
|
|
async def show_karing(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
name = query.data[len("karing_"):]
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
user = next((u for u in cfg.get("users", []) if u["name"] == name), None)
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
await query.answer("Пользователь не найден")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
data = _build_client_config(name, user["password"])
|
|
|
|
|
|
ip = data["profiles"][0]["servers"][0]["ipAddress"]
|
|
|
|
|
|
pb = data["profiles"][0]["servers"][0]["portBindings"][0]
|
|
|
|
|
|
port = (pb["portRange"] or str(pb.get("port", "?"))).split("-")[0]
|
|
|
|
|
|
|
|
|
|
|
|
text = (
|
|
|
|
|
|
f"📱 *Ручная настройка Karing* для `{escape_md(name)}`\n\n"
|
|
|
|
|
|
f"Поля для ввода в Karing:\n\n"
|
|
|
|
|
|
f"server:\n```\n{ip}\n```\n"
|
|
|
|
|
|
f"server port:\n```\n{port}\n```\n"
|
|
|
|
|
|
f"username:\n```\n{name}\n```\n"
|
|
|
|
|
|
f"password:\n```\n{user['password']}\n```\n"
|
|
|
|
|
|
f"transport:\n```\nTCP\n```\n"
|
|
|
|
|
|
f"multiplexing: *multiplexing\\_low*"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("📋 JSON-конфиг", callback_data=f"config_{name}")],
|
2026-06-29 00:36:32 +03:00
|
|
|
|
[InlineKeyboardButton("« К пользователю", callback_data=f"user_{name}")],
|
|
|
|
|
|
[InlineKeyboardButton("« К списку", callback_data="users")],
|
2026-06-27 00:37:32 +03:00
|
|
|
|
]),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── security ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@admin_only
|
|
|
|
|
|
async def show_security(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
|
|
|
|
|
|
installed = subprocess.run(["which","fail2ban-client"], capture_output=True).returncode == 0
|
|
|
|
|
|
active = jail_active = False
|
|
|
|
|
|
if installed:
|
|
|
|
|
|
r = subprocess.run(["systemctl","is-active","fail2ban"], capture_output=True, text=True)
|
|
|
|
|
|
active = r.stdout.strip() == "active"
|
|
|
|
|
|
if active:
|
|
|
|
|
|
r2 = subprocess.run(["fail2ban-client","status","mita-panel"], capture_output=True, text=True)
|
|
|
|
|
|
jail_active = r2.returncode == 0
|
|
|
|
|
|
|
|
|
|
|
|
pc = load_panel_config()
|
|
|
|
|
|
max_retry = pc.get("login_max_attempts", 5)
|
|
|
|
|
|
ban_time = pc.get("login_ban_seconds", 3600)
|
|
|
|
|
|
|
|
|
|
|
|
def fmt_sec(s):
|
|
|
|
|
|
if s >= 86400: return f"{s//86400} дн"
|
|
|
|
|
|
if s >= 3600: return f"{s//3600} ч"
|
|
|
|
|
|
if s >= 60: return f"{s//60} мин"
|
|
|
|
|
|
return f"{s} сек"
|
|
|
|
|
|
|
|
|
|
|
|
text = (
|
|
|
|
|
|
"🛡 *Безопасность*\n\n"
|
|
|
|
|
|
f"fail2ban: {'✅ установлен' if installed else '❌ не установлен'}\n"
|
|
|
|
|
|
f"Статус: {'🟢 активен' if active else '🔴 остановлен'}\n"
|
|
|
|
|
|
f"Jail mita\\-panel: {'🟢 активен' if jail_active else '⚪ не настроен'}\n\n"
|
|
|
|
|
|
f"Лимиты входа:\n"
|
|
|
|
|
|
f"• попыток: *{max_retry}*\n"
|
|
|
|
|
|
f"• бан: *{fmt_sec(ban_time)}*\n"
|
|
|
|
|
|
f"• встроенный лимит: {'✅' if pc.get('login_max_attempts') else '⚪'}\n\n"
|
|
|
|
|
|
"*Применить пресет:*"
|
|
|
|
|
|
)
|
2026-06-26 23:37:05 +03:00
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
await query.edit_message_text(
|
2026-06-27 00:37:32 +03:00
|
|
|
|
text,
|
2026-06-27 00:27:47 +03:00
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
2026-06-27 00:37:32 +03:00
|
|
|
|
[InlineKeyboardButton("Строгий 3/30m", callback_data="f2b_3_1800"),
|
|
|
|
|
|
InlineKeyboardButton("Стандарт 5/1h", callback_data="f2b_5_3600")],
|
|
|
|
|
|
[InlineKeyboardButton("Мягкий 10/15m", callback_data="f2b_10_900"),
|
|
|
|
|
|
InlineKeyboardButton("Жёсткий 3/24h", callback_data="f2b_3_86400")],
|
|
|
|
|
|
[InlineKeyboardButton("Установить fail2ban", callback_data="f2b_install")],
|
2026-06-27 00:27:47 +03:00
|
|
|
|
[InlineKeyboardButton("« Назад", callback_data="main")],
|
|
|
|
|
|
]),
|
2026-06-26 23:37:05 +03:00
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-27 00:37:32 +03:00
|
|
|
|
@admin_only
|
|
|
|
|
|
async def apply_f2b_preset(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
parts = query.data.split("_")
|
|
|
|
|
|
if len(parts) >= 3 and parts[1] == "install":
|
|
|
|
|
|
r = subprocess.run(["apt-get","install","-y","-qq","fail2ban"], capture_output=True, text=True)
|
|
|
|
|
|
if r.returncode != 0:
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"❌ Ошибка установки fail2ban: {r.stderr.strip()[:500]}",
|
|
|
|
|
|
reply_markup=back_button(),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
"✅ fail2ban установлен. Настройте лимиты через пресеты.",
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("« К безопасности", callback_data="security")],
|
|
|
|
|
|
]),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if len(parts) < 4:
|
|
|
|
|
|
return
|
|
|
|
|
|
max_retry = int(parts[2])
|
|
|
|
|
|
ban_time = int(parts[3])
|
|
|
|
|
|
|
|
|
|
|
|
pc = load_panel_config()
|
|
|
|
|
|
pc["login_max_attempts"] = max_retry
|
|
|
|
|
|
pc["login_ban_seconds"] = ban_time
|
|
|
|
|
|
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
filter_content = f"""[Definition]
|
|
|
|
|
|
failregex = ^<HOST> .+ "POST /[^"]+/login[^"]*" 4(?:01|29).*$
|
|
|
|
|
|
ignoreregex =
|
|
|
|
|
|
"""
|
|
|
|
|
|
jail_content = f"""[mita-panel]
|
|
|
|
|
|
enabled = true
|
|
|
|
|
|
filter = mita-panel
|
|
|
|
|
|
backend = auto
|
|
|
|
|
|
logpath = /var/log/mita-panel-access.log
|
|
|
|
|
|
maxretry = {max_retry}
|
|
|
|
|
|
bantime = {ban_time}
|
|
|
|
|
|
findtime = {ban_time}
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
Path("/etc/fail2ban/filter.d/mita-panel.conf").write_text(filter_content)
|
|
|
|
|
|
Path("/etc/fail2ban/jail.d/mita-panel.conf").write_text(jail_content)
|
|
|
|
|
|
subprocess.run(["systemctl","enable","fail2ban","--now"], capture_output=True)
|
|
|
|
|
|
subprocess.run(["systemctl","restart","fail2ban"], capture_output=True)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def fmt_sec(s):
|
|
|
|
|
|
if s >= 86400: return f"{s//86400} дн"
|
|
|
|
|
|
if s >= 3600: return f"{s//3600} ч"
|
|
|
|
|
|
if s >= 60: return f"{s//60} мин"
|
|
|
|
|
|
return f"{s} сек"
|
|
|
|
|
|
|
|
|
|
|
|
await query.edit_message_text(
|
|
|
|
|
|
f"🛡 *Пресет применён*\n\n"
|
|
|
|
|
|
f"Попыток: *{max_retry}*\nБан: *{fmt_sec(ban_time)}*",
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup([
|
|
|
|
|
|
[InlineKeyboardButton("« К безопасности", callback_data="security")],
|
|
|
|
|
|
[InlineKeyboardButton("« На главную", callback_data="main")],
|
|
|
|
|
|
]),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
# ── dashboard ─────────────────────────────────────────────────────────────────
|
2026-06-26 23:37:05 +03:00
|
|
|
|
@admin_only
|
2026-06-27 00:27:47 +03:00
|
|
|
|
async def show_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
query = update.callback_query
|
|
|
|
|
|
await query.answer()
|
|
|
|
|
|
cpu = psutil.cpu_percent(interval=0.5)
|
2026-06-26 23:37:05 +03:00
|
|
|
|
mem = psutil.virtual_memory()
|
|
|
|
|
|
disk = psutil.disk_usage("/")
|
|
|
|
|
|
ip = get_server_ip()
|
|
|
|
|
|
mita_ok = _mita_running()
|
|
|
|
|
|
|
|
|
|
|
|
warp_ip = "недоступен"
|
|
|
|
|
|
try:
|
|
|
|
|
|
r = subprocess.run(["curl","-s","--max-time","5","--proxy",
|
|
|
|
|
|
"socks5h://127.0.0.1:40000","https://ifconfig.me"],
|
|
|
|
|
|
capture_output=True, text=True)
|
|
|
|
|
|
warp_ip = r.stdout.strip()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
user_count = len(cfg.get("users", []))
|
|
|
|
|
|
bindings = cfg.get("portBindings", [{}])[0]
|
|
|
|
|
|
port = bindings.get("portRange", str(bindings.get("port","?")))
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
traffic = (0, 0)
|
2026-06-26 23:37:05 +03:00
|
|
|
|
try:
|
|
|
|
|
|
raw = mita_cmd("get", "users")
|
|
|
|
|
|
week_total = month_total = 0.0
|
2026-06-26 23:48:32 +03:00
|
|
|
|
for line in raw.splitlines():
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if not line or line.upper().startswith("USER"):
|
|
|
|
|
|
continue
|
|
|
|
|
|
parts = line.split()
|
|
|
|
|
|
if len(parts) < 8:
|
2026-06-26 23:37:05 +03:00
|
|
|
|
continue
|
2026-06-26 23:48:32 +03:00
|
|
|
|
week_total += _parse_traffic(parts[4]) + _parse_traffic(parts[5])
|
|
|
|
|
|
month_total += _parse_traffic(parts[6]) + _parse_traffic(parts[7])
|
2026-06-26 23:37:05 +03:00
|
|
|
|
traffic = (round(week_total/1024**3, 2), round(month_total/1024**3, 2))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
w, m = traffic
|
2026-06-26 23:37:05 +03:00
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
await query.edit_message_text(
|
2026-06-26 23:37:05 +03:00
|
|
|
|
f"📊 *Дашборд*\n\n"
|
2026-06-27 00:27:47 +03:00
|
|
|
|
f"IP: `{ip}`\nWARP: `{warp_ip}`\nПорт: `{port}`\n"
|
2026-06-26 23:37:05 +03:00
|
|
|
|
f"Пользователей: `{user_count}`\n"
|
2026-06-27 00:27:47 +03:00
|
|
|
|
f"Трафик нед: `{w} ГБ` мес: `{m} ГБ`\n"
|
2026-06-26 23:37:05 +03:00
|
|
|
|
f"mita: {'🟢' if mita_ok else '🔴'}\n"
|
|
|
|
|
|
f"CPU: `{cpu:.1f}%` RAM: `{mem.percent:.1f}%` Диск: `{disk.percent:.1f}%`",
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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"
|
2026-06-27 00:37:32 +03:00
|
|
|
|
"👥 Пользователи — список с трафиком, конфиг, Karing\n"
|
2026-06-27 00:27:47 +03:00
|
|
|
|
"➕ Создать — создать пользователя\n"
|
|
|
|
|
|
"🗑 Удалить — выбрать и удалить\n"
|
|
|
|
|
|
"🔄 WARP — включить/выключить\n"
|
2026-06-27 00:37:32 +03:00
|
|
|
|
"🛡 Безопасность — fail2ban, лимиты\n"
|
2026-06-27 00:27:47 +03:00
|
|
|
|
"📊 Дашборд — полный обзор",
|
|
|
|
|
|
reply_markup=back_button(),
|
2026-06-26 23:37:05 +03:00
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-27 00:27:47 +03:00
|
|
|
|
# ── 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)
|
2026-06-28 15:11:17 +03:00
|
|
|
|
elif data in ("create_easy", "create_hard"):
|
|
|
|
|
|
return await create_exec(update, context)
|
2026-06-27 00:27:47 +03:00
|
|
|
|
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)
|
2026-06-29 00:36:32 +03:00
|
|
|
|
elif data.startswith("user_"):
|
|
|
|
|
|
return await show_user_menu(update, context)
|
2026-06-27 00:37:32 +03:00
|
|
|
|
elif data == "security":
|
|
|
|
|
|
return await show_security(update, context)
|
|
|
|
|
|
elif data.startswith("config_"):
|
|
|
|
|
|
return await show_config(update, context)
|
|
|
|
|
|
elif data.startswith("karing_"):
|
|
|
|
|
|
return await show_karing(update, context)
|
|
|
|
|
|
elif data.startswith("f2b_"):
|
|
|
|
|
|
return await apply_f2b_preset(update, context)
|
2026-06-27 00:27:47 +03:00
|
|
|
|
elif data == "dashboard":
|
|
|
|
|
|
return await show_dashboard(update, context)
|
|
|
|
|
|
elif data == "help":
|
|
|
|
|
|
return await show_help(update, context)
|
|
|
|
|
|
|
2026-06-26 23:37:05 +03:00
|
|
|
|
# ── main ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def main():
|
|
|
|
|
|
cfg = load_bot_config()
|
|
|
|
|
|
token = os.environ.get("BOT_TOKEN", cfg.get("token", ""))
|
|
|
|
|
|
if not token:
|
|
|
|
|
|
_log.error("BOT_TOKEN не задан — ни в env, ни в %s", BOT_CONFIG)
|
|
|
|
|
|
raise SystemExit(1)
|
|
|
|
|
|
|
|
|
|
|
|
app = Application.builder().token(token).build()
|
|
|
|
|
|
app.add_handler(CommandHandler("start", cmd_start))
|
2026-06-27 00:27:47 +03:00
|
|
|
|
app.add_handler(CallbackQueryHandler(router))
|
2026-06-26 23:37:05 +03:00
|
|
|
|
|
|
|
|
|
|
_log.info("Бот запущен")
|
|
|
|
|
|
app.run_polling()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|