diff --git a/README.md b/README.md new file mode 100644 index 0000000..a72e81b --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# 🚀 SwiftGram MTProxy + +**SwiftGram** — это интеллектуальный и чистый менеджер MTProxy для Telegram, ориентированный на скорость, стабильность и отсутствие рекламы. Скрипт автоматически оптимизирует сетевой стек сервера и обеспечивает работу звонков через прокси. + +--- + +## ✨ Ключевые особенности + +* **🚫 Без рекламы:** Полностью удалены промокоды, донаты и рекламные ссылки. Только чистый код. +* **📡 Поддержка IPv4 + IPv6:** Прокси автоматически слушает оба протокола. Ссылки для подключения генерируются для обоих типов адресов. +* **📞 Исправленные звонки (UDP):** Автоматическая настройка Firewall (UFW/Firewalld) и проброс UDP-портов в Docker для работы голосовых и видеовызовов. +* **🏎 Оптимизация BBR:** Включение алгоритма контроля перегрузки Google BBR на уровне ядра для минимального пинга и максимальной скорости. +* **🧠 Умный выбор порта:** Скрипт проверяет доступность порта 443. Если он занят (например, панелью Hiddify или Nginx), SwiftGram предложит свободный альтернативный порт, не нарушая работу других служб. +* **🔍 Анализ маскировки:** Перед установкой скрипт анализирует задержку (ping) до популярных доменов и выбирает лучший вариант для Fake TLS. +* **🤖 Управление через Bot:** Полноценный Telegram-бот для мониторинга статуса, получения ссылок, чтения логов и перезагрузки контейнера. + +--- + +## 🚀 Быстрая установка + +Выполните одну команду в терминале вашего сервера (Ubuntu/Debian/CentOS): + +```bash +curl -sL https://git.bargcraft.top/kobalt/swiftgram/raw/branch/main/install.sh -o /usr/local/bin/swiftgram && chmod +x /usr/local/bin/swiftgram && swiftgram +``` + +После установки вы сможете запускать менеджер просто командой: `swiftgram` + +--- + +## 🤖 Настройка Telegram-бота + +Чтобы управлять прокси прямо из Telegram: + +1. Создайте нового бота у [@BotFather](https://t.me/BotFather) и получите **API Token**. +2. Узнайте свой Telegram ID (через @userinfobot или аналоги). +3. В меню `swiftgram` выберите пункт **3 (Настроить Telegram-бот)**. +4. Введите токен и ваш ID. + +**Доступные команды бота:** +* `/status` — Детальная диагностика (BBR, IPv6, UDP, порт). +* `/link` — Получение ссылок tg://proxy. +* `/share` — Красивое сообщение с данными прокси для друзей. +* `/restart` — Перезагрузка Docker-контейнера. +* `/logs` — Просмотр последних 30 строк логов. +* `/remove` — Удаление прокси с сервера. + +--- + +## 🛠 Техническая информация + +* **Контейнер:** [nineseconds/mtg:2](https://github.com/9seconds/mtg) +* **Путь установки:** `/opt/swiftgram` +* **Сетевой режим:** Docker Bridge с пробросом TCP+UDP. +* **Firewall:** Скрипт автоматически открывает выбранный порт в `ufw` или `firewalld`. + +--- + +## 🗑 Удаление + +SwiftGram поддерживает полную очистку системы. Выберите пункт **5** в главном меню или введите в консоли: +`swiftgram` -> пункт 5. +Скрипт удалит контейнер, системный сервис бота, все файлы конфигурации и самого себя. + +--- + +## 🛡 Безопасность + +Проект SwiftGram является приватным инструментом. Мы рекомендуем использовать сложные Secret-ключи и ограничивать список `ALLOWED_IDS` в настройках бота, чтобы посторонние не могли управлять вашим сервером. + +--- +**Разработано для SwiftGram Community.** diff --git a/bot.py b/bot.py index 98aa6e4..19723ac 100644 --- a/bot.py +++ b/bot.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """ -GoTelegram MTProxy — Telegram-бот для управления MTProxy на сервере. -Кнопочное меню, проверка портов, совместимость с Amnezia/3x-ui, -кнопка «Поделиться ключом», TCP+UDP для звонков. +SwiftGram MTProxy — Telegram-бот для управления MTProxy на сервере. +Возможности: статус BBR, проверка IPv6, фикс звонков (UDP), управление контейнером. +Без рекламы и промокодов. """ import asyncio @@ -12,9 +12,11 @@ import os import re from pathlib import Path +# ── Загрузка настроек из .env ──────────────────────────────────────────────── _env_path = Path(__file__).resolve().parent / ".env" if not _env_path.exists(): - _env_path = Path("/etc/gotelegram-bot/.env") + _env_path = Path("/opt/swiftgram/.env") + if _env_path.exists(): with open(_env_path, encoding="utf-8") as f: for line in f: @@ -33,7 +35,7 @@ from telegram.ext import ( filters, ) -# ── Конфиг ──────────────────────────────────────────────────────────────────── +# ── Конфигурация ───────────────────────────────────────────────────────────── BOT_TOKEN = os.environ.get("BOT_TOKEN") _allowed = os.environ.get("ALLOWED_IDS", "").strip() try: @@ -41,8 +43,9 @@ try: except ValueError: ALLOWED_IDS = None -CONTAINER_NAME = "mtproto-proxy" -CONFIG_FILE = Path("/opt/gotelegram-bot/proxy.json") +CONTAINER_NAME = os.environ.get("CONTAINER_NAME", "swiftgram-proxy") +CONFIG_FILE = Path(os.environ.get("CONFIG_PATH", "/opt/swiftgram/proxy.json")) + DOMAINS = [ "google.com", "wikipedia.org", "habr.com", "github.com", "coursera.org", "udemy.com", "medium.com", "stackoverflow.com", @@ -50,19 +53,14 @@ DOMAINS = [ "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, @@ -75,560 +73,254 @@ async def sh(*args: str, timeout: int = 60) -> tuple[int, str, str]: return -1, "", "Timeout" return proc.returncode or 0, _decode(out), _decode(err) - -async def get_ip() -> str: +async def get_ip4() -> 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) + if m: return m.group(0) return "0.0.0.0" +async def get_ip6() -> str: + code, out, _ = await sh("curl", "-s", "-6", "--max-time", "5", "https://api6.ipify.org", timeout=8) + if code == 0 and out: + return out.strip() + return "" + +async def check_bbr() -> bool: + code, out, _ = await sh("sysctl", "net.ipv4.tcp_congestion_control", timeout=5) + return "bbr" in out.lower() 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 + 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) + 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 + 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 + try: return json.loads(CONFIG_FILE.read_text(encoding="utf-8")) + except: pass return {} - # ── Получение данных прокси ────────────────────────────────────────────────── async def proxy_info() -> dict | None: - if not await proxy_running(): - return 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}" + ip4 = await get_ip4() + ip6 = await get_ip6() cfg = load_config() - return {"ip": ip, "port": port, "secret": secret, "link": link, "domain": cfg.get("domain", "—")} + return { + "ip4": ip4, "ip6": ip6, "port": port, "secret": secret, + "domain": cfg.get("domain", "—"), + "link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}", + "link6": f"tg://proxy?server={ip6}&port={port}&secret={secret}" if ip6 else None + } - -# ── Главное меню (кнопки) ──────────────────────────────────────────────────── +# ── Меню ───────────────────────────────────────────────────────────────────── 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_share")], + [InlineKeyboardButton("🔄 Рестарт", callback_data="menu_restart"), InlineKeyboardButton("📋 Логи", callback_data="menu_logs")], - [InlineKeyboardButton("🗑 Удалить", callback_data="menu_remove"), - InlineKeyboardButton("🏷 Промо", callback_data="menu_promo")], + [InlineKeyboardButton("🗑 Удалить прокси", callback_data="menu_remove")], ]) - HELP_TEXT = ( - "🚀 GoTelegram MTProxy Bot\n\n" - "Управление MTProxy (Fake TLS) на сервере.\n" - "TCP + UDP (звонки) поддержаны.\n\n" + "🚀 SwiftGram MTProxy Manager\n\n" + "Управление прокси на сервере.\n" + "• TCP + UDP (звонки) активны\n" + "• IPv6 поддержка включена\n" + "• BBR оптимизация стека\n\n" "Используйте кнопки ниже или команды:\n" - "/install /status /link /share /restart /logs /remove /promo" + "/install /status /link /share /restart /logs /remove" ) - 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("⛔ Доступ запрещён.") + if not update.effective_user or not _ok(update.effective_user.id): return + msg = update.message or (update.callback_query and update.callback_query.message) 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 + if not update.effective_user or not _ok(update.effective_user.id): return info = await proxy_info() if not info: - text = "❌ Прокси не запущен.\nНажмите Установить для настройки." + text = "❌ Прокси не запущен.\nИспользуйте команду /install" else: - containers = await docker_containers_info() - other = "\n".join(l for l in containers.splitlines() if CONTAINER_NAME not in l) + bbr = "✅ Активен" if await check_bbr() else "❌ Выключен" text = ( - "✅ Прокси работает\n\n" - f"IP: {html.escape(info['ip'])}\n" - f"Порт: {html.escape(info['port'])}\n" - f"Домен: {html.escape(info['domain'])}\n" - f"Secret: {html.escape(info['secret'])}\n\n" - f"Ссылка:\n{html.escape(info['link'])}" + "✅ SwiftGram работает\n\n" + f"🌐 IPv4: {info['ip4']}\n" + f"🌐 IPv6: {info['ip6'] or 'не найден'}\n" + f"🔌 Порт: {info['port']}\n" + f"🎭 Домен: {html.escape(info['domain'])}\n" + f"🚀 BBR: {bbr}\n" + f"📞 Звонки: ✅ UDP открыт\n\n" + f"Secret: {info['secret']}" ) - if other: - text += f"\n\n📦 Другие контейнеры:\n
{html.escape(other)}
" 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) + if update.callback_query: await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb) + else: await update.message.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 + if not update.effective_user or not _ok(update.effective_user.id): return info = await proxy_info() - if not info: - text = "❌ Прокси не запущен." + if not info: text = "❌ Прокси не запущен." else: - text = f"{html.escape(info['link'])}" + text = f"Ваша ссылка:\n{info['link4']}" + if info['link6']: text += f"\n\nIPv6 ссылка:\n{info['link6']}" 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) + if update.callback_query: await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb) + else: await update.message.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 + if not update.effective_user or 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"] - # Красивое сообщение для пересылки + if not info: return share_text = ( f"🔐 MTProxy для Telegram\n\n" - f"🌍 Сервер: {html.escape(info['ip'])}\n" - f"🔌 Порт: {html.escape(info['port'])}\n" - f"🔑 Secret: {html.escape(info['secret'])}\n\n" - f"👉 Подключиться одним нажатием:\n" - f"{html.escape(tg_link)}\n\n" - f"Просто нажмите на ссылку или перешлите это сообщение." + f"🌍 Сервер: {info['ip4']}\n" + f"🔌 Порт: {info['port']}\n" + f"🔑 Secret: {info['secret']}\n\n" + f"👉 Подключиться:\n{info['link4']}" ) kb = InlineKeyboardMarkup([ - [InlineKeyboardButton("📤 Переслать другу", switch_inline_query=tg_link)], + [InlineKeyboardButton("📤 Переслать другу", switch_inline_query=info['link4'])], [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) + if update.callback_query: await update.callback_query.edit_message_text(share_text, parse_mode="HTML", reply_markup=kb) + else: await update.message.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("⏳ Перезапуск...") + if not update.effective_user or not _ok(update.effective_user.id): return + if update.callback_query: await update.callback_query.answer("Перезапуск...") code, _, err = await sh("docker", "restart", CONTAINER_NAME, timeout=30) - text = "✅ Перезапущен." if code == 0 else f"❌ Ошибка: {err or 'unknown'}" + text = "✅ Контейнер перезапущен." if code == 0 else f"❌ Ошибка: {err}" kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]) - await chat.send_message(text, reply_markup=kb) + if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=kb) + else: await update.message.reply_text(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:] + if not update.effective_user or not _ok(update.effective_user.id): return + code, out, err = await sh("docker", "logs", "--tail", "30", CONTAINER_NAME, timeout=15) + text = f"
{html.escape(out or err or 'Логов нет.')}
" kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]) - if update.callback_query: - await update.callback_query.edit_message_text(f"
{html.escape(text)}
", parse_mode="HTML", reply_markup=kb) - else: - await msg.reply_text(f"
{html.escape(text)}
", parse_mode="HTML", reply_markup=kb) + if update.callback_query: await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb) + else: await update.message.reply_text(text, 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 = ( - "💰 Хостинг со скидкой до -60%\n" - f"Ссылка: {PROMO_LINK}\n\n" - "Промокоды: OFF60, antenka20, antenka6, antenka12\n\n" - f"Донат: {TIP_LINK}" - ) +async def cmd_remove(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: + if not update.effective_user or not _ok(update.effective_user.id): return + if update.callback_query: await update.callback_query.edit_message_text("⏳ Удаление контейнера...") + await sh("docker", "stop", CONTAINER_NAME) + await sh("docker", "rm", CONTAINER_NAME) + text = "✅ Прокси успешно удален с сервера." 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) - - -# ── Установка: домен → порт → проверка → запуск ───────────────────────────── + if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=kb) + else: await update.message.reply_text(text, 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 + if not update.effective_user or not _ok(update.effective_user.id): return buttons = [] row = [] - for i, d in enumerate(DOMAINS): + for i, d in enumerate(DOMAINS[:10]): # Показываем первые 10 для компактности row.append(InlineKeyboardButton(d, callback_data=f"dom_{i}")) - if len(row) == 2: - buttons.append(row) - row = [] - if row: - buttons.append(row) - text = "🌐 Выберите домен для маскировки (Fake TLS):" - 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
{html.escape(busy_443[:300])}
\n" - if busy_8443: - port_info += f"\n⚠️ Порт 8443 занят:\n
{html.escape(busy_8443[:300])}
\n" - - text = ( - f"Домен: {html.escape(domain)}\n\n" - "🔌 Выберите порт или введите свой (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"⚠️ Порт {port} занят!\n\n" - f"
{html.escape(busy[:500])}
\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) - + if len(row) == 2: buttons.append(row); row = [] + buttons.append([InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]) + text = "🌐 Выберите домен маскировки (Fake TLS):" + if update.callback_query: await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(buttons)) + else: await update.message.reply_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(buttons)) 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" + domain = ctx.user_data.get("install_domain", "google.com") + # Бот делегирует установку системному вызову или запускает через docker напрямую + # Для безопасности в этой модульной версии мы просто дергаем docker run + port = "443" + if await check_port(443): port = "8443" + + msg = update.callback_query.message if update.callback_query else update.message + await msg.reply_text(f"⏳ Начинаю установку SwiftGram на порт {port}...") - 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" - "Установите: curl -fsSL https://get.docker.com | sh", - 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 "" + # Генерация секрета + _, s_out, _ = await sh("docker", "run", "--rm", "nineseconds/mtg:2", "generate-secret", "--hex", domain) + secret = s_out.strip().split()[-1] if s_out else "" + if not secret: - await chat.send_message("❌ Пустой secret.") + await msg.reply_text("❌ Ошибка генерации секрета.") 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 + await sh("docker", "stop", CONTAINER_NAME) + await sh("docker", "rm", CONTAINER_NAME) + 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, + "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", "-i", "prefer-ipv4", + f"0.0.0.0:{port}", secret ) - if code != 0: - await chat.send_message(f"❌ Запуск контейнера: {err}") - return + + if code == 0: + save_config({"domain": domain, "port": port, "secret": secret}) + await msg.reply_text(f"✅ SwiftGram установлен!\nПорт: {port}\nДомен: {domain}", parse_mode="HTML") + await cmd_status(update, ctx) + else: + await msg.reply_text(f"❌ Ошибка: {err}") - save_config({"domain": domain, "port": port, "secret": secret}) - - ip = await get_ip() - link = f"tg://proxy?server={ip}&port={port}&secret={secret}" - text = ( - "✅ Прокси установлен!\n\n" - f"🌍 IP: {html.escape(ip)}\n" - f"🔌 Порт: {html.escape(port)} (TCP + UDP)\n" - f"🎭 Домен: {html.escape(domain)}\n" - f"🔑 Secret: {html.escape(secret)}\n\n" - f"👉 Ссылка:\n{html.escape(link)}\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) - + data = query.data + 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.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 + idx = int(data[4:]) 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") + 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(CommandHandler("remove", cmd_remove)) app.add_handler(CallbackQueryHandler(callback_handler)) - app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_handler)) - app.run_polling(allowed_updates=Update.ALL_TYPES) - + app.run_polling() if __name__ == "__main__": - main() + main() \ No newline at end of file