2026-06-26 23:37:05 +03:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
mita Telegram Bot — управление сервером через Telegram
|
|
|
|
|
|
"""
|
|
|
|
|
|
import os, json, re, subprocess, psutil, logging
|
|
|
|
|
|
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:
|
|
|
|
|
|
from telegram import Update
|
|
|
|
|
|
from telegram.ext import Application, CommandHandler, ContextTypes
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
# ── 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:
|
|
|
|
|
|
await update.message.reply_text("⛔ Доступ запрещён.")
|
|
|
|
|
|
return
|
|
|
|
|
|
return await func(update, context)
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
# ── commands ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@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 \\- все команды",
|
|
|
|
|
|
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 `имя` \\- клиентский конфиг",
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin_only
|
|
|
|
|
|
async def cmd_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
cfg = load_mita_config()
|
|
|
|
|
|
users = cfg.get("users", [])
|
|
|
|
|
|
if not users:
|
|
|
|
|
|
await update.message.reply_text("👤 Нет пользователей.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
raw = mita_cmd("get", "users")
|
|
|
|
|
|
pc = load_panel_config()
|
|
|
|
|
|
warp_users = set(pc.get("warp_users", []))
|
|
|
|
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
today = datetime.now().date()
|
|
|
|
|
|
|
|
|
|
|
|
data_lines = []
|
|
|
|
|
|
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:
|
2026-06-26 23:48:32 +03:00
|
|
|
|
parts = line.split()
|
|
|
|
|
|
if len(parts) < 8:
|
2026-06-26 23:37:05 +03:00
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
name = parts[0]
|
2026-06-26 23:48:32 +03:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-06-26 23:37:05 +03:00
|
|
|
|
|
|
|
|
|
|
online = False
|
2026-06-26 23:48:32 +03:00
|
|
|
|
la_raw = parts[1]
|
2026-06-26 23:37:05 +03:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-26 23:48:32 +03:00
|
|
|
|
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)}
|
2026-06-26 23:37:05 +03:00
|
|
|
|
except Exception:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
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 "—"
|
|
|
|
|
|
lines.append(f"{o} `{name}` {w} \\| день: {day} \\| мес: {mon}")
|
|
|
|
|
|
|
|
|
|
|
|
await update.message.reply_text(
|
|
|
|
|
|
"👥 *Пользователи*\n\n" + "\n".join(lines),
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin_only
|
|
|
|
|
|
async def cmd_create(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
args = context.args
|
|
|
|
|
|
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}"
|
|
|
|
|
|
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})
|
|
|
|
|
|
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","?")))
|
|
|
|
|
|
|
|
|
|
|
|
await update.message.reply_text(
|
|
|
|
|
|
f"✅ Пользователь *{name}* создан\\.\n\n"
|
|
|
|
|
|
f"Сервер: `{ip}`\n"
|
|
|
|
|
|
f"Порт: `{port}`\n"
|
|
|
|
|
|
f"Пароль: `{password}`\n\n"
|
|
|
|
|
|
f"Получить JSON\\-конфиг: /config `{name}`",
|
|
|
|
|
|
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()
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
|
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\\)",
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
@admin_only
|
|
|
|
|
|
async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
cpu = psutil.cpu_percent(interval=1)
|
|
|
|
|
|
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(
|
|
|
|
|
|
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 '🔴 остановлен'}",
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin_only
|
|
|
|
|
|
async def cmd_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
|
cpu = psutil.cpu_percent(interval=1)
|
|
|
|
|
|
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","?")))
|
|
|
|
|
|
|
|
|
|
|
|
traffic = None
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
w, m = traffic or (0, 0)
|
|
|
|
|
|
|
|
|
|
|
|
await update.message.reply_text(
|
|
|
|
|
|
f"📊 *Дашборд*\n\n"
|
|
|
|
|
|
f"IP сервера: `{ip}`\n"
|
|
|
|
|
|
f"IP WARP: `{warp_ip}`\n"
|
|
|
|
|
|
f"Порт mita: `{port}`\n"
|
|
|
|
|
|
f"Пользователей: `{user_count}`\n"
|
|
|
|
|
|
f"Трафик нед\\.: `{w} ГБ`\n"
|
|
|
|
|
|
f"Трафик мес\\.: `{m} ГБ`\n"
|
|
|
|
|
|
f"mita: {'🟢' if mita_ok else '🔴'}\n"
|
|
|
|
|
|
f"CPU: `{cpu:.1f}%` RAM: `{mem.percent:.1f}%` Диск: `{disk.percent:.1f}%`",
|
|
|
|
|
|
parse_mode="MarkdownV2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 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))
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
|
_log.info("Бот запущен")
|
|
|
|
|
|
app.run_polling()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|