#!/usr/bin/env python3 """ GoTelegram MTProxy — Telegram-бот для управления MTProxy на сервере. Функции: установка, статус, ссылка, удаление, рестарт, логи (по аналогии с CLI gotelegram). """ import asyncio import html import os import re from pathlib import Path # Загрузка .env из текущей папки или /etc/gotelegram-bot _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() if _allowed: try: ALLOWED_IDS = set(int(x.strip()) for x in _allowed.split(",") if x.strip()) except ValueError: ALLOWED_IDS = None else: ALLOWED_IDS = None # все пользователи CONTAINER_NAME = "mtproto-proxy" 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 check_access(user_id: int) -> bool: if ALLOWED_IDS is None: return True return user_id in ALLOWED_IDS def _decode(data: bytes) -> str: return (data or b"").decode("utf-8", errors="replace").strip() async def run_cmd(*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: stdout, stderr = 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(stdout), _decode(stderr) async def docker_inspect(fmt: str) -> str: code, out, err = await run_cmd( "docker", "inspect", CONTAINER_NAME, "--format", fmt, timeout=10 ) if code != 0: return "" return out.strip() async def get_ip() -> str: for url in ["https://api.ipify.org", "https://icanhazip.com"]: code, out, _ = await run_cmd("curl", "-s", "-4", "--max-time", "5", url, timeout=8) if code == 0 and out: m = re.search(r"([0-9]{1,3}\.){3}[0-9]{1,3}", out) if m: return m.group(0) return "0.0.0.0" async def proxy_is_running() -> bool: code, out, _ = await run_cmd("docker", "ps", "--format", "{{.Names}}", timeout=10) if code != 0: return False return CONTAINER_NAME in (out or "") # --- Обработчики --- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") return text = ( "🚀 *GoTelegram MTProxy Bot*\n\n" "Управление MTProxy на этом сервере.\n\n" "Команды:\n" "/install — установить или обновить прокси (выбор домена и порта)\n" "/status — статус контейнера и данные подключения\n" "/link — только ссылка tg://proxy\n" "/restart — перезапустить прокси\n" "/logs — последние логи\n" "/remove — удалить прокси\n" "/promo — промо хостинга\n" "/help — эта справка" ) await update.message.reply_text(text, parse_mode="Markdown") async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") return if not await proxy_is_running(): await update.message.reply_text( "❌ Прокси не запущен.\nИспользуйте /install для установки." ) return secret = await docker_inspect("{{range .Config.Cmd}}{{.}} {{end}}") secret = secret.split()[-1] if secret else "" port = await docker_inspect("{{range $p, $conf := .HostConfig.PortBindings}}{{(index $conf 0).HostPort}}{{end}}") port = port or "443" ip = await get_ip() link = f"tg://proxy?server={ip}&port={port}&secret={secret}" # HTML безопаснее для произвольного secret (экранируем) text = ( "✅ Прокси запущен\n\n" f"IP: {html.escape(ip)}\n" f"Port: {html.escape(port)}\n" f"Secret: {html.escape(secret)}\n\n" f"Ссылка (скопируйте):\n{html.escape(link)}" ) await update.message.reply_text(text, parse_mode="HTML") async def cmd_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") return if not await proxy_is_running(): await update.message.reply_text("❌ Прокси не запущен. /install") return secret = await docker_inspect("{{range .Config.Cmd}}{{.}} {{end}}") secret = secret.split()[-1] if secret else "" port = await docker_inspect("{{range $p, $conf := .HostConfig.PortBindings}}{{(index $conf 0).HostPort}}{{end}}") port = port or "443" ip = await get_ip() link = f"tg://proxy?server={ip}&port={port}&secret={secret}" await update.message.reply_text(link) async def cmd_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") return await update.message.reply_text("Удаляю прокси...") await run_cmd("docker", "stop", CONTAINER_NAME, timeout=15) await run_cmd("docker", "rm", CONTAINER_NAME, timeout=10) if await proxy_is_running(): await update.message.reply_text("⚠️ Не удалось удалить. Проверьте docker вручную.") else: await update.message.reply_text("✅ Прокси удалён.") async def cmd_restart(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") return if not await proxy_is_running(): await update.message.reply_text("❌ Прокси не запущен. /install") return await update.message.reply_text("Перезапускаю...") code, _, err = await run_cmd("docker", "restart", CONTAINER_NAME, timeout=30) if code == 0: await update.message.reply_text("✅ Прокси перезапущен.") else: await update.message.reply_text(f"❌ Ошибка: {err or 'unknown'}") async def cmd_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") return if not await proxy_is_running(): await update.message.reply_text("❌ Прокси не запущен. /install") return code, out, err = await run_cmd("docker", "logs", "--tail", "40", CONTAINER_NAME, timeout=15) text = (out or "") + (("\n" + err) if err else "") if not text: text = "Нет вывода." if len(text) > 4000: text = text[-4000:] await update.message.reply_text(f"
{html.escape(text)}
", parse_mode="HTML") async def cmd_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") return text = ( "💰 *Хостинг со скидкой до -60%*\n" f"Ссылка: {PROMO_LINK}\n\n" "Промокоды: OFF60, antenka20, antenka6, antenka12\n\n" f"Донат: {TIP_LINK}" ) await update.message.reply_text(text, parse_mode="Markdown") # --- Установка: выбор домена и порта через инлайн-кнопки и диалог --- async def install_choice_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not update.message: return if not check_access(update.effective_user.id): await update.message.reply_text("⛔ Доступ запрещён.") 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) await update.message.reply_text( "Выберите домен для маскировки (Fake TLS):", reply_markup=InlineKeyboardMarkup(buttons), ) async def install_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query if not query or not update.effective_user: return await query.answer() if not check_access(update.effective_user.id): await query.edit_message_text("⛔ Доступ запрещён.") return data = query.data or "" if 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 domain = DOMAINS[idx] context.user_data["gotelegram_domain"] = domain kb = InlineKeyboardMarkup([ [InlineKeyboardButton("443 (рекомендуется)", callback_data="port_443"), InlineKeyboardButton("8443", callback_data="port_8443")], ]) await query.edit_message_text( f"Домен: {domain}\n\nВыберите порт или введите свой (1-65535):", reply_markup=kb, ) context.user_data["gotelegram_wait_port"] = True return if data == "port_443": context.user_data["gotelegram_port"] = "443" context.user_data["gotelegram_wait_port"] = False await do_install(update, context) return if data == "port_8443": context.user_data["gotelegram_port"] = "8443" context.user_data["gotelegram_wait_port"] = False await do_install(update, context) return async def do_install(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: domain = context.user_data.get("gotelegram_domain") or "google.com" port = context.user_data.get("gotelegram_port") or "443" if update.callback_query: msg = update.callback_query.message await msg.edit_text("⏳ Генерация secret и запуск контейнера...", reply_markup=None) else: msg = update.message chat = msg.chat if not update.callback_query: await chat.send_message("⏳ Генерация secret и запуск контейнера...") # generate secret code, secret_out, err = await run_cmd( "docker", "run", "--rm", "nineseconds/mtg:2", "generate-secret", "--hex", domain, timeout=30, ) if code != 0: await chat.send_message(f"❌ Ошибка генерации secret: {err or secret_out}") return secret = (secret_out or "").strip().split()[-1] or secret_out.strip() if not secret: await chat.send_message("❌ Не удалось получить secret.") return await run_cmd("docker", "stop", CONTAINER_NAME, timeout=15) await run_cmd("docker", "rm", CONTAINER_NAME, timeout=10) code, _, err = await run_cmd( "docker", "run", "-d", "--name", CONTAINER_NAME, "--restart", "always", "-p", f"{port}:{port}", "nineseconds/mtg:2", "simple-run", "-n", "1.1.1.1", "-i", "prefer-ipv4", f"0.0.0.0:{port}", secret, timeout=60, ) if code != 0: await chat.send_message(f"❌ Ошибка запуска контейнера: {err}") return ip = await get_ip() link = f"tg://proxy?server={ip}&port={port}&secret={secret}" text = ( "✅ Прокси установлен.\n" f"Домен: {html.escape(domain)}, порт: {html.escape(port)}\n\n" f"Ссылка:\n{html.escape(link)}" ) await chat.send_message(text, parse_mode="HTML") context.user_data.pop("gotelegram_domain", None) context.user_data.pop("gotelegram_port", None) context.user_data.pop("gotelegram_wait_port", None) async def handle_port_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.message: return if not context.user_data.get("gotelegram_wait_port"): return text = (update.message.text or "").strip() if not re.match(r"^[0-9]+$", text): return port_num = int(text) if not (1 <= port_num <= 65535): await update.message.reply_text("Введите число от 1 до 65535.") return context.user_data["gotelegram_port"] = str(port_num) context.user_data["gotelegram_wait_port"] = False await do_install(update, context) async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await start(update, context) 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", help_cmd)) app.add_handler(CommandHandler("install", install_choice_domain)) app.add_handler(CommandHandler("status", cmd_status)) app.add_handler(CommandHandler("link", cmd_link)) 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(install_callback)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_port_message)) app.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main()