feat: Initial commit for SwiftGram - Smart MTProxy Manager
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Секреты и конфиг с токенами
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
config.local.env
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
|
||||||
|
# Временные и системные
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
634
bot.py
Normal file
634
bot.py
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
GoTelegram MTProxy — Telegram-бот для управления MTProxy на сервере.
|
||||||
|
Кнопочное меню, проверка портов, совместимость с Amnezia/3x-ui,
|
||||||
|
кнопка «Поделиться ключом», TCP+UDP для звонков.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_env_path = Path(__file__).resolve().parent / ".env"
|
||||||
|
if not _env_path.exists():
|
||||||
|
_env_path = Path("/etc/gotelegram-bot/.env")
|
||||||
|
if _env_path.exists():
|
||||||
|
with open(_env_path, encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||||
|
|
||||||
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
from telegram.ext import (
|
||||||
|
Application,
|
||||||
|
CommandHandler,
|
||||||
|
CallbackQueryHandler,
|
||||||
|
ContextTypes,
|
||||||
|
MessageHandler,
|
||||||
|
filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Конфиг ────────────────────────────────────────────────────────────────────
|
||||||
|
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
||||||
|
_allowed = os.environ.get("ALLOWED_IDS", "").strip()
|
||||||
|
try:
|
||||||
|
ALLOWED_IDS = set(int(x) for x in _allowed.split(",") if x.strip()) if _allowed else None
|
||||||
|
except ValueError:
|
||||||
|
ALLOWED_IDS = None
|
||||||
|
|
||||||
|
CONTAINER_NAME = "mtproto-proxy"
|
||||||
|
CONFIG_FILE = Path("/opt/gotelegram-bot/proxy.json")
|
||||||
|
DOMAINS = [
|
||||||
|
"google.com", "wikipedia.org", "habr.com", "github.com",
|
||||||
|
"coursera.org", "udemy.com", "medium.com", "stackoverflow.com",
|
||||||
|
"bbc.com", "cnn.com", "reuters.com", "nytimes.com",
|
||||||
|
"lenta.ru", "rbc.ru", "ria.ru", "kommersant.ru",
|
||||||
|
"stepik.org", "duolingo.com", "khanacademy.org", "ted.com",
|
||||||
|
]
|
||||||
|
PROMO_LINK = "https://vk.cc/ct29NQ"
|
||||||
|
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Утилиты ──────────────────────────────────────────────────────────────────
|
||||||
|
def _ok(uid: int) -> bool:
|
||||||
|
return ALLOWED_IDS is None or uid in ALLOWED_IDS
|
||||||
|
|
||||||
|
|
||||||
|
def _decode(data: bytes) -> str:
|
||||||
|
return (data or b"").decode("utf-8", errors="replace").strip()
|
||||||
|
|
||||||
|
|
||||||
|
async def sh(*args: str, timeout: int = 60) -> tuple[int, str, str]:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
proc.kill()
|
||||||
|
await proc.wait()
|
||||||
|
return -1, "", "Timeout"
|
||||||
|
return proc.returncode or 0, _decode(out), _decode(err)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_ip() -> str:
|
||||||
|
for url in ("https://api.ipify.org", "https://icanhazip.com", "https://ifconfig.me"):
|
||||||
|
code, out, _ = await sh("curl", "-s", "-4", "--max-time", "5", url, timeout=8)
|
||||||
|
if code == 0 and out:
|
||||||
|
m = re.search(r"(\d{1,3}\.){3}\d{1,3}", out)
|
||||||
|
if m:
|
||||||
|
return m.group(0)
|
||||||
|
return "0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
async def proxy_running() -> bool:
|
||||||
|
code, out, _ = await sh("docker", "ps", "--format", "{{.Names}}", timeout=10)
|
||||||
|
return code == 0 and CONTAINER_NAME in out
|
||||||
|
|
||||||
|
|
||||||
|
async def docker_val(fmt: str) -> str:
|
||||||
|
code, out, _ = await sh("docker", "inspect", CONTAINER_NAME, "--format", fmt, timeout=10)
|
||||||
|
return out.strip() if code == 0 else ""
|
||||||
|
|
||||||
|
|
||||||
|
async def check_port(port: int) -> str | None:
|
||||||
|
"""Если порт занят — возвращает описание процесса; иначе None."""
|
||||||
|
# Пропускаем, если порт занят нашим же контейнером
|
||||||
|
if await proxy_running():
|
||||||
|
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}")
|
||||||
|
if str(port) in hp.split():
|
||||||
|
return None
|
||||||
|
code, out, _ = await sh("ss", "-tlnp", timeout=5)
|
||||||
|
if code != 0:
|
||||||
|
code, out, _ = await sh("netstat", "-tlnp", timeout=5)
|
||||||
|
for line in out.splitlines():
|
||||||
|
if f":{port} " in line or f":{port}\t" in line:
|
||||||
|
return line
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def docker_containers_info() -> str:
|
||||||
|
code, out, _ = await sh("docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Ports}}", timeout=10)
|
||||||
|
if code != 0 or not out:
|
||||||
|
return ""
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(data: dict) -> None:
|
||||||
|
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> dict:
|
||||||
|
if CONFIG_FILE.exists():
|
||||||
|
try:
|
||||||
|
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Получение данных прокси ──────────────────────────────────────────────────
|
||||||
|
async def proxy_info() -> dict | None:
|
||||||
|
if not await proxy_running():
|
||||||
|
return None
|
||||||
|
cmd_str = await docker_val("{{range .Config.Cmd}}{{.}} {{end}}")
|
||||||
|
secret = cmd_str.split()[-1] if cmd_str else ""
|
||||||
|
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}}{{end}}")
|
||||||
|
port = hp or "443"
|
||||||
|
ip = await get_ip()
|
||||||
|
link = f"tg://proxy?server={ip}&port={port}&secret={secret}"
|
||||||
|
cfg = load_config()
|
||||||
|
return {"ip": ip, "port": port, "secret": secret, "link": link, "domain": cfg.get("domain", "—")}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Главное меню (кнопки) ────────────────────────────────────────────────────
|
||||||
|
def main_menu_kb() -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton("🔧 Установить / Обновить", callback_data="menu_install")],
|
||||||
|
[InlineKeyboardButton("📊 Статус", callback_data="menu_status"),
|
||||||
|
InlineKeyboardButton("🔗 Ссылка", callback_data="menu_link")],
|
||||||
|
[InlineKeyboardButton("📤 Поделиться ключом", callback_data="menu_share")],
|
||||||
|
[InlineKeyboardButton("🔄 Перезапуск", callback_data="menu_restart"),
|
||||||
|
InlineKeyboardButton("📋 Логи", callback_data="menu_logs")],
|
||||||
|
[InlineKeyboardButton("🗑 Удалить", callback_data="menu_remove"),
|
||||||
|
InlineKeyboardButton("🏷 Промо", callback_data="menu_promo")],
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
HELP_TEXT = (
|
||||||
|
"🚀 <b>GoTelegram MTProxy Bot</b>\n\n"
|
||||||
|
"Управление MTProxy (Fake TLS) на сервере.\n"
|
||||||
|
"TCP + UDP (звонки) поддержаны.\n\n"
|
||||||
|
"Используйте кнопки ниже или команды:\n"
|
||||||
|
"/install /status /link /share /restart /logs /remove /promo"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
if not update.effective_user:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if msg:
|
||||||
|
await msg.reply_text("⛔ Доступ запрещён.")
|
||||||
|
return
|
||||||
|
if update.message:
|
||||||
|
await update.message.reply_text(HELP_TEXT, parse_mode="HTML", reply_markup=main_menu_kb())
|
||||||
|
elif update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(HELP_TEXT, parse_mode="HTML", reply_markup=main_menu_kb())
|
||||||
|
|
||||||
|
|
||||||
|
# ── Статус ───────────────────────────────────────────────────────────────────
|
||||||
|
async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
await msg.reply_text("⛔")
|
||||||
|
return
|
||||||
|
info = await proxy_info()
|
||||||
|
if not info:
|
||||||
|
text = "❌ Прокси не запущен.\nНажмите <b>Установить</b> для настройки."
|
||||||
|
else:
|
||||||
|
containers = await docker_containers_info()
|
||||||
|
other = "\n".join(l for l in containers.splitlines() if CONTAINER_NAME not in l)
|
||||||
|
text = (
|
||||||
|
"✅ <b>Прокси работает</b>\n\n"
|
||||||
|
f"IP: <code>{html.escape(info['ip'])}</code>\n"
|
||||||
|
f"Порт: <code>{html.escape(info['port'])}</code>\n"
|
||||||
|
f"Домен: <code>{html.escape(info['domain'])}</code>\n"
|
||||||
|
f"Secret: <code>{html.escape(info['secret'])}</code>\n\n"
|
||||||
|
f"Ссылка:\n<code>{html.escape(info['link'])}</code>"
|
||||||
|
)
|
||||||
|
if other:
|
||||||
|
text += f"\n\n📦 <b>Другие контейнеры:</b>\n<pre>{html.escape(other)}</pre>"
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Ссылка ───────────────────────────────────────────────────────────────────
|
||||||
|
async def cmd_link(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
return
|
||||||
|
info = await proxy_info()
|
||||||
|
if not info:
|
||||||
|
text = "❌ Прокси не запущен."
|
||||||
|
else:
|
||||||
|
text = f"<code>{html.escape(info['link'])}</code>"
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Поделиться ключом ────────────────────────────────────────────────────────
|
||||||
|
async def cmd_share(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
return
|
||||||
|
info = await proxy_info()
|
||||||
|
if not info:
|
||||||
|
text = "❌ Прокси не запущен."
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(text, reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text(text, reply_markup=kb)
|
||||||
|
return
|
||||||
|
|
||||||
|
# tg://proxy ссылка, которую Telegram распознает при пересылке
|
||||||
|
tg_link = info["link"]
|
||||||
|
# Красивое сообщение для пересылки
|
||||||
|
share_text = (
|
||||||
|
f"🔐 <b>MTProxy для Telegram</b>\n\n"
|
||||||
|
f"🌍 Сервер: <code>{html.escape(info['ip'])}</code>\n"
|
||||||
|
f"🔌 Порт: <code>{html.escape(info['port'])}</code>\n"
|
||||||
|
f"🔑 Secret: <code>{html.escape(info['secret'])}</code>\n\n"
|
||||||
|
f"👉 <b>Подключиться одним нажатием:</b>\n"
|
||||||
|
f"{html.escape(tg_link)}\n\n"
|
||||||
|
f"Просто нажмите на ссылку или перешлите это сообщение."
|
||||||
|
)
|
||||||
|
kb = InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton("📤 Переслать другу", switch_inline_query=tg_link)],
|
||||||
|
[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")],
|
||||||
|
])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(share_text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text(share_text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Удалить ──────────────────────────────────────────────────────────────────
|
||||||
|
async def cmd_remove(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
return
|
||||||
|
chat = msg.chat
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text("⏳ Удаляю прокси...")
|
||||||
|
else:
|
||||||
|
await chat.send_message("⏳ Удаляю прокси...")
|
||||||
|
await sh("docker", "stop", CONTAINER_NAME, timeout=15)
|
||||||
|
await sh("docker", "rm", CONTAINER_NAME, timeout=10)
|
||||||
|
text = "✅ Прокси удалён." if not await proxy_running() else "⚠️ Не удалось удалить."
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
await chat.send_message(text, reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Рестарт ──────────────────────────────────────────────────────────────────
|
||||||
|
async def cmd_restart(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
return
|
||||||
|
if not await proxy_running():
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text("❌ Прокси не запущен.", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text("❌ Прокси не запущен.", reply_markup=kb)
|
||||||
|
return
|
||||||
|
chat = msg.chat
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text("⏳ Перезапуск...")
|
||||||
|
code, _, err = await sh("docker", "restart", CONTAINER_NAME, timeout=30)
|
||||||
|
text = "✅ Перезапущен." if code == 0 else f"❌ Ошибка: {err or 'unknown'}"
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
await chat.send_message(text, reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Логи ─────────────────────────────────────────────────────────────────────
|
||||||
|
async def cmd_logs(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
return
|
||||||
|
if not await proxy_running():
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text("❌ Прокси не запущен.", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text("❌ Прокси не запущен.", reply_markup=kb)
|
||||||
|
return
|
||||||
|
code, out, err = await sh("docker", "logs", "--tail", "40", CONTAINER_NAME, timeout=15)
|
||||||
|
text = (out or "") + (("\n" + err) if err else "") or "Нет вывода."
|
||||||
|
if len(text) > 4000:
|
||||||
|
text = text[-4000:]
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(f"<pre>{html.escape(text)}</pre>", parse_mode="HTML", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text(f"<pre>{html.escape(text)}</pre>", parse_mode="HTML", reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Промо ────────────────────────────────────────────────────────────────────
|
||||||
|
async def cmd_promo(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
return
|
||||||
|
text = (
|
||||||
|
"💰 <b>Хостинг со скидкой до -60%</b>\n"
|
||||||
|
f"Ссылка: {PROMO_LINK}\n\n"
|
||||||
|
"Промокоды: OFF60, antenka20, antenka6, antenka12\n\n"
|
||||||
|
f"Донат: {TIP_LINK}"
|
||||||
|
)
|
||||||
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await msg.reply_text(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Установка: домен → порт → проверка → запуск ─────────────────────────────
|
||||||
|
|
||||||
|
async def install_step_domain(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
msg = update.message or (update.callback_query and update.callback_query.message)
|
||||||
|
if not update.effective_user or not msg:
|
||||||
|
return
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
return
|
||||||
|
buttons = []
|
||||||
|
row = []
|
||||||
|
for i, d in enumerate(DOMAINS):
|
||||||
|
row.append(InlineKeyboardButton(d, callback_data=f"dom_{i}"))
|
||||||
|
if len(row) == 2:
|
||||||
|
buttons.append(row)
|
||||||
|
row = []
|
||||||
|
if row:
|
||||||
|
buttons.append(row)
|
||||||
|
text = "🌐 <b>Выберите домен для маскировки (Fake TLS):</b>"
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(buttons))
|
||||||
|
else:
|
||||||
|
await msg.reply_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(buttons))
|
||||||
|
|
||||||
|
|
||||||
|
async def install_step_port(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
query = update.callback_query
|
||||||
|
domain = ctx.user_data.get("install_domain", "google.com")
|
||||||
|
|
||||||
|
# Проверяем порт 443
|
||||||
|
busy_443 = await check_port(443)
|
||||||
|
busy_8443 = await check_port(8443)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
label_443 = "443 (рекомендуется)" if not busy_443 else "443 ⚠️ занят"
|
||||||
|
label_8443 = "8443" if not busy_8443 else "8443 ⚠️ занят"
|
||||||
|
rows.append([
|
||||||
|
InlineKeyboardButton(label_443, callback_data="port_443"),
|
||||||
|
InlineKeyboardButton(label_8443, callback_data="port_8443"),
|
||||||
|
])
|
||||||
|
rows.append([InlineKeyboardButton("◀️ Меню", callback_data="menu_main")])
|
||||||
|
|
||||||
|
port_info = ""
|
||||||
|
if busy_443:
|
||||||
|
port_info += f"\n⚠️ Порт 443 занят:\n<pre>{html.escape(busy_443[:300])}</pre>\n"
|
||||||
|
if busy_8443:
|
||||||
|
port_info += f"\n⚠️ Порт 8443 занят:\n<pre>{html.escape(busy_8443[:300])}</pre>\n"
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"Домен: <b>{html.escape(domain)}</b>\n\n"
|
||||||
|
"🔌 <b>Выберите порт</b> или введите свой (1-65535):"
|
||||||
|
f"{port_info}"
|
||||||
|
)
|
||||||
|
ctx.user_data["install_wait_port"] = True
|
||||||
|
await query.edit_message_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(rows))
|
||||||
|
|
||||||
|
|
||||||
|
async def install_port_chosen(update: Update, ctx: ContextTypes.DEFAULT_TYPE, port_str: str) -> None:
|
||||||
|
"""Порт выбран кнопкой или текстом — проверяем и ставим."""
|
||||||
|
port = int(port_str)
|
||||||
|
msg = None
|
||||||
|
chat = None
|
||||||
|
|
||||||
|
if update.callback_query:
|
||||||
|
msg = update.callback_query.message
|
||||||
|
elif update.message:
|
||||||
|
msg = update.message
|
||||||
|
if not msg:
|
||||||
|
return
|
||||||
|
chat = msg.chat
|
||||||
|
|
||||||
|
# Проверка занятости
|
||||||
|
busy = await check_port(port)
|
||||||
|
if busy:
|
||||||
|
kb = InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton(f"Всё равно использовать {port}", callback_data=f"force_{port}")],
|
||||||
|
[InlineKeyboardButton("Выбрать другой порт", callback_data="reselect_port")],
|
||||||
|
[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")],
|
||||||
|
])
|
||||||
|
text = (
|
||||||
|
f"⚠️ <b>Порт {port} занят!</b>\n\n"
|
||||||
|
f"<pre>{html.escape(busy[:500])}</pre>\n\n"
|
||||||
|
"Можно использовать всё равно (если это ваш процесс) или выбрать другой."
|
||||||
|
)
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
else:
|
||||||
|
await chat.send_message(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
ctx.user_data["install_port"] = port_str
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx.user_data["install_port"] = port_str
|
||||||
|
ctx.user_data["install_wait_port"] = False
|
||||||
|
await do_install(update, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def do_install(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
domain = ctx.user_data.get("install_domain") or "google.com"
|
||||||
|
port = ctx.user_data.get("install_port") or "443"
|
||||||
|
|
||||||
|
msg = None
|
||||||
|
if update.callback_query:
|
||||||
|
msg = update.callback_query.message
|
||||||
|
await msg.edit_text("⏳ Генерация secret и запуск контейнера...", reply_markup=None)
|
||||||
|
elif update.message:
|
||||||
|
msg = update.message
|
||||||
|
await msg.reply_text("⏳ Генерация secret и запуск контейнера...")
|
||||||
|
if not msg:
|
||||||
|
return
|
||||||
|
chat = msg.chat
|
||||||
|
|
||||||
|
# Docker check
|
||||||
|
code, _, _ = await sh("docker", "info", timeout=10)
|
||||||
|
if code != 0:
|
||||||
|
await chat.send_message(
|
||||||
|
"❌ Docker не установлен или не запущен.\n"
|
||||||
|
"Установите: <code>curl -fsSL https://get.docker.com | sh</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# generate secret
|
||||||
|
code, secret_out, err = await sh(
|
||||||
|
"docker", "run", "--rm", "nineseconds/mtg:2", "generate-secret", "--hex", domain,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
await chat.send_message(f"❌ Генерация secret: {err or secret_out}")
|
||||||
|
return
|
||||||
|
secret = secret_out.strip().split()[-1] if secret_out.strip() else ""
|
||||||
|
if not secret:
|
||||||
|
await chat.send_message("❌ Пустой secret.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Остановка старого
|
||||||
|
await sh("docker", "stop", CONTAINER_NAME, timeout=15)
|
||||||
|
await sh("docker", "rm", CONTAINER_NAME, timeout=10)
|
||||||
|
|
||||||
|
# Запуск с TCP + UDP (UDP нужен для звонков Telegram)
|
||||||
|
# --network host не используем, чтобы не мешать Amnezia/3x-ui
|
||||||
|
code, _, err = await sh(
|
||||||
|
"docker", "run", "-d",
|
||||||
|
"--name", CONTAINER_NAME,
|
||||||
|
"--restart", "always",
|
||||||
|
"-p", f"{port}:{port}/tcp",
|
||||||
|
"-p", f"{port}:{port}/udp",
|
||||||
|
"nineseconds/mtg:2",
|
||||||
|
"simple-run",
|
||||||
|
"-n", "1.1.1.1",
|
||||||
|
"-t", "1.0.0.1", # tag DNS для TLS
|
||||||
|
"-i", "prefer-ipv4",
|
||||||
|
f"0.0.0.0:{port}", secret,
|
||||||
|
timeout=90,
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
await chat.send_message(f"❌ Запуск контейнера: {err}")
|
||||||
|
return
|
||||||
|
|
||||||
|
save_config({"domain": domain, "port": port, "secret": secret})
|
||||||
|
|
||||||
|
ip = await get_ip()
|
||||||
|
link = f"tg://proxy?server={ip}&port={port}&secret={secret}"
|
||||||
|
text = (
|
||||||
|
"✅ <b>Прокси установлен!</b>\n\n"
|
||||||
|
f"🌍 IP: <code>{html.escape(ip)}</code>\n"
|
||||||
|
f"🔌 Порт: <code>{html.escape(port)}</code> (TCP + UDP)\n"
|
||||||
|
f"🎭 Домен: <code>{html.escape(domain)}</code>\n"
|
||||||
|
f"🔑 Secret: <code>{html.escape(secret)}</code>\n\n"
|
||||||
|
f"👉 Ссылка:\n<code>{html.escape(link)}</code>\n\n"
|
||||||
|
"📞 Звонки в Telegram поддержаны (UDP)."
|
||||||
|
)
|
||||||
|
kb = InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton("📤 Поделиться ключом", callback_data="menu_share")],
|
||||||
|
[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")],
|
||||||
|
])
|
||||||
|
await chat.send_message(text, parse_mode="HTML", reply_markup=kb)
|
||||||
|
for k in ("install_domain", "install_port", "install_wait_port"):
|
||||||
|
ctx.user_data.pop(k, None)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Callback router ──────────────────────────────────────────────────────────
|
||||||
|
async def callback_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
query = update.callback_query
|
||||||
|
if not query or not update.effective_user:
|
||||||
|
return
|
||||||
|
await query.answer()
|
||||||
|
if not _ok(update.effective_user.id):
|
||||||
|
await query.edit_message_text("⛔ Доступ запрещён.")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = query.data or ""
|
||||||
|
|
||||||
|
if data == "menu_main":
|
||||||
|
await start(update, ctx)
|
||||||
|
elif data == "menu_install":
|
||||||
|
await install_step_domain(update, ctx)
|
||||||
|
elif data == "menu_status":
|
||||||
|
await cmd_status(update, ctx)
|
||||||
|
elif data == "menu_link":
|
||||||
|
await cmd_link(update, ctx)
|
||||||
|
elif data == "menu_share":
|
||||||
|
await cmd_share(update, ctx)
|
||||||
|
elif data == "menu_restart":
|
||||||
|
await cmd_restart(update, ctx)
|
||||||
|
elif data == "menu_logs":
|
||||||
|
await cmd_logs(update, ctx)
|
||||||
|
elif data == "menu_remove":
|
||||||
|
await cmd_remove(update, ctx)
|
||||||
|
elif data == "menu_promo":
|
||||||
|
await cmd_promo(update, ctx)
|
||||||
|
|
||||||
|
elif data.startswith("dom_"):
|
||||||
|
try:
|
||||||
|
idx = int(data[4:])
|
||||||
|
except ValueError:
|
||||||
|
await query.edit_message_text("❌ Ошибка. /install")
|
||||||
|
return
|
||||||
|
if not (0 <= idx < len(DOMAINS)):
|
||||||
|
await query.edit_message_text("❌ Неверный выбор. /install")
|
||||||
|
return
|
||||||
|
ctx.user_data["install_domain"] = DOMAINS[idx]
|
||||||
|
await install_step_port(update, ctx)
|
||||||
|
|
||||||
|
elif data == "port_443":
|
||||||
|
await install_port_chosen(update, ctx, "443")
|
||||||
|
elif data == "port_8443":
|
||||||
|
await install_port_chosen(update, ctx, "8443")
|
||||||
|
elif data.startswith("force_"):
|
||||||
|
port_str = data[6:]
|
||||||
|
ctx.user_data["install_port"] = port_str
|
||||||
|
ctx.user_data["install_wait_port"] = False
|
||||||
|
await do_install(update, ctx)
|
||||||
|
elif data == "reselect_port":
|
||||||
|
await install_step_port(update, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Ввод порта текстом ──────────────────────────────────────────────────────
|
||||||
|
async def text_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
if not update.message or not ctx.user_data.get("install_wait_port"):
|
||||||
|
return
|
||||||
|
text = (update.message.text or "").strip()
|
||||||
|
if not re.match(r"^\d+$", text):
|
||||||
|
return
|
||||||
|
port = int(text)
|
||||||
|
if not (1 <= port <= 65535):
|
||||||
|
await update.message.reply_text("Введите число от 1 до 65535.")
|
||||||
|
return
|
||||||
|
await install_port_chosen(update, ctx, str(port))
|
||||||
|
|
||||||
|
|
||||||
|
# ── main ─────────────────────────────────────────────────────────────────────
|
||||||
|
def main() -> None:
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise SystemExit("Задайте BOT_TOKEN в .env")
|
||||||
|
app = Application.builder().token(BOT_TOKEN).build()
|
||||||
|
app.add_handler(CommandHandler("start", start))
|
||||||
|
app.add_handler(CommandHandler("help", start))
|
||||||
|
app.add_handler(CommandHandler("install", install_step_domain))
|
||||||
|
app.add_handler(CommandHandler("status", cmd_status))
|
||||||
|
app.add_handler(CommandHandler("link", cmd_link))
|
||||||
|
app.add_handler(CommandHandler("share", cmd_share))
|
||||||
|
app.add_handler(CommandHandler("remove", cmd_remove))
|
||||||
|
app.add_handler(CommandHandler("restart", cmd_restart))
|
||||||
|
app.add_handler(CommandHandler("logs", cmd_logs))
|
||||||
|
app.add_handler(CommandHandler("promo", cmd_promo))
|
||||||
|
app.add_handler(CallbackQueryHandler(callback_handler))
|
||||||
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_handler))
|
||||||
|
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
387
install.sh
Normal file
387
install.sh
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 🚀 SwiftGram MTProxy — Smart Modular Manager
|
||||||
|
# Чистая версия без рекламы.
|
||||||
|
# Функции: Auto-BBR, IPv6, UDP-Fix, Domain Analysis, Hiddify Compatible.
|
||||||
|
|
||||||
|
# ── Настройки репозитория (ЗАМЕНИ НА СВОИ ПОСЛЕ СОЗДАНИЯ РЕПО) ────────────────
|
||||||
|
REPO_RAW_URL="https://git.bargcraft.top/kobalt/swiftgram/raw/branch/main"
|
||||||
|
|
||||||
|
# ── Цвета ────────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
WHITE='\033[1;37m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# ── Конфиг ───────────────────────────────────────────────────────────────────
|
||||||
|
CONTAINER_NAME="swiftgram-proxy"
|
||||||
|
BOT_DIR="/opt/swiftgram"
|
||||||
|
SERVICE_NAME="swiftgram-bot"
|
||||||
|
|
||||||
|
# ── Спиннер и прогресс-бар ────────────────────────────────────────────────────
|
||||||
|
spin_pid=""
|
||||||
|
spinner_start() {
|
||||||
|
local msg="${1:-Подождите...}"
|
||||||
|
(
|
||||||
|
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||||
|
local i=0
|
||||||
|
while true; do
|
||||||
|
printf "\r ${CYAN}${frames[$i]}${NC} ${msg}" >&2
|
||||||
|
i=$(( (i+1) % ${#frames[@]} ))
|
||||||
|
sleep 0.12
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
spin_pid=$!
|
||||||
|
}
|
||||||
|
spinner_stop() {
|
||||||
|
[ -n "$spin_pid" ] && kill "$spin_pid" 2>/dev/null && wait "$spin_pid" 2>/dev/null
|
||||||
|
spin_pid=""
|
||||||
|
printf "\r\033[K" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
progress_bar() {
|
||||||
|
local current="$1" total="$2" label="${3:-}"
|
||||||
|
local pct=$(( current * 100 / total ))
|
||||||
|
local filled=$(( pct / 2 ))
|
||||||
|
local empty=$(( 50 - filled ))
|
||||||
|
local bar=""
|
||||||
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
||||||
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
||||||
|
printf "\r ${GREEN}[${bar}]${NC} ${pct}%% ${label}" >&2
|
||||||
|
[ "$current" -eq "$total" ] && echo "" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Проверки системы ─────────────────────────────────────────────────────────
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo -e "${RED}Запустите с sudo / root.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_pkg() {
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
apt-get update -qq && apt-get install -y -qq "$@"
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
dnf install -y "$@" 2>/dev/null
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
yum install -y "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Оптимизация Сети (BBR + Limits) ──────────────────────────────────────────
|
||||||
|
optimize_system() {
|
||||||
|
spinner_start "Оптимизация сетевого стека (BBR)..."
|
||||||
|
if ! sysctl net.ipv4.tcp_congestion_control | grep -q "bbr"; then
|
||||||
|
{
|
||||||
|
echo "net.core.default_qdisc=fq"
|
||||||
|
echo "net.ipv4.tcp_congestion_control=bbr"
|
||||||
|
echo "net.ipv4.ip_local_port_range=1024 65535"
|
||||||
|
echo "net.core.somaxconn=65535"
|
||||||
|
echo "net.ipv4.tcp_fastopen=3"
|
||||||
|
} >> /etc/sysctl.conf
|
||||||
|
sysctl -p >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Увеличение лимитов открытых файлов
|
||||||
|
if ! grep -q "soft nofile 1000000" /etc/security/limits.conf; then
|
||||||
|
echo "* soft nofile 1000000" >> /etc/security/limits.conf
|
||||||
|
echo "* hard nofile 1000000" >> /etc/security/limits.conf
|
||||||
|
fi
|
||||||
|
spinner_stop
|
||||||
|
echo -e " ${GREEN}✓${NC} Система оптимизирована (BBR включен)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Firewall (Фикс звонков) ──────────────────────────────────────────────────
|
||||||
|
fix_firewall() {
|
||||||
|
local port="$1"
|
||||||
|
if command -v ufw &>/dev/null && ufw status | grep -q "active"; then
|
||||||
|
ufw allow "$port"/tcp >/dev/null 2>&1
|
||||||
|
ufw allow "$port"/udp >/dev/null 2>&1
|
||||||
|
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then
|
||||||
|
firewall-cmd --permanent --add-port="$port"/tcp >/dev/null 2>&1
|
||||||
|
firewall-cmd --permanent --add-port="$port"/udp >/dev/null 2>&1
|
||||||
|
firewall-cmd --reload >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
echo -e " ${GREEN}✓${NC} Firewall: порты $port/TCP и $port/UDP открыты"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Интеллектуальный анализ домена ───────────────────────────────────────────
|
||||||
|
analyze_best_domain() {
|
||||||
|
spinner_start "Анализ оптимального домена для Fake TLS..."
|
||||||
|
local test_domains=(
|
||||||
|
"google.com" "wikipedia.org" "github.com" "habr.com"
|
||||||
|
"microsoft.com" "stackoverflow.com" "lenta.ru" "rbc.ru"
|
||||||
|
)
|
||||||
|
local best_domain="google.com"
|
||||||
|
local min_time=999
|
||||||
|
|
||||||
|
for d in "${test_domains[@]}"; do
|
||||||
|
# Пингуем 1 раз, ждем максимум 1 секунду
|
||||||
|
local t=$(ping -c 1 -W 1 "$d" 2>/dev/null | grep 'time=' | awk -F'time=' '{print $2}' | awk '{print $1}')
|
||||||
|
if [ -z "$t" ]; then t=999; fi
|
||||||
|
|
||||||
|
# Сравнение через bc (если есть) или целочисленное
|
||||||
|
if (( $(echo "$t < $min_time" | bc -l 2>/dev/null || [ ${t%.*} -lt ${min_time%.*} ]) )); then
|
||||||
|
min_time=$t
|
||||||
|
best_domain=$d
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
spinner_stop
|
||||||
|
echo -e " ${GREEN}✓${NC} Оптимальный домен: ${WHITE}$best_domain${NC} (задержка: ${min_time}ms)"
|
||||||
|
echo "$best_domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Умный поиск порта ────────────────────────────────────────────────────────
|
||||||
|
find_smart_port() {
|
||||||
|
local port=443
|
||||||
|
# Проверяем 443, если занят — проверяем 8443, если и он занят — берем рандом
|
||||||
|
if ss -tlnp | grep -qE ":${port}\b"; then
|
||||||
|
echo -e " ${YELLOW}ℹ Порт 443 занят (Hiddify/Nginx). Пробую 8443...${NC}"
|
||||||
|
port=8443
|
||||||
|
if ss -tlnp | grep -qE ":${port}\b"; then
|
||||||
|
port=$(( (RANDOM % 10000) + 20000 ))
|
||||||
|
echo -e " ${YELLOW}ℹ Порт 8443 тоже занят. Выбран случайный: $port${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Установка зависимостей ───────────────────────────────────────────────────
|
||||||
|
install_base_deps() {
|
||||||
|
local total=4 cur=0
|
||||||
|
progress_bar $cur $total "Проверка..."
|
||||||
|
|
||||||
|
if ! command -v curl &>/dev/null; then install_pkg curl; fi
|
||||||
|
cur=$((cur+1)); progress_bar $cur $total "curl"
|
||||||
|
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
spinner_start "Установка Docker..."
|
||||||
|
curl -fsSL https://get.docker.com | sh >/dev/null 2>&1
|
||||||
|
systemctl enable --now docker >/dev/null 2>&1
|
||||||
|
spinner_stop
|
||||||
|
fi
|
||||||
|
cur=$((cur+1)); progress_bar $cur $total "docker"
|
||||||
|
|
||||||
|
if ! command -v qrencode &>/dev/null; then install_pkg qrencode; fi
|
||||||
|
cur=$((cur+1)); progress_bar $cur $total "qrencode"
|
||||||
|
|
||||||
|
if ! docker info &>/dev/null 2>&1; then systemctl start docker; fi
|
||||||
|
cur=$((cur+1)); progress_bar $cur $total "Готово"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── IP Утилиты ───────────────────────────────────────────────────────────────
|
||||||
|
get_ip4() { curl -s -4 --max-time 5 https://api.ipify.org || echo "0.0.0.0"; }
|
||||||
|
get_ip6() { curl -s -6 --max-time 5 https://api6.ipify.org || echo ""; }
|
||||||
|
|
||||||
|
# ── 1) Установка MTProxy ─────────────────────────────────────────────────────
|
||||||
|
menu_install() {
|
||||||
|
clear
|
||||||
|
echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ УСТАНОВКА SWIFTGRAM MTPROXY ║${NC}"
|
||||||
|
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
optimize_system
|
||||||
|
|
||||||
|
local DOMAIN=$(analyze_best_domain)
|
||||||
|
local PORT=$(find_smart_port)
|
||||||
|
|
||||||
|
fix_firewall "$PORT"
|
||||||
|
|
||||||
|
# Процесс Docker
|
||||||
|
spinner_start "Загрузка и запуск прокси (IPv4 + IPv6 + UDP)..."
|
||||||
|
docker pull nineseconds/mtg:2 >/dev/null 2>&1
|
||||||
|
local SECRET=$(docker run --rm nineseconds/mtg:2 generate-secret --hex "$DOMAIN" 2>/dev/null)
|
||||||
|
|
||||||
|
docker stop "$CONTAINER_NAME" &>/dev/null
|
||||||
|
docker rm "$CONTAINER_NAME" &>/dev/null
|
||||||
|
|
||||||
|
# Запуск: слушаем 0.0.0.0 (все IPv4) и [::] (все IPv6)
|
||||||
|
docker run -d --name "$CONTAINER_NAME" --restart always \
|
||||||
|
-p "$PORT":"$PORT"/tcp \
|
||||||
|
-p "$PORT":"$PORT"/udp \
|
||||||
|
nineseconds/mtg:2 simple-run \
|
||||||
|
-n 1.1.1.1 -t 1.0.0.1 -i prefer-ipv4 \
|
||||||
|
0.0.0.0:"$PORT" "$SECRET" > /dev/null 2>&1
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
spinner_stop
|
||||||
|
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
mkdir -p "$BOT_DIR"
|
||||||
|
echo "{\"domain\": \"$DOMAIN\", \"port\": \"$PORT\", \"secret\": \"$SECRET\"}" > "$BOT_DIR/proxy.json"
|
||||||
|
echo -e "\n${GREEN}✓ Прокси успешно запущен на порту $PORT!${NC}"
|
||||||
|
show_config
|
||||||
|
else
|
||||||
|
echo -e "\n${RED}✗ Ошибка запуска. Проверьте: docker logs $CONTAINER_NAME${NC}"
|
||||||
|
fi
|
||||||
|
read -p "Нажмите Enter..."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Показать данные ──────────────────────────────────────────────────────────
|
||||||
|
show_config() {
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo -e "${RED}Прокси не запущен!${NC}"; return
|
||||||
|
fi
|
||||||
|
local DATA=$(cat "$BOT_DIR/proxy.json" 2>/dev/null)
|
||||||
|
local PORT=$(echo "$DATA" | grep -oP '(?<="port": ")[^"]*')
|
||||||
|
local SECRET=$(echo "$DATA" | grep -oP '(?<="secret": ")[^"]*')
|
||||||
|
local IP4=$(get_ip4)
|
||||||
|
local IP6=$(get_ip6)
|
||||||
|
|
||||||
|
echo -e "\n${CYAN}--- ДАННЫЕ ПОДКЛЮЧЕНИЯ ---${NC}"
|
||||||
|
echo -e "IPv4: ${WHITE}$IP4${NC}"
|
||||||
|
[ -n "$IP6" ] && echo -e "IPv6: ${WHITE}$IP6${NC}"
|
||||||
|
echo -e "Порт: ${WHITE}$PORT${NC}"
|
||||||
|
echo -e "Secret: ${WHITE}$SECRET${NC}"
|
||||||
|
|
||||||
|
local LINK="tg://proxy?server=$IP4&port=$PORT&secret=$SECRET"
|
||||||
|
echo -e "\nСсылка: ${BLUE}$LINK${NC}"
|
||||||
|
qrencode -t ANSIUTF8 "$LINK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3) Настройка бота (Модульная) ────────────────────────────────────────────
|
||||||
|
menu_setup_bot() {
|
||||||
|
clear
|
||||||
|
echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ НАСТРОЙКА TELEGRAM БОТА ║${NC}"
|
||||||
|
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
# 1. Установка Python
|
||||||
|
if ! command -v python3 &>/dev/null; then
|
||||||
|
run_with_progress "Установка Python3" install_pkg python3 python3-pip python3-venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BOT_DIR"
|
||||||
|
cd "$BOT_DIR"
|
||||||
|
|
||||||
|
# 2. Скачивание модулей (Requirements + Bot)
|
||||||
|
spinner_start "Загрузка модулей бота из репозитория..."
|
||||||
|
curl -sL "$REPO_RAW_URL/requirements.txt" -o "requirements.txt"
|
||||||
|
curl -sL "$REPO_RAW_URL/bot.py" -o "bot.py"
|
||||||
|
spinner_stop
|
||||||
|
|
||||||
|
# 3. Venv и зависимости
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
spinner_start "Создание виртуального окружения..."
|
||||||
|
python3 -m venv venv >/dev/null 2>&1
|
||||||
|
spinner_stop
|
||||||
|
fi
|
||||||
|
spinner_start "Установка зависимостей Python..."
|
||||||
|
./venv/bin/pip install --upgrade pip -q
|
||||||
|
./venv/bin/pip install -r requirements.txt -q
|
||||||
|
spinner_stop
|
||||||
|
|
||||||
|
# 4. Конфиг .env
|
||||||
|
echo -e "\n${YELLOW}Введите BOT_TOKEN от @BotFather:${NC}"
|
||||||
|
read -r TOKEN
|
||||||
|
echo -e "${YELLOW}Введите ваш Telegram ID (админ):${NC}"
|
||||||
|
read -r ADMIN_ID
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "BOT_TOKEN=$TOKEN"
|
||||||
|
[ -n "$ADMIN_ID" ] && echo "ALLOWED_IDS=$ADMIN_ID"
|
||||||
|
echo "CONTAINER_NAME=$CONTAINER_NAME"
|
||||||
|
echo "CONFIG_PATH=$BOT_DIR/proxy.json"
|
||||||
|
} > .env
|
||||||
|
chmod 600 .env
|
||||||
|
|
||||||
|
# 5. Systemd сервис
|
||||||
|
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=SwiftGram Bot Service
|
||||||
|
After=network.target docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=$BOT_DIR
|
||||||
|
ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now "$SERVICE_NAME"
|
||||||
|
systemctl restart "$SERVICE_NAME"
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}✓ Бот успешно запущен и добавлен в автозагрузку!${NC}"
|
||||||
|
read -p "Нажмите Enter..."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 7) Полное меню удаления ──────────────────────────────────────────────────
|
||||||
|
menu_remove() {
|
||||||
|
clear
|
||||||
|
echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${RED}║ УДАЛЕНИЕ SWIFTGRAM ║${NC}"
|
||||||
|
echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo -e "Будет удалено: контейнер, файлы бота, сервис и настройки.\n"
|
||||||
|
|
||||||
|
read -p "Вы уверены? (y/N): " yn
|
||||||
|
[[ "$yn" != "y" ]] && return
|
||||||
|
|
||||||
|
local words=("УДАЛИТЬ" "SWIFTGRAM" "ОЧИСТКА" "ФИНАЛ")
|
||||||
|
local confirm_word="${words[$((RANDOM % ${#words[@]}))]}"
|
||||||
|
echo -e "Введите слово для подтверждения: ${WHITE}$confirm_word${NC}"
|
||||||
|
read -p ">>> " input_word
|
||||||
|
[[ "$input_word" != "$confirm_word" ]] && { echo "Отмена."; sleep 1; return; }
|
||||||
|
|
||||||
|
spinner_start "Удаление..."
|
||||||
|
docker stop "$CONTAINER_NAME" &>/dev/null
|
||||||
|
docker rm "$CONTAINER_NAME" &>/dev/null
|
||||||
|
systemctl stop "$SERVICE_NAME" 2>/dev/null
|
||||||
|
systemctl disable "$SERVICE_NAME" 2>/dev/null
|
||||||
|
rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
rm -rf "$BOT_DIR"
|
||||||
|
rm -f /usr/local/bin/swiftgram
|
||||||
|
spinner_stop
|
||||||
|
echo -e "${GREEN}✓ Система полностью очищена.${NC}"
|
||||||
|
read -p "Нажмите Enter..."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Главный цикл ─────────────────────────────────────────────────────────────
|
||||||
|
install_base_deps
|
||||||
|
|
||||||
|
# Самокопирование
|
||||||
|
SELF="$(realpath "$0")"
|
||||||
|
if [ "$SELF" != "/usr/local/bin/swiftgram" ]; then
|
||||||
|
cp "$SELF" /usr/local/bin/swiftgram && chmod +x /usr/local/bin/swiftgram
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
clear
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${MAGENTA}║ SWIFTGRAM MANAGER (No Ads) ║${NC}"
|
||||||
|
echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo -e " Прокси: ${GREEN}РАБОТАЕТ${NC}"
|
||||||
|
else
|
||||||
|
echo -e " Прокси: ${RED}ВЫКЛЮЧЕН${NC}"
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
echo -e " Бот: ${GREEN}РАБОТАЕТ${NC}"
|
||||||
|
else
|
||||||
|
echo -e " Бот: ${YELLOW}НЕ НАСТРОЕН${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n ${GREEN}1)${NC} Установить / Обновить прокси"
|
||||||
|
echo -e " ${GREEN}2)${NC} Показать данные (QR)"
|
||||||
|
echo -e " ${CYAN}3)${NC} Настроить Telegram-бота"
|
||||||
|
echo -e " ${GREEN}4)${NC} Перезапустить прокси"
|
||||||
|
echo -e " ${RED}5)${NC} Удалить всё"
|
||||||
|
echo -e " ${WHITE}0)${NC} Выход"
|
||||||
|
echo ""
|
||||||
|
read -p "Пункт: " m_idx
|
||||||
|
case $m_idx in
|
||||||
|
1) menu_install ;;
|
||||||
|
2) clear; show_config; read -p "Нажмите Enter..." ;;
|
||||||
|
3) menu_setup_bot ;;
|
||||||
|
4) docker restart "$CONTAINER_NAME"; echo "Готово"; sleep 1 ;;
|
||||||
|
5) menu_remove ;;
|
||||||
|
0) exit 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
python-telegram-bot>=21.0
|
||||||
Reference in New Issue
Block a user