mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 14:26:02 +00:00
695 lines
30 KiB
Bash
695 lines
30 KiB
Bash
#!/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
|