#!/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()