#!/bin/bash # GoTelegram MTProxy Bot — всё в одном файле. # Установка: curl -sL -H "Authorization: token TOKEN" https://raw.githubusercontent.com/anten-ka/gotelegram_pro/main/install.sh -o /usr/local/bin/gotelegram && chmod +x /usr/local/bin/gotelegram && gotelegram RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' fail() { echo -e "${RED}[ОШИБКА] $1${NC}"; exit 1; } [ "$EUID" -ne 0 ] && { echo -e "${RED}Запустите с sudo.${NC}"; exit 1; } BOT_DIR="/opt/gotelegram-bot" SERVICE_NAME="gotelegram-bot" echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ GoTelegram MTProxy Bot — установка ║${NC}" echo -e "${GREEN}╚═══════════════════════════════════════════╝${NC}" # ── Зависимости ────────────────────────────────────────────────────────────── 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 "$@" elif command -v yum &>/dev/null; then yum install -y "$@" fi } for cmd in python3 curl; do command -v $cmd &>/dev/null || { echo -e "${YELLOW}[*] Установка $cmd...${NC}"; install_pkg $cmd; } done command -v python3 &>/dev/null || fail "python3 не установлен." echo -e "${GREEN}[*] Проверка python3-venv...${NC}" if ! python3 -m venv --help &>/dev/null 2>&1; then echo -e "${YELLOW}[*] Установка python3-venv...${NC}" install_pkg python3-venv || install_pkg python3-virtualenv || true python3 -m venv --help &>/dev/null 2>&1 || { # На некоторых системах пакет называется python3.X-venv PY_VER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") install_pkg "python${PY_VER}-venv" 2>/dev/null || true } fi if ! command -v docker &>/dev/null; then echo -e "${YELLOW}[*] Docker не найден. Устанавливаю...${NC}" curl -fsSL https://get.docker.com | sh systemctl enable --now docker fi if ! docker info &>/dev/null 2>&1; then systemctl start docker 2>/dev/null || true sleep 2 docker info &>/dev/null 2>&1 || { echo -e "${RED}Docker не запускается.${NC}"; exit 1; } fi EXISTING=$(docker ps --format "{{.Names}}\t{{.Image}}\t{{.Ports}}" 2>/dev/null) if [ -n "$EXISTING" ]; then echo -e "${CYAN}[*] Работающие контейнеры (не будут затронуты):${NC}" echo "$EXISTING" | while IFS= read -r line; do echo " $line"; done fi # ── Встроенные файлы бота ──────────────────────────────────────────────────── mkdir -p "$BOT_DIR" cat > "$BOT_DIR/requirements.txt" << 'REQEOF' python-telegram-bot>=21.0 REQEOF cat > "$BOT_DIR/bot.py" << 'BOTEOF' #!/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: 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) return out if code == 0 else "" 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 = ( "🚀 GoTelegram MTProxy Bot\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Нажмите Установить для настройки." else: containers = await docker_containers_info() other = "\n".join(l for l in containers.splitlines() if CONTAINER_NAME not in l) 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'])}" ) 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) 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() text = f"{html.escape(info['link'])}" if info else "❌ Прокси не запущен." 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: 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 tg_link = info["link"] 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"Просто нажмите на ссылку или перешлите это сообщение." ) 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"
{html.escape(text)}
", parse_mode="HTML", reply_markup=kb) else: await msg.reply_text(f"
{html.escape(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}" ) 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 = "🌐 Выберите домен для маскировки (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") 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" f"🔌 Выберите порт или введите свой (1-65535):{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 = update.callback_query.message if update.callback_query else 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) 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" 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 и запуск контейнера...") else: return chat = msg.chat 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 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) 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", "-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 = ( "✅ Прокси установлен!\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) 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_"): ctx.user_data["install_port"] = data[6:] 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)) 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() BOTEOF echo -e "${GREEN}[*] Файлы бота записаны.${NC}" # ── Python venv ────────────────────────────────────────────────────────────── echo -e "${GREEN}[*] Настройка Python venv...${NC}" if [ ! -d "$BOT_DIR/venv" ]; then python3 -m venv "$BOT_DIR/venv" || fail "Не удалось создать venv. Установите: apt install python3-venv" fi echo -e "${GREEN}[*] Установка зависимостей (pip)...${NC}" "$BOT_DIR/venv/bin/pip" install --upgrade pip -q 2>/dev/null || true "$BOT_DIR/venv/bin/pip" install -r "$BOT_DIR/requirements.txt" -q || fail "pip install не удался." # ── Конфиг (.env) ──────────────────────────────────────────────────────────── if [ ! -f "$BOT_DIR/.env" ]; then echo "" echo -e "${YELLOW}Введите BOT_TOKEN от @BotFather:${NC}" TOKEN="" while [ -z "$TOKEN" ]; do read -r TOKEN TOKEN=$(echo "$TOKEN" | tr -d '[:space:]') [ -z "$TOKEN" ] && echo -e "${RED}Токен не может быть пустым.${NC}" done echo "BOT_TOKEN=$TOKEN" > "$BOT_DIR/.env" chmod 600 "$BOT_DIR/.env" echo -e "${GREEN}[*] .env создан.${NC}" else echo -e "${GREEN}[*] .env уже есть — пропускаю.${NC}" fi # ── systemd ────────────────────────────────────────────────────────────────── cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF [Unit] Description=GoTelegram MTProxy Bot 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 Environment=PATH=$BOT_DIR/venv/bin:/usr/bin:/usr/local/bin [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable "$SERVICE_NAME" 2>/dev/null systemctl restart "$SERVICE_NAME" 2>/dev/null || systemctl start "$SERVICE_NAME" echo "" echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Установка завершена! ║${NC}" echo -e "${GREEN}╚═══════════════════════════════════════════╝${NC}" echo -e "Бот: systemctl status $SERVICE_NAME" echo -e "Логи: journalctl -u $SERVICE_NAME -f" echo -e "Конфиг: $BOT_DIR/.env" exit 0