Files
gotelegram_pro/gotelegram-bot/bot.py
anten-ka 29b6e05866 GoTelegram MTProxy + bot (gotelegram_pro)
Made-with: Cursor
2026-03-06 14:59:00 +03:00

403 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
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.
#!/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 = (
"✅ <b>Прокси запущен</b>\n\n"
f"IP: <code>{html.escape(ip)}</code>\n"
f"Port: <code>{html.escape(port)}</code>\n"
f"Secret: <code>{html.escape(secret)}</code>\n\n"
f"Ссылка (скопируйте):\n<code>{html.escape(link)}</code>"
)
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"<pre>{html.escape(text)}</pre>", 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<code>{html.escape(link)}</code>"
)
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()