mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 11:26:03 +00:00
GoTelegram MTProxy + bot (gotelegram_pro)
Made-with: Cursor
This commit is contained in:
70
gotelegram-bot/README.md
Normal file
70
gotelegram-bot/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# GoTelegram MTProxy Bot
|
||||
|
||||
Telegram-бот для управления MTProxy на сервере — те же функции, что и у CLI `gotelegram`, но через бота.
|
||||
|
||||
## Команды
|
||||
|
||||
| Команда | Описание |
|
||||
|--------|----------|
|
||||
| `/start`, `/help` | Справка |
|
||||
| `/install` | Установить или обновить прокси (выбор домена и порта) |
|
||||
| `/status` | Статус и данные подключения (IP, порт, secret, ссылка) |
|
||||
| `/link` | Только ссылка `tg://proxy` |
|
||||
| `/restart` | Перезапустить контейнер |
|
||||
| `/logs` | Последние логи контейнера |
|
||||
| `/remove` | Удалить прокси |
|
||||
| `/promo` | Промо хостинга |
|
||||
|
||||
## Установка на сервер
|
||||
|
||||
### Публичный репозиторий (одной командой)
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/anten-ka/gotelegram_pro/main/install_gotelegram_bot.sh -o /tmp/install_gotelegram_bot.sh && sudo bash /tmp/install_gotelegram_bot.sh
|
||||
```
|
||||
|
||||
При установке скрипт запросит **BOT_TOKEN** (получить у [@BotFather](https://t.me/BotFather)).
|
||||
|
||||
### Закрытый репозиторий (установка по ключу)
|
||||
|
||||
Для **приватного** репо используется клонирование по **SSH-ключу** или по **токену (PAT)**. Подробно: **[INSTALL_PRIVATE.md](../INSTALL_PRIVATE.md)** в корне репозитория.
|
||||
|
||||
Кратко:
|
||||
- **По SSH:** скопируйте `bootstrap_install.sh` на сервер, затем
|
||||
`GIT_REPO_SSH=git@github.com:USER/REPO.git sudo bash bootstrap_install.sh`
|
||||
- **По токену:**
|
||||
`GITHUB_TOKEN=ghp_xxx GIT_REPO_HTTPS=https://github.com/USER/REPO.git sudo -E bash bootstrap_install.sh`
|
||||
- Или клонируйте репо вручную и запустите:
|
||||
`sudo ./install_gotelegram_bot.sh`
|
||||
|
||||
### Локально (файлы уже рядом со скриптом)
|
||||
|
||||
```bash
|
||||
sudo ./install_gotelegram_bot.sh
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Файл: `/opt/gotelegram-bot/.env`
|
||||
|
||||
- **BOT_TOKEN** — токен от @BotFather (обязательно).
|
||||
- **ALLOWED_IDS** — опционально. Список ID пользователей через запятую; если не задан, бот доступен всем.
|
||||
|
||||
После изменения `.env` перезапуск сервиса:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart gotelegram-bot
|
||||
```
|
||||
|
||||
## Требования на сервере
|
||||
|
||||
- Linux (systemd), Docker, Python 3.
|
||||
- Перед использованием бота на сервере должен быть установлен Docker (бот сам поднимает контейнер `nineseconds/mtg:2` по команде `/install`).
|
||||
|
||||
## Управление сервисом
|
||||
|
||||
```bash
|
||||
sudo systemctl status gotelegram-bot
|
||||
sudo systemctl restart gotelegram-bot
|
||||
journalctl -u gotelegram-bot -f
|
||||
```
|
||||
402
gotelegram-bot/bot.py
Normal file
402
gotelegram-bot/bot.py
Normal file
@@ -0,0 +1,402 @@
|
||||
#!/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()
|
||||
4
gotelegram-bot/config.example.env
Normal file
4
gotelegram-bot/config.example.env
Normal file
@@ -0,0 +1,4 @@
|
||||
# Скопируйте в .env и заполните
|
||||
BOT_TOKEN=your_bot_token_from_@BotFather
|
||||
# Опционально: список ID пользователей с доступом (через запятую). Пусто = все.
|
||||
# ALLOWED_IDS=123456789,987654321
|
||||
1
gotelegram-bot/requirements.txt
Normal file
1
gotelegram-bot/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
python-telegram-bot>=21.0
|
||||
Reference in New Issue
Block a user