Files
gotelegram_pro/install.sh

695 lines
30 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 = (
"🚀 <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()
text = f"<code>{html.escape(info['link'])}</code>" 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"🔐 <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")
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"
f"🔌 <b>Выберите порт</b> или введите свой (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"⚠️ <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"
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"
"Установите: <code>curl -fsSL https://get.docker.com | sh</code>",
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 = (
"✅ <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)
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