Files
MSWP/bot.py
T
grendervill 49db39c877 version 1.2.2: исправлен парсинг статистики трафика
- app.py: get_traffic_stats() — позиционный парсинг 8 колонок mita get users, без сканирования суффиксов
- bot.py: cmd_users, cmd_dashboard — единый позиционный парсинг вместо поиска единиц
- README: обновлена дорожная карта
2026-06-26 23:48:32 +03:00

403 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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:
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
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
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
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
week_total += _parse_traffic(parts[4]) + _parse_traffic(parts[5])
month_total += _parse_traffic(parts[6]) + _parse_traffic(parts[7])
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()