16 Commits

Author SHA1 Message Date
anten-ka
d70e046035 add bootstrap.sh installer 2026-04-09 11:15:43 +03:00
anten-ka
c70cb36a2b GoTelegram v2.3.1: full push
- MTProxy manager with Telegram bot
- 1801 site templates, 18 categories
- Stealth mode (fake-TLS + nginx mask)
- Admin auto-registration in bot
- Fixed frame alignment, bot venv setup
2026-04-09 11:14:41 +03:00
anten-ka
b36fb5cf10 v2.3.1: fix frame alignment + bot venv setup 2026-04-09 11:04:23 +03:00
anten-ka
8b4b4892a4 v2.3.1: bot install with spinner waiting for first admin + auto/manual choice 2026-04-09 10:54:06 +03:00
anten-ka
046a08fdb6 v2.3.1: admin management (auto-register first admin, /addadmin, /deladmin, menu button) 2026-04-09 10:46:50 +03:00
anten-ka
a21d2ebea2 v2.3.1: fix QR top margin, use domain instead of IP for pro-mode links 2026-04-09 00:59:42 +03:00
anten-ka
96cbd243d9 v2.3.1: promo only on gotelegram launch (once/day), not after proxy setup; bot promo with 2 hosters 2026-04-08 23:08:04 +03:00
anten-ka
3f136ec8a0 v2.3.1: promo update (2 hosters, QR codes, once-per-day display, 5sec delay) 2026-04-08 22:42:29 +03:00
anten-ka
a24d64d33c fix: stats refresh 3s instead of 1s 2026-04-08 22:31:30 +03:00
anten-ka
52912e0ead fix: stats crash (set -u + CSV header), header box, QR spacing 2026-04-08 22:21:17 +03:00
anten-ka
0dae922d1b fix: main menu 30s refresh, stats submenu flicker-free via tput 2026-04-08 22:01:02 +03:00
anten-ka
6ec2123f83 v2.3.0: Lite/Pro rebrand, submenu system, traffic stats, bot stats 2026-04-08 21:49:03 +03:00
anten-ka
364501d66d v2.2.1: redesign dashboard menu - no right borders, clean layout 2026-04-08 21:05:46 +03:00
anten-ka
f445f7a27e feat: dashboard with live status, proxy link, QR, auto-refresh 30s 2026-04-08 20:52:30 +03:00
anten-ka
6e32ca9d12 fix: telemt v3 config format ([server], [censorship], [access.users]), stealth with dns_overrides 2026-04-08 20:31:50 +03:00
anten-ka
7f81c21d8e stealth: telemt on 0.0.0.0:443, nginx on 127.0.0.1:8443, domain-based proxy link with ee-secret 2026-04-08 20:18:25 +03:00
13 changed files with 18088 additions and 16695 deletions

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Environment
.env
*.env.local
# Python
__pycache__/
*.pyc
venv/
.venv/
# Backups (contain secrets)
backups/
*.tar.gz
*.tar.gz.enc
*.sha256
# Temp
/tmp/
*.tmp
*.swp
# IDE
.vscode/
.idea/
*.code-workspace
# OS
.DS_Store
Thumbs.db

140
bootstrap.sh Normal file → Executable file
View File

@@ -1,63 +1,115 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.2.1 — Bootstrap installer (private repo) # GoTelegram Pro — Bootstrap installer for private repo
# Downloads all files and launches install.sh
set -euo pipefail set -euo pipefail
TOKEN="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"
REPO="anten-ka/gotelegram_pro" REPO="anten-ka/gotelegram_pro"
BRANCH="test" BRANCH="test"
API="https://api.github.com/repos/$REPO" PAT="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"
INSTALL_DIR="/opt/gotelegram" INSTALL_DIR="/opt/gotelegram"
API="https://api.github.com/repos/${REPO}/contents"
echo -e "\033[1;36m" RED='\033[0;31m'
echo "╔══════════════════════════════════════════╗" GREEN='\033[0;32m'
echo "║ GoTelegram v2.2.1 — Установка ║" CYAN='\033[0;36m'
echo "╚══════════════════════════════════════════╝" YELLOW='\033[1;33m'
echo -e "\033[0m" NC='\033[0m'
BOLD='\033[1m'
# Функция загрузки файла из приватного репо echo ""
dl() { echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
local path="$1" dest="$2" echo -e " ${YELLOW}${NC} ${BOLD}GoTelegram Pro — Установка${NC} ${YELLOW}${NC}"
mkdir -p "$(dirname "$dest")" echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
curl -sfL -H "Authorization: token $TOKEN" \ echo -e " ${YELLOW}${NC} MTProxy менеджер с Telegram-ботом ${YELLOW}${NC}"
-H "Accept: application/vnd.github.v3.raw" \ echo -e " ${YELLOW}${NC} Stealth-режим, 1800+ шаблонов сайтов ${YELLOW}${NC}"
"$API/contents/$path?ref=$BRANCH" -o "$dest" echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
sed -i 's/\r$//' "$dest" echo ""
echo "$path"
# Check root
if [ "$(id -u)" -ne 0 ]; then
echo -e " ${RED}${NC} Запустите от root: ${CYAN}sudo bash bootstrap.sh${NC}"
exit 1
fi
# Check dependencies
for cmd in curl jq; do
if ! command -v "$cmd" &>/dev/null; then
echo -e " ${CYAN}${NC} Установка $cmd..."
apt-get update -qq && apt-get install -y -qq "$cmd" >/dev/null 2>&1
fi
done
download_file() {
local remote_path="$1"
local local_path="$2"
local dir
dir=$(dirname "$local_path")
mkdir -p "$dir"
local http_code
http_code=$(curl -sL -w "%{http_code}" -o "$local_path" \
-H "Authorization: token ${PAT}" \
-H "Accept: application/vnd.github.raw" \
"${API}/${remote_path}?ref=${BRANCH}")
if [ "$http_code" != "200" ]; then
echo -e " ${RED}${NC} Ошибка загрузки ${remote_path} (HTTP ${http_code})"
return 1
fi
return 0
} }
echo "📦 Скачиваю файлы..." # File list
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/gotelegram-bot" FILES=(
"install.sh"
"install_gotelegram_bot.sh"
"templates_catalog.json"
"lib/common.sh"
"lib/telemt.sh"
"lib/telemt_config.sh"
"lib/backup.sh"
"lib/website.sh"
"lib/templates_catalog.sh"
"lib/stats.sh"
"gotelegram-bot/bot.py"
"gotelegram-bot/config.example.env"
"gotelegram-bot/requirements.txt"
"gotelegram-bot/README.md"
)
# Основные файлы echo -e " ${CYAN}${NC} Загрузка файлов в ${INSTALL_DIR}..."
dl "install.sh" "$INSTALL_DIR/install.sh" mkdir -p "${INSTALL_DIR}/lib" "${INSTALL_DIR}/gotelegram-bot"
dl "install_gotelegram_bot.sh" "$INSTALL_DIR/install_gotelegram_bot.sh"
dl "templates_catalog.json" "$INSTALL_DIR/templates_catalog.json"
# Библиотеки failed=0
dl "lib/common.sh" "$INSTALL_DIR/lib/common.sh" for f in "${FILES[@]}"; do
dl "lib/telemt.sh" "$INSTALL_DIR/lib/telemt.sh" if download_file "$f" "${INSTALL_DIR}/${f}"; then
dl "lib/telemt_config.sh" "$INSTALL_DIR/lib/telemt_config.sh" echo -e " ${GREEN}${NC} ${f}"
dl "lib/backup.sh" "$INSTALL_DIR/lib/backup.sh" else
dl "lib/website.sh" "$INSTALL_DIR/lib/website.sh" failed=$((failed + 1))
dl "lib/templates_catalog.sh" "$INSTALL_DIR/lib/templates_catalog.sh" fi
done
# Бот if [ "$failed" -gt 0 ]; then
dl "gotelegram-bot/bot.py" "$INSTALL_DIR/gotelegram-bot/bot.py" echo ""
dl "gotelegram-bot/config.example.env" "$INSTALL_DIR/gotelegram-bot/config.example.env" echo -e " ${RED}${NC} Не удалось загрузить ${failed} файл(ов)"
dl "gotelegram-bot/requirements.txt" "$INSTALL_DIR/gotelegram-bot/requirements.txt" echo -e " ${YELLOW}Проверьте токен доступа и подключение к сети${NC}"
dl "gotelegram-bot/README.md" "$INSTALL_DIR/gotelegram-bot/README.md" exit 1
fi
# Права # Fix permissions and line endings
chmod +x "$INSTALL_DIR/install.sh" "$INSTALL_DIR/install_gotelegram_bot.sh" echo -e " ${CYAN}${NC} Настройка прав..."
chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh"
chmod +x "${INSTALL_DIR}"/lib/*.sh
sed -i 's/\r$//' "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh" "${INSTALL_DIR}"/lib/*.sh 2>/dev/null
# Команда gotelegram # Create symlink
ln -sf "$INSTALL_DIR/install.sh" /usr/local/bin/gotelegram ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
chmod +x /usr/local/bin/gotelegram echo -e " ${GREEN}${NC} Команда ${CYAN}gotelegram${NC} доступна"
echo "" echo ""
echo -e "\033[1;32m✅ Все 13 файлов скачаны в $INSTALL_DIR\033[0m" echo -e " ${GREEN}${NC} Установка завершена! Запуск..."
echo -e "\033[1;33m💡 Команда для запуска меню: gotelegram\033[0m"
echo "" echo ""
# Запускаем меню # Launch
exec bash "$INSTALL_DIR/install.sh" exec bash "${INSTALL_DIR}/install.sh"

View File

@@ -6,14 +6,17 @@ Uses python-telegram-bot v21+
""" """
import asyncio import asyncio
import csv
import html import html
import json import json
import logging import logging
import os import os
import re import re
import subprocess import subprocess
import time
import toml import toml
from datetime import datetime from datetime import datetime
from io import StringIO
from pathlib import Path from pathlib import Path
from typing import Tuple, Optional, List, Dict, Any from typing import Tuple, Optional, List, Dict, Any
@@ -47,7 +50,7 @@ logger = logging.getLogger(__name__)
# CONFIGURATION # CONFIGURATION
# ============================================================================ # ============================================================================
GOTELEGRAM_VERSION = "2.2.0" GOTELEGRAM_VERSION = "2.3.1"
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json" GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
TELEMT_CONFIG = "/etc/telemt/config.toml" TELEMT_CONFIG = "/etc/telemt/config.toml"
TELEMT_SERVICE = "telemt" TELEMT_SERVICE = "telemt"
@@ -55,21 +58,75 @@ WEBSITE_ROOT = "/var/www/gotelegram-site"
BACKUP_DIR = "/opt/gotelegram/backups" BACKUP_DIR = "/opt/gotelegram/backups"
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json" TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
PROMO_LINK = "https://vk.cc/ct29NQ" PROMO_LINK_1 = "https://vk.cc/ct29NQ"
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f" TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
BOT_TOKEN = os.getenv("BOT_TOKEN") BOT_TOKEN = os.getenv("BOT_TOKEN")
ALLOWED_IDS_STR = os.getenv("ALLOWED_IDS", "") ENV_FILE = "/opt/gotelegram-bot/.env"
ALLOWED_IDS: set = set()
for _id_str in ALLOWED_IDS_STR.split(","):
_id_str = _id_str.strip()
if _id_str:
try:
ALLOWED_IDS.add(int(_id_str))
except ValueError:
logging.warning(f"Invalid ALLOWED_IDS entry: {_id_str}")
QUICK_DOMAINS = [ # ── Загрузка ALLOWED_IDS ────────────────────────────────────────────────────
# Поддерживает запятую, пробел, или их комбинацию как разделитель
ALLOWED_IDS: set = set()
_WAITING_FOR_ADMIN = False # True если список пуст → ждём первого админа
def _load_allowed_ids() -> None:
"""Загрузить ALLOWED_IDS из переменной окружения."""
global ALLOWED_IDS, _WAITING_FOR_ADMIN
raw = os.getenv("ALLOWED_IDS", "")
ALLOWED_IDS = set()
# Разделители: запятая, пробел, или оба
for part in re.split(r'[,\s]+', raw):
part = part.strip()
if part:
try:
ALLOWED_IDS.add(int(part))
except ValueError:
logging.warning(f"Invalid ALLOWED_IDS entry: {part}")
_WAITING_FOR_ADMIN = len(ALLOWED_IDS) == 0
def _save_allowed_ids() -> None:
"""Сохранить ALLOWED_IDS в .env файл и обновить os.environ."""
global _WAITING_FOR_ADMIN
ids_str = ",".join(str(i) for i in sorted(ALLOWED_IDS))
os.environ["ALLOWED_IDS"] = ids_str
_WAITING_FOR_ADMIN = len(ALLOWED_IDS) == 0
if not os.path.exists(ENV_FILE):
return
try:
with open(ENV_FILE, "r") as f:
lines = f.readlines()
found = False
new_lines = []
for line in lines:
if line.strip().startswith("ALLOWED_IDS="):
if ids_str:
new_lines.append(f"ALLOWED_IDS={ids_str}\n")
# Если пусто — удаляем строку
found = True
else:
new_lines.append(line)
if not found and ids_str:
new_lines.append(f"ALLOWED_IDS={ids_str}\n")
with open(ENV_FILE, "w") as f:
f.writelines(new_lines)
logger.info(f"ALLOWED_IDS updated in .env: {ids_str or '(empty)'}")
except OSError as e:
logger.error(f"Failed to update .env: {e}")
_load_allowed_ids()
LITE_DOMAINS = [
"google.com", "google.com",
"microsoft.com", "microsoft.com",
"cloudflare.com", "cloudflare.com",
@@ -225,21 +282,41 @@ async def check_old_container() -> Optional[str]:
def is_user_allowed(user_id: int) -> bool: def is_user_allowed(user_id: int) -> bool:
"""Check if user ID is in ALLOWED_IDS.""" """Check if user ID is in ALLOWED_IDS. If list is empty — waiting for admin."""
if not ALLOWED_IDS: if _WAITING_FOR_ADMIN:
return True return False # Никому не даём доступ пока не назначен админ
return user_id in ALLOWED_IDS return user_id in ALLOWED_IDS
def add_admin(user_id: int) -> None:
"""Добавить администратора и сохранить в .env."""
ALLOWED_IDS.add(user_id)
_save_allowed_ids()
logger.info(f"Admin added: {user_id}")
def remove_admin(user_id: int) -> None:
"""Убрать администратора и сохранить в .env."""
ALLOWED_IDS.discard(user_id)
_save_allowed_ids()
logger.info(f"Admin removed: {user_id}")
async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Check authorization and send error if not allowed.""" """Check authorization and send error if not allowed."""
if not is_user_allowed(update.effective_user.id): user_id = update.effective_user.id
await update.message.reply_text(
f"Access denied. Your ID: {update.effective_user.id}" # Режим ожидания первого админа — обрабатывается в cmd_start
) if _WAITING_FOR_ADMIN:
logger.warning( return False
f"Unauthorized access attempt from user {update.effective_user.id}"
) if not is_user_allowed(user_id):
if update.message:
await update.message.reply_text(
f"⛔ Доступ запрещён.\nВаш ID: <code>{user_id}</code>",
parse_mode="HTML",
)
logger.warning(f"Unauthorized access attempt from user {user_id}")
return False return False
return True return True
@@ -274,13 +351,19 @@ def get_main_menu() -> InlineKeyboardMarkup:
], ],
[ [
InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"), InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"),
InlineKeyboardButton("🎁 Promo", callback_data="menu_promo"), InlineKeyboardButton("🎁 Промо", callback_data="menu_promo"),
], ],
[ [
InlineKeyboardButton("📊 Traffic Stats", callback_data="menu_stats"),
InlineKeyboardButton("🗑️ Remove", callback_data="menu_remove"), InlineKeyboardButton("🗑️ Remove", callback_data="menu_remove"),
],
[
InlineKeyboardButton("👤 Админы", callback_data="menu_admins"),
InlineKeyboardButton(" Credits", callback_data="menu_credits"), InlineKeyboardButton(" Credits", callback_data="menu_credits"),
], ],
[InlineKeyboardButton("❌ Close", callback_data="close_menu")], [
InlineKeyboardButton("❌ Close", callback_data="close_menu"),
],
] ]
return InlineKeyboardMarkup(buttons) return InlineKeyboardMarkup(buttons)
@@ -291,8 +374,37 @@ def get_main_menu() -> InlineKeyboardMarkup:
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Start command - show main menu.""" """Start command - show main menu, promo once per day.
if not await require_auth(update, context):
Если ALLOWED_IDS пуст — режим авто-регистрации первого админа.
"""
user = update.effective_user
user_id = user.id
# ── Режим ожидания первого админа ──
if _WAITING_FOR_ADMIN:
name = user.full_name or user.username or str(user_id)
text = (
f"<b>👋 Привет, {html.escape(name)}!</b>\n\n"
f"Бот ещё не настроен.\n"
f"Ваш Telegram ID: <code>{user_id}</code>\n\n"
f"Назначить вас администратором?"
)
keyboard = InlineKeyboardMarkup([
[
InlineKeyboardButton("✅ Да", callback_data=f"admin_confirm_{user_id}"),
InlineKeyboardButton("❌ Нет", callback_data="admin_cancel"),
]
])
await update.message.reply_text(text, reply_markup=keyboard, parse_mode="HTML")
return
# ── Проверка доступа ──
if not is_user_allowed(user_id):
await update.message.reply_text(
f"⛔ Доступ запрещён.\nВаш ID: <code>{user_id}</code>",
parse_mode="HTML",
)
return return
welcome = ( welcome = (
@@ -305,6 +417,13 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
welcome, reply_markup=get_main_menu(), parse_mode="HTML" welcome, reply_markup=get_main_menu(), parse_mode="HTML"
) )
# Промо раз в сутки
if should_show_promo_bot():
mark_promo_shown_bot()
await update.message.reply_text(
get_promo_text(), parse_mode="HTML"
)
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Help command - show available commands.""" """Help command - show available commands."""
@@ -312,12 +431,14 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
return return
help_text = ( help_text = (
"<b>GoTelegram Bot Commands</b>\n\n" "<b>GoTelegram Bot — Команды</b>\n\n"
"/start - Show main menu\n" "/start — Главное меню\n"
"/help - Show this help message\n" "/help — Эта справка\n"
"/status - Quick status check\n" "/status — Быстрый статус\n"
"/logs - Show recent logs\n\n" "/logs — Последние логи\n"
"Use the inline menu for all other operations." "/addadmin ID — Добавить админа\n"
"/deladmin ID — Удалить админа\n\n"
"Используйте кнопки меню для остальных операций."
) )
await update.message.reply_text(help_text, parse_mode="HTML") await update.message.reply_text(help_text, parse_mode="HTML")
@@ -403,19 +524,135 @@ async def get_status_text() -> str:
return "\n".join(lines) return "\n".join(lines)
async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def get_traffic_stats() -> str:
"""Status callback.""" """Get formatted traffic statistics."""
# Read current snapshot
current_file = "/run/gotelegram/stats_current.json"
history_file = "/opt/gotelegram/stats_history.csv"
try:
with open(current_file, "r") as f:
current = json.load(f)
except Exception:
return "📊 <b>Статистика</b>\n\n<i>Данные недоступны. Убедитесь что модуль статистики включён.</i>"
# Read history
history = []
try:
with open(history_file, "r") as f:
reader = csv.reader(f)
for row in reader:
if len(row) >= 3:
history.append({
"ts": int(row[0]),
"proxy": int(row[1]),
"site": int(row[2]),
})
except Exception:
pass
now = int(time.time())
def format_bytes(b):
if b < 1024:
return f"{b} B"
if b < 1048576:
return f"{b/1024:.1f} KB"
if b < 1073741824:
return f"{b/1048576:.1f} MB"
return f"{b/1073741824:.1f} GB"
def format_rate(bps):
if bps < 1024:
return f"{bps:.0f} B/s"
if bps < 1048576:
return f"{bps/1024:.1f} KB/s"
return f"{bps/1048576:.1f} MB/s"
def calc_for_period(secs, key):
target_ts = now - secs
# Find closest snapshot to target_ts
closest = None
for h in history:
if h["ts"] <= target_ts:
if closest is None or h["ts"] > closest["ts"]:
closest = h
if closest is None:
return "", ""
current_val = current.get(f"{key}_bytes", 0)
diff = current_val - closest[key]
if diff < 0:
diff = 0
elapsed = now - closest["ts"]
if elapsed <= 0:
elapsed = 1
rate = diff / elapsed
return format_bytes(diff), format_rate(rate)
periods = [
("1 мин", 60),
("5 мин", 300),
("60 мин", 3600),
("1 день", 86400),
("7 дней", 604800),
("30 дней", 2592000),
("365 дней", 31536000),
]
lines = ["📊 <b>Статистика трафика</b>\n"]
for label, key in [("Proxy (telemt)", "proxy"), ("Сайт (nginx)", "site")]:
lines.append(f"\n<b>{label}:</b>")
lines.append("<pre>")
lines.append(f"{'Период':<10}{'Трафик':>10}{'Скорость':>10}")
lines.append("" * 36)
for name, secs in periods:
total, rate = calc_for_period(secs, key)
lines.append(f"{name:<10}{total:>10}{rate:>10}")
lines.append("</pre>")
return "\n".join(lines)
async def cb_menu_stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Show traffic statistics."""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
await safe_edit_message(query,"⏳ Checking status...") stats_text = await get_traffic_stats()
status_text = await get_status_text() keyboard = [
keyboard = InlineKeyboardMarkup( [InlineKeyboardButton("🔄 Обновить", callback_data="menu_stats")],
[[InlineKeyboardButton("« Back", callback_data="menu_main")]] [InlineKeyboardButton("« Меню", callback_data="menu_main")],
]
await safe_edit_message(
query,
stats_text,
reply_markup=InlineKeyboardMarkup(keyboard),
parse_mode="HTML",
) )
await safe_edit_message(query,
status_text, reply_markup=keyboard, parse_mode="HTML"
async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Status callback — show detailed proxy/server status."""
query = update.callback_query
await query.answer()
if not await require_auth(update, context):
return
text = await get_status_text()
keyboard = [
[InlineKeyboardButton("🔄 Обновить", callback_data="menu_status")],
[InlineKeyboardButton("« Меню", callback_data="menu_main")],
]
await safe_edit_message(
query,
text,
reply_markup=InlineKeyboardMarkup(keyboard),
parse_mode="HTML",
) )
@@ -427,8 +664,8 @@ async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
def get_install_mode_menu() -> InlineKeyboardMarkup: def get_install_mode_menu() -> InlineKeyboardMarkup:
"""Install mode selection menu.""" """Install mode selection menu."""
buttons = [ buttons = [
[InlineKeyboardButton("Quick Mode", callback_data="install_mode_quick")], [InlineKeyboardButton("Lite", callback_data="install_mode_lite")],
[InlineKeyboardButton("🔒 Stealth Mode", callback_data="install_mode_stealth")], [InlineKeyboardButton("🛡 Pro", callback_data="install_mode_pro")],
[InlineKeyboardButton("« Back", callback_data="menu_main")], [InlineKeyboardButton("« Back", callback_data="menu_main")],
] ]
return InlineKeyboardMarkup(buttons) return InlineKeyboardMarkup(buttons)
@@ -452,7 +689,7 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
) )
buttons = [ buttons = [
[InlineKeyboardButton("🔄 Migrate from v1", callback_data="install_migrate")], [InlineKeyboardButton("🔄 Migrate from v1", callback_data="install_migrate")],
[InlineKeyboardButton("✨ Fresh Install", callback_data="install_mode_quick")], [InlineKeyboardButton("✨ Fresh Install", callback_data="install_mode_lite")],
[InlineKeyboardButton("« Back", callback_data="menu_main")], [InlineKeyboardButton("« Back", callback_data="menu_main")],
] ]
keyboard = InlineKeyboardMarkup(buttons) keyboard = InlineKeyboardMarkup(buttons)
@@ -465,39 +702,39 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
) )
async def cb_install_mode_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_install_mode_lite(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Quick mode domain selection.""" """Lite mode domain selection."""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
# Show domains with pagination (4 per row, 2 rows) # Show domains with pagination (4 per row, 2 rows)
buttons = [] buttons = []
for i in range(0, len(QUICK_DOMAINS), 2): for i in range(0, len(LITE_DOMAINS), 2):
row = [] row = []
for j in range(2): for j in range(2):
if i + j < len(QUICK_DOMAINS): if i + j < len(LITE_DOMAINS):
domain = QUICK_DOMAINS[i + j] domain = LITE_DOMAINS[i + j]
row.append( row.append(
InlineKeyboardButton( InlineKeyboardButton(
domain, callback_data=f"quick_dom_{i+j}" domain, callback_data=f"lite_dom_{i+j}"
) )
) )
buttons.append(row) buttons.append(row)
buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")]) buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")])
text = "Select a domain for quick mode:" text = "Select a domain for Lite mode:"
keyboard = InlineKeyboardMarkup(buttons) keyboard = InlineKeyboardMarkup(buttons)
await safe_edit_message(query,text, reply_markup=keyboard) await safe_edit_message(query,text, reply_markup=keyboard)
async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Quick domain selection callback.""" """Lite domain selection callback."""
query = update.callback_query query = update.callback_query
data = query.data data = query.data
try: try:
domain_idx = int(data.split("_")[-1]) domain_idx = int(data.split("_")[-1])
domain = QUICK_DOMAINS[domain_idx] domain = LITE_DOMAINS[domain_idx]
except (ValueError, IndexError): except (ValueError, IndexError):
await query.answer("Invalid domain selection") await query.answer("Invalid domain selection")
return return
@@ -507,7 +744,7 @@ async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
# Simulate installation (in real scenario, call install script) # Simulate installation (in real scenario, call install script)
config = { config = {
"mode": "quick", "mode": "lite",
"domain": domain, "domain": domain,
"port": 443, "port": 443,
"installed_at": datetime.now().isoformat(), "installed_at": datetime.now().isoformat(),
@@ -515,9 +752,9 @@ async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
if save_json(GOTELEGRAM_CONFIG, config): if save_json(GOTELEGRAM_CONFIG, config):
text = ( text = (
f"✅ <b>Quick mode installed!</b>\n\n" f"✅ <b>Lite mode installed!</b>\n\n"
f"<b>Domain:</b> {domain}\n" f"<b>Domain:</b> {domain}\n"
f"<b>Mode:</b> Quick\n\n" f"<b>Mode:</b> Lite\n\n"
f"Service starting... Check status in 10 seconds." f"Service starting... Check status in 10 seconds."
) )
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
@@ -535,8 +772,8 @@ async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
) )
async def cb_install_mode_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_install_mode_pro(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Stealth mode - show template categories.""" """Pro mode - show template categories."""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
@@ -555,22 +792,22 @@ async def cb_install_mode_stealth(update: Update, context: ContextTypes.DEFAULT_
buttons.append( buttons.append(
[ [
InlineKeyboardButton( InlineKeyboardButton(
f"📁 {cat['name']}", callback_data=f"stealth_cat_{cat['id']}" f"📁 {cat['name']}", callback_data=f"pro_cat_{cat['id']}"
) )
] ]
) )
buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")]) buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")])
text = "Stealth Mode - Select Template Category:" text = "Pro Mode - Select Template Category:"
keyboard = InlineKeyboardMarkup(buttons) keyboard = InlineKeyboardMarkup(buttons)
await safe_edit_message(query,text, reply_markup=keyboard) await safe_edit_message(query,text, reply_markup=keyboard)
async def cb_stealth_category(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_pro_category(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Show templates in category.""" """Show templates in category."""
query = update.callback_query query = update.callback_query
data = query.data data = query.data
cat_id = data.removeprefix("stealth_cat_") cat_id = data.removeprefix("pro_cat_")
await query.answer() await query.answer()
@@ -597,22 +834,22 @@ async def cb_stealth_category(update: Update, context: ContextTypes.DEFAULT_TYPE
buttons.append( buttons.append(
[ [
InlineKeyboardButton( InlineKeyboardButton(
f"🎨 {tpl['name']}", callback_data=f"stealth_tpl_{tpl['id']}" f"🎨 {tpl['name']}", callback_data=f"pro_tpl_{tpl['id']}"
) )
] ]
) )
buttons.append([InlineKeyboardButton("« Back", callback_data="install_mode_stealth")]) buttons.append([InlineKeyboardButton("« Back", callback_data="install_mode_pro")])
text = f"Select template from <b>{html.escape(category['name'])}</b>:" text = f"Select template from <b>{html.escape(category['name'])}</b>:"
keyboard = InlineKeyboardMarkup(buttons) keyboard = InlineKeyboardMarkup(buttons)
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
async def cb_stealth_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_pro_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Show template preview and confirm.""" """Show template preview and confirm."""
query = update.callback_query query = update.callback_query
data = query.data data = query.data
tpl_id = data.removeprefix("stealth_tpl_") tpl_id = data.removeprefix("pro_tpl_")
await query.answer() await query.answer()
@@ -651,26 +888,26 @@ async def cb_stealth_template(update: Update, context: ContextTypes.DEFAULT_TYPE
buttons = [ buttons = [
[ [
InlineKeyboardButton( InlineKeyboardButton(
"✅ Install", callback_data=f"stealth_confirm_{tpl_id}" "✅ Install", callback_data=f"pro_confirm_{tpl_id}"
) )
], ],
[InlineKeyboardButton("« Back", callback_data="install_mode_stealth")], [InlineKeyboardButton("« Back", callback_data="install_mode_pro")],
] ]
keyboard = InlineKeyboardMarkup(buttons) keyboard = InlineKeyboardMarkup(buttons)
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Confirm and install stealth template.""" """Confirm and install pro template."""
query = update.callback_query query = update.callback_query
data = query.data data = query.data
tpl_id = data.removeprefix("stealth_confirm_") tpl_id = data.removeprefix("pro_confirm_")
await query.answer() await query.answer()
await safe_edit_message(query,"⏳ Installing template...") await safe_edit_message(query,"⏳ Installing template...")
config = { config = {
"mode": "stealth", "mode": "pro",
"template": tpl_id, "template": tpl_id,
"port": 443, "port": 443,
"installed_at": datetime.now().isoformat(), "installed_at": datetime.now().isoformat(),
@@ -678,9 +915,9 @@ async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
if save_json(GOTELEGRAM_CONFIG, config): if save_json(GOTELEGRAM_CONFIG, config):
text = ( text = (
f"✅ <b>Stealth mode installed!</b>\n\n" f"✅ <b>Pro mode installed!</b>\n\n"
f"<b>Template:</b> {html.escape(tpl_id)}\n" f"<b>Template:</b> {html.escape(tpl_id)}\n"
f"<b>Mode:</b> Stealth\n\n" f"<b>Mode:</b> Pro\n\n"
f"Service starting... Check status in 10 seconds." f"Service starting... Check status in 10 seconds."
) )
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
@@ -704,7 +941,7 @@ async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
async def get_proxy_link() -> Optional[str]: async def get_proxy_link() -> Optional[str]:
"""Generate proxy link from config.""" """Generate proxy link from config. Pro-mode uses domain + fake-TLS secret."""
config = load_json(GOTELEGRAM_CONFIG) config = load_json(GOTELEGRAM_CONFIG)
if not config: if not config:
return None return None
@@ -720,12 +957,20 @@ async def get_proxy_link() -> Optional[str]:
if not secret: if not secret:
return None return None
# Get server IP mode = config.get("mode", "lite")
domain = config.get("domain", "")
port = config.get("port", 443)
# Pro-режим: ссылка с доменом и fake-TLS секретом (ee + secret + hex domain)
if mode == "pro" and domain:
domain_hex = domain.encode().hex()
faketls_secret = f"ee{secret}{domain_hex}"
return f"tg://proxy?server={domain}&port={port}&secret={faketls_secret}"
# Lite-режим: IP
code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org") code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0" server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
port = config.get("port", 443)
return f"tg://proxy?server={server}&port={port}&secret={secret}" return f"tg://proxy?server={server}&port={port}&secret={secret}"
@@ -1074,8 +1319,8 @@ async def cb_menu_change(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.answer() await query.answer()
buttons = [ buttons = [
[InlineKeyboardButton("⚡ Switch to Quick Mode", callback_data="change_quick")], [InlineKeyboardButton("⚡ Switch to Lite Mode", callback_data="change_lite")],
[InlineKeyboardButton("🔒 Switch to Stealth Mode", callback_data="change_stealth")], [InlineKeyboardButton("🛡 Switch to Pro Mode", callback_data="change_pro")],
[InlineKeyboardButton("« Back", callback_data="menu_main")], [InlineKeyboardButton("« Back", callback_data="menu_main")],
] ]
keyboard = InlineKeyboardMarkup(buttons) keyboard = InlineKeyboardMarkup(buttons)
@@ -1084,20 +1329,20 @@ async def cb_menu_change(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
) )
async def cb_change_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_change_lite(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Switch to quick mode — show domain selection.""" """Switch to lite mode — show domain selection."""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
# Reuse the quick mode domain selection flow # Reuse the lite mode domain selection flow
await cb_install_mode_quick(update, context) await cb_install_mode_lite(update, context)
async def cb_change_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_change_pro(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Switch to stealth mode — show template categories.""" """Switch to pro mode — show template categories."""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
# Reuse the stealth mode template selection flow # Reuse the pro mode template selection flow
await cb_install_mode_stealth(update, context) await cb_install_mode_pro(update, context)
async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -1183,28 +1428,170 @@ async def cb_ssl_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
# ============================================================================
# ADMIN MANAGEMENT
# ============================================================================
async def cb_menu_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Показать список админов и кнопки управления."""
query = update.callback_query
await query.answer()
if ALLOWED_IDS:
ids_list = "\n".join(f" • <code>{uid}</code>" for uid in sorted(ALLOWED_IDS))
text = f"<b>👤 Администраторы</b>\n\n{ids_list}\n"
else:
text = "<b>👤 Администраторы</b>\n\n<i>Список пуст — доступ для всех</i>\n"
text += (
f"\nВсего: {len(ALLOWED_IDS)}\n\n"
"Чтобы <b>добавить</b> — перешлите любое сообщение от нового админа, "
"или отправьте команду:\n"
"<code>/addadmin 123456789</code>\n\n"
"Чтобы <b>удалить</b>:\n"
"<code>/deladmin 123456789</code>"
)
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("« Назад", callback_data="menu_main")],
])
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
async def cmd_addadmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/addadmin ID [ID2 ID3 ...] — добавить админа вручную."""
if not is_user_allowed(update.effective_user.id):
await update.message.reply_text(
f"⛔ Доступ запрещён.\nВаш ID: <code>{update.effective_user.id}</code>",
parse_mode="HTML",
)
return
args = context.args or []
if not args:
await update.message.reply_text(
"Использование: <code>/addadmin ID [ID2 ID3 ...]</code>\n"
"Пример: <code>/addadmin 123456789 987654321</code>",
parse_mode="HTML",
)
return
added = []
errors = []
for a in args:
a = a.strip().replace(",", "")
if not a:
continue
try:
uid = int(a)
add_admin(uid)
added.append(str(uid))
except ValueError:
errors.append(a)
parts = []
if added:
parts.append(f"✅ Добавлены: {', '.join(added)}")
if errors:
parts.append(f"❌ Ошибки: {', '.join(errors)}")
await update.message.reply_text("\n".join(parts), parse_mode="HTML")
async def cmd_deladmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""/deladmin ID — удалить админа."""
if not is_user_allowed(update.effective_user.id):
await update.message.reply_text(
f"⛔ Доступ запрещён.\nВаш ID: <code>{update.effective_user.id}</code>",
parse_mode="HTML",
)
return
args = context.args or []
if not args:
await update.message.reply_text(
"Использование: <code>/deladmin ID</code>",
parse_mode="HTML",
)
return
removed = []
for a in args:
a = a.strip().replace(",", "")
try:
uid = int(a)
if uid == update.effective_user.id:
await update.message.reply_text("⚠️ Нельзя удалить себя!")
continue
if uid in ALLOWED_IDS:
remove_admin(uid)
removed.append(str(uid))
else:
await update.message.reply_text(f"ID {uid} не найден в списке")
except ValueError:
await update.message.reply_text(f"❌ Некорректный ID: {html.escape(a)}")
if removed:
await update.message.reply_text(f"✅ Удалены: {', '.join(removed)}")
# ============================================================================ # ============================================================================
# PROMO & CREDITS # PROMO & CREDITS
# ============================================================================ # ============================================================================
def get_promo_text() -> str:
"""Return promo text with 2 hosters + donate."""
return (
"<b>💰 Хостинг #1 — скидка до 60%</b>\n"
f"<a href='{PROMO_LINK_1}'>{PROMO_LINK_1}</a>\n\n"
"<b>Промокоды:</b>\n"
" <code>OFF60</code> — 60% на первый месяц\n"
" <code>antenka20</code> — 20% + 3% за 3 мес\n"
" <code>antenka6</code> — 15% + 5% за 6 мес\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"<b>💰 Хостинг #2 — скидка до 60%</b>\n"
f"<a href='{PROMO_LINK_2}'>{PROMO_LINK_2}</a>\n\n"
"<b>Промокод:</b>\n"
" <code>OFF60</code> — 60% на первый месяц\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"<b>☕ Донат / Чаевые</b>\n"
f"<a href='{TIP_LINK}'>{TIP_LINK}</a>"
)
def should_show_promo_bot() -> bool:
"""Check if promo should be shown (once per 24h)."""
try:
if not os.path.exists(PROMO_STAMP_FILE):
return True
with open(PROMO_STAMP_FILE, "r") as f:
last_ts = int(f.read().strip())
return (int(time.time()) - last_ts) >= 86400
except (ValueError, OSError):
return True
def mark_promo_shown_bot() -> None:
"""Mark promo as shown."""
try:
os.makedirs(os.path.dirname(PROMO_STAMP_FILE), exist_ok=True)
with open(PROMO_STAMP_FILE, "w") as f:
f.write(str(int(time.time())))
except OSError:
pass
async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Promo information.""" """Promo information — always shown from menu."""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
text = (
f"<b>🎁 GoTelegram Promo</b>\n\n"
f"Share the love! Invite friends to use GoTelegram.\n\n"
f"<a href='{PROMO_LINK}'>Promo Link</a>\n\n"
f"Support development:\n"
f"<a href='{TIP_LINK}'>Send a Tip</a>"
)
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
[[InlineKeyboardButton("« Back", callback_data="menu_main")]] [[InlineKeyboardButton("« Назад", callback_data="menu_main")]]
) )
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") await safe_edit_message(query, get_promo_text(), reply_markup=keyboard, parse_mode="HTML")
async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -1290,9 +1677,43 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
query = update.callback_query query = update.callback_query
data = query.data data = query.data
# ── Авто-регистрация админа (до проверки доступа!) ──
if data.startswith("admin_confirm_"):
await query.answer()
try:
new_admin_id = int(data.split("_")[-1])
except (ValueError, IndexError):
await safe_edit_message(query, "❌ Ошибка: некорректный ID")
return
# Безопасность: только тот кто нажал кнопку может стать админом
if update.effective_user.id != new_admin_id:
await query.answer("Эта кнопка не для вас", show_alert=True)
return
# Race condition: если кто-то уже стал админом
if not _WAITING_FOR_ADMIN:
await safe_edit_message(query, " Администратор уже назначен.")
return
add_admin(new_admin_id)
await safe_edit_message(
query,
f"✅ <b>Вы назначены администратором!</b>\n\n"
f"ID: <code>{new_admin_id}</code>\n\n"
f"Нажмите /start чтобы открыть меню.",
parse_mode="HTML",
)
return
if data == "admin_cancel":
await query.answer()
await safe_edit_message(
query,
"👋 Ок. Напишите /start когда будете готовы.",
)
return
# Access control # Access control
if not is_user_allowed(update.effective_user.id): if not is_user_allowed(update.effective_user.id):
await query.answer("Access denied") await query.answer("Доступ запрещён")
return return
# Main menu # Main menu
@@ -1327,28 +1748,30 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
"menu_website": cb_menu_website, "menu_website": cb_menu_website,
"menu_promo": cb_menu_promo, "menu_promo": cb_menu_promo,
"menu_credits": cb_menu_credits, "menu_credits": cb_menu_credits,
"menu_admins": cb_menu_admins,
"menu_remove": cb_menu_remove, "menu_remove": cb_menu_remove,
"install_mode_quick": cb_install_mode_quick, "install_mode_lite": cb_install_mode_lite,
"install_mode_stealth": cb_install_mode_stealth, "install_mode_pro": cb_install_mode_pro,
"backup_create": cb_backup_create, "backup_create": cb_backup_create,
"backup_list": cb_backup_list, "backup_list": cb_backup_list,
"ssl_renew": cb_ssl_renew, "ssl_renew": cb_ssl_renew,
"ssl_status": cb_ssl_status, "ssl_status": cb_ssl_status,
"remove_confirm": cb_remove_confirm, "remove_confirm": cb_remove_confirm,
"change_quick": cb_change_quick, "change_lite": cb_change_lite,
"change_stealth": cb_change_stealth, "change_pro": cb_change_pro,
"install_migrate": cb_install_migrate, "install_migrate": cb_install_migrate,
"menu_stats": cb_menu_stats,
} }
# Pattern-based handlers # Pattern-based handlers
if data.startswith("quick_dom_"): if data.startswith("lite_dom_"):
await cb_quick_domain(update, context) await cb_lite_domain(update, context)
elif data.startswith("stealth_cat_"): elif data.startswith("pro_cat_"):
await cb_stealth_category(update, context) await cb_pro_category(update, context)
elif data.startswith("stealth_tpl_"): elif data.startswith("pro_tpl_"):
await cb_stealth_template(update, context) await cb_pro_template(update, context)
elif data.startswith("stealth_confirm_"): elif data.startswith("pro_confirm_"):
await cb_stealth_confirm(update, context) await cb_pro_confirm(update, context)
elif data.startswith("restore_idx_"): elif data.startswith("restore_idx_"):
await cb_restore_backup(update, context) await cb_restore_backup(update, context)
elif data in handlers: elif data in handlers:
@@ -1386,6 +1809,8 @@ def main() -> None:
application.add_handler(CommandHandler("help", cmd_help)) application.add_handler(CommandHandler("help", cmd_help))
application.add_handler(CommandHandler("status", cmd_status)) application.add_handler(CommandHandler("status", cmd_status))
application.add_handler(CommandHandler("logs", cmd_logs)) application.add_handler(CommandHandler("logs", cmd_logs))
application.add_handler(CommandHandler("addadmin", cmd_addadmin))
application.add_handler(CommandHandler("deladmin", cmd_deladmin))
# Callback query handler (buttons) # Callback query handler (buttons)
application.add_handler(CallbackQueryHandler(handle_callback)) application.add_handler(CallbackQueryHandler(handle_callback))

728
install.sh Normal file → Executable file
View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
# GoTelegram v2.2.1 — MTProxy на ядре telemt (Rust + Tokio) # GoTelegram v2.3.0 — MTProxy на ядре telemt (Rust + Tokio)
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance # Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance
# #
# Установка: # Установка:
@@ -20,48 +20,225 @@ source "$LIB_DIR/telemt_config.sh"
source "$LIB_DIR/website.sh" source "$LIB_DIR/website.sh"
source "$LIB_DIR/templates_catalog.sh" source "$LIB_DIR/templates_catalog.sh"
source "$LIB_DIR/backup.sh" source "$LIB_DIR/backup.sh"
[ -f "$LIB_DIR/stats.sh" ] && source "$LIB_DIR/stats.sh"
# ── Главное меню ───────────────────────────────────────────────────────────── # ── Главное меню (Compact Dashboard + 5 Top-Level Items) ──────────────────────
show_main_menu() { show_main_menu() {
local proxy_status bot_status local proxy_status bot_status nginx_st mode domain secret port ip link ssl_expiry
proxy_status=$(telemt_status) proxy_status=$(telemt_status)
bot_status=$(bot_service_status) bot_status=$(bot_service_status)
nginx_st=$(nginx_status 2>/dev/null || echo "stopped")
mode=$(config_get mode 2>/dev/null || echo "—")
domain=$(config_get domain 2>/dev/null || echo "")
secret=$(get_config_value secret 2>/dev/null || echo "")
port=$(get_config_value port 2>/dev/null || echo "443")
ip=$(get_server_ip 2>/dev/null || echo "N/A")
local proxy_badge bot_badge local W=54
case "$proxy_status" in local line; line=$(printf '━%.0s' $(seq 1 $W))
running) proxy_badge="${GREEN}● Работает${NC}" ;; local line2; line2=$(printf '─%.0s' $(seq 1 $W))
stopped) proxy_badge="${YELLOW}○ Остановлен${NC}" ;;
*) proxy_badge="${RED}Не установлен${NC}" ;;
esac
case "$bot_status" in
running) bot_badge="${GREEN}● Бот работает${NC}" ;;
stopped) bot_badge="${YELLOW}○ Бот остановлен${NC}" ;;
*) bot_badge="${DIM}нет${NC}" ;;
esac
# ── Заголовок (без правого бордера — ANSI ломает выравнивание) ──
echo "" echo ""
echo -e " ${BOLD}${WHITE}Главное меню${NC} │ Proxy: ${proxy_badge}${bot_badge}" echo -e " ${BOLD}${CYAN}${line}${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" echo -e " ${BOLD}${WHITE} GoTelegram v${GOTELEGRAM_VERSION}${NC} ${DIM}— Панель управления${NC}"
echo -e " ${DIM}── Прокси ──${NC}" echo -e " ${BOLD}${CYAN}${line}${NC}"
echo -e " ${CYAN} 1)${NC} 🔧 Установить / Обновить прокси"
echo -e " ${CYAN} 2)${NC} 📊 Статус" # ── Здоровье сервисов ──
echo -e " ${CYAN} 3)${NC} 🔗 Ссылка для подключения" echo ""
echo -e " ${CYAN} 4)${NC} 📤 Поделиться ключом" echo -e " ${DIM}${line2}${NC}"
echo -e " ${CYAN} 5)${NC} 🔄 Перезапуск"
echo -e " ${CYAN} 6)${NC} 📋 Логи" # Proxy
echo -e " ${CYAN} 7)${NC} 🎭 Сменить режим / шаблон" local proxy_icon proxy_color
echo -e " ${DIM}── Управление ──${NC}" case "$proxy_status" in
echo -e " ${CYAN} 8)${NC} 💾 Бекап конфигурации" running) proxy_icon="●"; proxy_color="${GREEN}" ;;
echo -e " ${CYAN} 9)${NC} 📦 Восстановить из бекапа" stopped) proxy_icon="○"; proxy_color="${YELLOW}" ;;
echo -e " ${CYAN}10)${NC} ⬆️ Обновить telemt" *) proxy_icon="✗"; proxy_color="${RED}" ;;
echo -e " ${CYAN}11)${NC} 🌐 Управление сайтом (SSL)" esac
echo -e " ${DIM}── Бот и прочее ──${NC}" echo -e " ${proxy_color}${proxy_icon}${NC} Прокси ${proxy_color}${proxy_status}${NC} ${DIM}(telemt ${mode})${NC}"
echo -e " ${CYAN}12)${NC} 🤖 Telegram-бот"
echo -e " ${CYAN}13)${NC} 🗑 Удалить всё" # nginx
echo -e " ${CYAN}14)${NC} 🏷 Промо" local nginx_icon nginx_color
echo -e " ${CYAN} 0)${NC} 🚪 Выход" case "$nginx_st" in
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" running) nginx_icon="●"; nginx_color="${GREEN}" ;;
echo -ne " ${WHITE}Выбор:${NC} " *) nginx_icon="✗"; nginx_color="${RED}" ;;
esac
echo -e " ${nginx_icon}${nginx_color}${NC} nginx ${nginx_color}${nginx_st}${NC} ${DIM}(127.0.0.1:8443)${NC}"
# Site (pro)
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
local site_icon site_color
if curl -sk --max-time 3 "https://${domain}/" -o /dev/null 2>/dev/null; then
site_icon="●"; site_color="${GREEN}"
else
site_icon="✗"; site_color="${RED}"
fi
echo -e " ${site_color}${site_icon}${NC} Сайт ${site_color}https://${domain}${NC}"
ssl_expiry=$(get_ssl_expiry "$domain" 2>/dev/null || echo "N/A")
echo -e " ${GREEN}${NC} SSL ${DIM}до ${ssl_expiry}${NC}"
fi
# Bot
case "$bot_status" in
running) echo -e " ${GREEN}${NC} Бот ${GREEN}running${NC}" ;;
stopped) echo -e " ${YELLOW}${NC} Бот ${YELLOW}stopped${NC}" ;;
esac
echo -e " ${DIM}${line2}${NC}"
# ── Сетевые параметры ──
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC} ${WHITE}Порт:${NC} ${CYAN}${port}${NC} ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
if [ -n "$domain" ]; then
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
fi
echo -e " ${DIM}${line2}${NC}"
# ── Прокси-ссылка + QR ──
if [ -n "$secret" ] && [ "$proxy_status" = "running" ]; then
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
local raw_secret faketls_secret domain_hex
raw_secret="$secret"
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
faketls_secret="ee${raw_secret}${domain_hex}"
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
else
link="tg://proxy?server=${ip}&port=${port}&secret=${secret}"
fi
echo -e " ${BOLD}${WHITE}Ссылка для Telegram:${NC}"
echo -e " ${GREEN}${link}${NC}"
if command -v qrencode &>/dev/null; then
echo ""
qrencode -t UTF8 -m 2 "$link" 2>/dev/null | while IFS= read -r qr_line; do
echo " ${qr_line}"
done
echo ""
fi
else
echo -e " ${DIM}Прокси не настроен. Выберите пункт 1.${NC}"
echo ""
fi
# ── Меню ──
echo -e " ${DIM}${line2}${NC}"
echo -e " ${CYAN}1${NC}) Прокси ▸"
echo -e " ${CYAN}2${NC}) Статистика ▸"
echo -e " ${CYAN}3${NC}) Управление ▸"
echo -e " ${CYAN}4${NC}) Telegram-бот ▸"
echo -e " ${CYAN}5${NC}) О программе ▸"
echo -e " ${CYAN}0${NC}) ${DIM}Выход${NC}"
echo -e " ${DIM}${line2}${NC}"
echo -e " ${DIM}Обновление через 30 сек${NC}"
echo -ne " ${WHITE}${NC}"
}
# ── Подменю: Прокси ──────────────────────────────────────────────────────────
submenu_proxy() {
while true; do
echo ""
echo -e " ${BOLD}${WHITE}🚀 ПРОКСИ${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${CYAN}1${NC}) Установить / Обновить"
echo -e " ${CYAN}2${NC}) Статус подробно"
echo -e " ${CYAN}3${NC}) Скопировать ссылку"
echo -e " ${CYAN}4${NC}) Поделиться ключом"
echo -e " ${CYAN}5${NC}) Перезапуск"
echo -e " ${CYAN}6${NC}) Логи"
echo -e " ${CYAN}7${NC}) Сменить режим / шаблон"
echo -e " ${CYAN}0${NC}) « Назад"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -ne " ${WHITE}Выбор:${NC} "
read -r ch
case "$ch" in
1) menu_install ;;
2) menu_status ;;
3) menu_link ;;
4) menu_share ;;
5) menu_restart ;;
6) menu_logs ;;
7) menu_change_mode ;;
0) break ;;
*) log_error "Неверный выбор" ;;
esac
echo ""
echo -ne " ${DIM}Нажмите Enter...${NC}"
read -r
done
}
# ── Подменю: Управление ──────────────────────────────────────────────────────
submenu_manage() {
while true; do
echo ""
echo -e " ${BOLD}${WHITE}⚙️ УПРАВЛЕНИЕ${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${CYAN}1${NC}) Бекап"
echo -e " ${CYAN}2${NC}) Восстановить"
echo -e " ${CYAN}3${NC}) Обновить telemt"
echo -e " ${CYAN}4${NC}) Сайт / SSL"
echo -e " ${CYAN}5${NC}) Удалить"
echo -e " ${CYAN}0${NC}) « Назад"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -ne " ${WHITE}Выбор:${NC} "
read -r ch
case "$ch" in
1) interactive_backup ;;
2) interactive_restore ;;
3) update_telemt ;;
4) menu_website ;;
5) menu_remove ;;
0) break ;;
*) log_error "Неверный выбор" ;;
esac
echo ""
echo -ne " ${DIM}Нажмите Enter...${NC}"
read -r
done
}
# ── Подменю: О программе ─────────────────────────────────────────────────────
submenu_about() {
while true; do
echo ""
echo -e " ${BOLD}${WHITE} О ПРОГРАММЕ${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${CYAN}1${NC}) Информация о версии"
echo -e " ${CYAN}2${NC}) Промо / Донат"
echo -e " ${CYAN}0${NC}) « Назад"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -ne " ${WHITE}Выбор:${NC} "
read -r ch
case "$ch" in
1) menu_version ;;
2) menu_promo ;;
0) break ;;
*) log_error "Неверный выбор" ;;
esac
echo ""
echo -ne " ${DIM}Нажмите Enter...${NC}"
read -r
done
}
# ── Информация о версии ──────────────────────────────────────────────────────
menu_version() {
echo ""
echo -e " ${BOLD}${WHITE}🔍 Информация${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${WHITE}GoTelegram:${NC} v${GOTELEGRAM_VERSION}"
echo -e " ${WHITE}Ядро:${NC} telemt (Rust + Tokio)"
echo -e " ${WHITE}Технология:${NC} Anti-DPI, Fake TLS, TCP Splice"
echo -e " ${WHITE}Лицензия:${NC} MIT"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
} }
# ── Установка: выбор режима ────────────────────────────────────────────────── # ── Установка: выбор режима ──────────────────────────────────────────────────
@@ -80,29 +257,29 @@ menu_install() {
echo "" echo ""
echo -e " ${BOLD}${WHITE}🎭 Выберите режим маскировки:${NC}" echo -e " ${BOLD}${WHITE}🎭 Выберите режим маскировки:${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${CYAN}1)${NC} ${GREEN}Quick${NC} — маскировка под популярный сайт" echo -e " ${CYAN}1)${NC} ${GREEN}Lite${NC} — маскировка под популярный сайт"
echo -e " ${DIM}Быстро, без домена. telemt маскирует трафик${NC}" echo -e " ${DIM}Быстро, без домена. telemt маскирует трафик${NC}"
echo -e " ${DIM}под выбранный сайт (google.com и т.д.)${NC}" echo -e " ${DIM}под выбранный сайт (google.com и т.д.)${NC}"
echo "" echo ""
echo -e " ${CYAN}2)${NC} ${MAGENTA}🛡 Stealth${NC} — свой сайт + полная маскировка" echo -e " ${CYAN}2)${NC} ${MAGENTA}🛡 Pro${NC} — свой сайт + полная маскировка"
echo -e " ${DIM}nginx + SSL + HTML-шаблон + telemt.${NC}" echo -e " ${DIM}nginx + SSL + HTML-шаблон + telemt.${NC}"
echo -e " ${DIM}DPI видит реальный сайт с реальным сертификатом.${NC}" echo -e " ${DIM}DPI видит реальный сайт с реальным сертификатом.${NC}"
echo -e " ${DIM}Требует: домен, направленный на этот сервер.${NC}" echo -e " ${DIM}Требует: домен, направленный на этот сервер.{{NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -ne " ${WHITE}Выбор (1/2):${NC} " echo -ne " ${WHITE}Выбор (1/2):${NC} "
read -r mode_choice read -r mode_choice
mode_choice="${mode_choice:-}" mode_choice="${mode_choice:-}"
case "$mode_choice" in case "$mode_choice" in
1) install_quick_mode ;; 1) install_lite_mode ;;
2) install_stealth_mode ;; 2) install_pro_mode ;;
*) log_error "Неверный выбор: ${mode_choice:-<пусто>}" ;; *) log_error "Неверный выбор: ${mode_choice:-<пусто>}" ;;
esac esac
} }
# ── Quick-режим ────────────────────────────────────────────────────────────── # ── Lite-режим ──────────────────────────────────────────────────────────────
install_quick_mode() { install_lite_mode() {
log_step "Установка Quick-режима" log_step "Установка Lite-режима"
# Выбор домена # Выбор домена
local domain local domain
@@ -126,7 +303,7 @@ install_quick_mode() {
echo -e " IP: ${CYAN}${ip}${NC}" echo -e " IP: ${CYAN}${ip}${NC}"
echo -e " Порт: ${CYAN}${port}${NC}" echo -e " Порт: ${CYAN}${port}${NC}"
echo -e " Маскировка: ${CYAN}${domain}${NC}" echo -e " Маскировка: ${CYAN}${domain}${NC}"
echo -e " Режим: ${GREEN}Quick${NC}" echo -e " Режим: ${GREEN}Lite${NC}"
echo "" echo ""
if ! confirm "Установить прокси?"; then if ! confirm "Установить прокси?"; then
@@ -138,7 +315,7 @@ install_quick_mode() {
install_telemt_full || return install_telemt_full || return
# Генерируем конфиг telemt # Генерируем конфиг telemt
generate_telemt_toml "$secret" "$port" "quick" "$domain" "443" generate_telemt_toml "$secret" "$port" "lite" "$domain" "443"
# Валидация # Валидация
validate_telemt_config || return validate_telemt_config || return
@@ -147,19 +324,19 @@ install_quick_mode() {
start_telemt || return start_telemt || return
# Сохраняем GoTelegram конфиг # Сохраняем GoTelegram конфиг
save_gotelegram_config "telemt" "quick" "$port" "$secret" "$domain" "" "" save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
# Благодарности # Благодарности
show_credits show_credits
# Результат # Результат
show_proxy_info show_proxy_info
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Quick-режим)" log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Lite-режим)"
} }
# ── Stealth-режим ──────────────────────────────────────────────────────────── # ── Pro-режим ────────────────────────────────────────────────────────────────
install_stealth_mode() { install_pro_mode() {
log_step "Установка Stealth-режима" log_step "Установка Pro-режима"
# Ввод домена # Ввод домена
echo "" echo ""
@@ -192,24 +369,32 @@ install_stealth_mode() {
template_dir=$(interactive_template_selection) template_dir=$(interactive_template_selection)
[ $? -ne 0 ] && return [ $? -ne 0 ] && return
# Внутренний порт для telemt (только localhost, не открыт наружу) # Архитектура Pro:
# nginx принимает весь трафик на внешнем 443 и проксирует MTProxy-соединения на localhost:8443 # telemt слушает на 0.0.0.0:443 (принимает ВСЕ подключения)
# Внешний порт 8443 НЕ затрагивается и НЕ открывается — telemt слушает только 127.0.0.1 # nginx слушает на 127.0.0.1:8443 с SSL (обслуживает сайт)
local telemt_port=8443 # MTProxy клиент → :443 → telemt (проксирует)
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
# Провайдер видит только HTTPS на 443 к домену
local nginx_internal_port=8443
echo "" echo ""
echo -e " ${DIM}telemt будет слушать на localhost:$telemt_port (только внутренний, не открыт наружу)${NC}" echo -e " ${DIM}telemt принимает весь трафик на 443 (маскировка под HTTPS)${NC}"
echo -e " ${DIM}nginx принимает трафик на 443 (внешний) и проксирует к telemt${NC}" echo -e " ${DIM}nginx обслуживает сайт на внутреннем порту $nginx_internal_port${NC}"
echo -e " ${DIM}Провайдер видит только HTTPS-трафик к ${user_domain}:443${NC}"
# Генерация секрета # Генерация fake-TLS секрета (ee + secret + hex domain)
local secret # Префикс ee говорит Telegram-клиенту маскировать трафик под TLS к домену
secret=$(generate_hex 32) local raw_secret
raw_secret=$(generate_hex 32)
local domain_hex
domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n')
local faketls_secret="ee${raw_secret}${domain_hex}"
# Подтверждение # Подтверждение
echo "" echo ""
echo -e " ${BOLD}${WHITE}📋 Конфигурация:${NC}" echo -e " ${BOLD}${WHITE}📋 Конфигурация:${NC}"
echo -e " Домен: ${CYAN}${user_domain}${NC}" echo -e " Домен: ${CYAN}${user_domain}${NC}"
echo -e " Порт: ${CYAN}443 (nginx) → $telemt_port (telemt)${NC}" echo -e " Порт: ${CYAN}443 (telemt + nginx внутри)${NC}"
echo -e " Режим: ${MAGENTA}Stealth${NC}" echo -e " Режим: ${MAGENTA}Pro (fake-TLS)${NC}"
echo "" echo ""
if ! confirm "Установить прокси + сайт?"; then if ! confirm "Установить прокси + сайт?"; then
@@ -220,11 +405,15 @@ install_stealth_mode() {
ensure_deps ensure_deps
install_telemt_full || return install_telemt_full || return
# Конфиг telemt: маскировка на localhost (nginx) # Конфиг telemt: слушает 443, маскировка на локальный nginx через dns_override
generate_telemt_toml "$secret" "$telemt_port" "stealth" "127.0.0.1" "443" generate_telemt_toml "$raw_secret" "443" "pro" "$user_domain" "$nginx_internal_port"
# Настройка сайта (nginx + certbot + шаблон) # Настройка сайта (nginx на внутреннем порту + certbot + шаблон)
setup_stealth_mode "$user_domain" "$template_dir" "$telemt_port" "$ssl_email" || return setup_pro_mode "$user_domain" "$template_dir" "$nginx_internal_port" "$ssl_email" || return
# Останавливаем nginx на 443 перед запуском telemt (telemt займёт 443)
# nginx уже перенастроен на внутренний порт
systemctl restart nginx 2>/dev/null
# Запуск telemt # Запуск telemt
start_telemt || return start_telemt || return
@@ -232,22 +421,22 @@ install_stealth_mode() {
# Сохраняем конфиг # Сохраняем конфиг
local tpl_id local tpl_id
tpl_id=$(basename "$template_dir") tpl_id=$(basename "$template_dir")
save_gotelegram_config "telemt" "stealth" "443" "$secret" "127.0.0.1" "$user_domain" "$tpl_id" save_gotelegram_config "telemt" "pro" "443" "$raw_secret" "$user_domain" "$user_domain" "$tpl_id"
# Результат # Результат — используем домен и fake-TLS ссылку
show_proxy_info show_proxy_info_pro "$user_domain" "$faketls_secret"
echo -e " ${WHITE}Сайт:${NC} ${GREEN}https://${user_domain}${NC}" echo -e " ${WHITE}Сайт:${NC} ${GREEN}https://${user_domain}${NC}"
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Stealth-режим)" log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Pro-режим)"
} }
# ── Статус ─────────────────────────────────────────────────────────────────── # ── Статус ───────────────────────────────────────────────────────────────────
menu_status() { menu_status() {
show_proxy_info show_proxy_info
# Дополнительно для stealth # Дополнительно для pro
local mode local mode
mode=$(config_get mode 2>/dev/null) mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "stealth" ]; then if [ "$mode" = "pro" ]; then
local domain local domain
domain=$(config_get domain 2>/dev/null) domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ]; then if [ -n "$domain" ]; then
@@ -265,11 +454,21 @@ menu_status() {
# ── Ссылка ─────────────────────────────────────────────────────────────────── # ── Ссылка ───────────────────────────────────────────────────────────────────
menu_link() { menu_link() {
local secret port ip link local secret port ip link mode domain
secret=$(get_config_value secret) secret=$(get_config_value secret)
port=$(get_config_value port) port=$(get_config_value port)
ip=$(get_server_ip) ip=$(get_server_ip)
link=$(generate_proxy_link "$ip" "$port" "$secret") mode=$(config_get mode 2>/dev/null || echo "lite")
domain=$(config_get domain 2>/dev/null || echo "")
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
local domain_hex faketls_secret
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
faketls_secret="ee${secret}${domain_hex}"
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
else
link=$(generate_proxy_link "$ip" "$port" "$secret")
fi
echo "" echo ""
echo -e " ${BOLD}${WHITE}🔗 Ссылка для подключения:${NC}" echo -e " ${BOLD}${WHITE}🔗 Ссылка для подключения:${NC}"
@@ -284,18 +483,30 @@ menu_link() {
# ── Поделиться ─────────────────────────────────────────────────────────────── # ── Поделиться ───────────────────────────────────────────────────────────────
menu_share() { menu_share() {
local secret port ip link local secret port ip link mode domain server_display
secret=$(get_config_value secret) secret=$(get_config_value secret)
port=$(get_config_value port) port=$(get_config_value port)
ip=$(get_server_ip) ip=$(get_server_ip)
link=$(generate_proxy_link "$ip" "$port" "$secret") mode=$(config_get mode 2>/dev/null || echo "lite")
domain=$(config_get domain 2>/dev/null || echo "")
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
local domain_hex faketls_secret
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
faketls_secret="ee${secret}${domain_hex}"
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
server_display="$domain"
else
link=$(generate_proxy_link "$ip" "$port" "$secret")
server_display="$ip"
fi
echo "" echo ""
echo -e " ${BOLD}📤 Перешлите это сообщение:${NC}" echo -e " ${BOLD}📤 Перешлите это сообщение:${NC}"
echo "" echo ""
echo "🔐 MTProxy для Telegram (GoTelegram v${GOTELEGRAM_VERSION})" echo "🔐 MTProxy для Telegram (GoTelegram v${GOTELEGRAM_VERSION})"
echo "" echo ""
echo "🌍 Сервер: $ip" echo "🌍 Сервер: $server_display"
echo "🔌 Порт: $port" echo "🔌 Порт: $port"
echo "" echo ""
echo "👉 Подключиться одним нажатием:" echo "👉 Подключиться одним нажатием:"
@@ -310,7 +521,7 @@ menu_restart() {
restart_telemt restart_telemt
local mode local mode
mode=$(config_get mode 2>/dev/null) mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "stealth" ]; then if [ "$mode" = "pro" ]; then
restart_nginx restart_nginx
fi fi
} }
@@ -331,16 +542,16 @@ menu_change_mode() {
echo "" echo ""
echo -e " ${WHITE}Текущий режим:${NC} ${CYAN}${current_mode}${NC}" echo -e " ${WHITE}Текущий режим:${NC} ${CYAN}${current_mode}${NC}"
echo "" echo ""
echo -e " ${CYAN}1)${NC} Сменить шаблон сайта (только stealth)" echo -e " ${CYAN}1${NC}) Сменить шаблон сайта (только pro)"
echo -e " ${CYAN}2)${NC} Переключить режим (quick ↔ stealth)" echo -e " ${CYAN}2${NC}) Переключить режим (lite ↔ pro)"
echo -e " ${CYAN}0)${NC} Назад" echo -e " ${CYAN}0${NC}) Назад"
echo -ne " ${WHITE}Выбор:${NC} " echo -ne " ${WHITE}Выбор:${NC} "
read -r ch read -r ch
case "$ch" in case "$ch" in
1) 1)
if [ "$current_mode" != "stealth" ]; then if [ "$current_mode" != "pro" ]; then
log_error "Смена шаблона доступна только в stealth-режиме" log_error "Смена шаблона доступна только в pro-режиме"
return return
fi fi
local template_dir local template_dir
@@ -357,12 +568,13 @@ menu_change_mode() {
esac esac
} }
# ── Управление сайтом ─────────────────────────────────────────────────────── # ── Управление сайтом ───────────────────────────────────────────────────────
menu_website() { menu_website() {
local mode local mode
mode=$(config_get mode 2>/dev/null) mode=$(config_get mode 2>/dev/null)
if [ "$mode" != "stealth" ]; then
log_info "Управление сайтом доступно только в stealth-режиме" if [ "$mode" != "pro" ]; then
log_info "Управление сайтом доступно только в pro-режиме"
return return
fi fi
@@ -374,10 +586,10 @@ menu_website() {
echo -e " Домен: ${CYAN}${domain}${NC}" echo -e " Домен: ${CYAN}${domain}${NC}"
echo -e " SSL до: $(get_ssl_expiry "$domain")" echo -e " SSL до: $(get_ssl_expiry "$domain")"
echo "" echo ""
echo -e " ${CYAN}1)${NC} Обновить SSL сертификат" echo -e " ${CYAN}1${NC}) Обновить SSL сертификат"
echo -e " ${CYAN}2)${NC} Перезапустить nginx" echo -e " ${CYAN}2${NC}) Перезапустить nginx"
echo -e " ${CYAN}3)${NC} Сменить шаблон" echo -e " ${CYAN}3${NC}) Сменить шаблон"
echo -e " ${CYAN}0)${NC} Назад" echo -e " ${CYAN}0${NC}) Назад"
echo -ne " ${WHITE}Выбор:${NC} " echo -ne " ${WHITE}Выбор:${NC} "
read -r ch read -r ch
@@ -398,10 +610,10 @@ menu_remove() {
echo "" echo ""
echo -e " ${BOLD}${RED}🗑 Удаление GoTelegram${NC}" echo -e " ${BOLD}${RED}🗑 Удаление GoTelegram${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${CYAN}1)${NC} Удалить только прокси (telemt)" echo -e " ${CYAN}1${NC}) Удалить только прокси (telemt)"
echo -e " ${CYAN}2)${NC} Удалить только Telegram-бота" echo -e " ${CYAN}2${NC}) Удалить только Telegram-бота"
echo -e " ${CYAN}3)${NC} Удалить всё (прокси + бот + настройки)" echo -e " ${CYAN}3${NC}) Удалить всё (прокси + бот + настройки)"
echo -e " ${CYAN}0)${NC} Назад" echo -e " ${CYAN}0${NC}) Назад"
echo -ne " ${WHITE}Выбор:${NC} " echo -ne " ${WHITE}Выбор:${NC} "
read -r rm_choice read -r rm_choice
@@ -415,8 +627,8 @@ menu_remove() {
remove_telemt remove_telemt
local mode local mode
mode=$(config_get mode 2>/dev/null) mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "stealth" ]; then if [ "$mode" = "pro" ]; then
remove_stealth_mode remove_pro_mode
fi fi
rm -f "$GOTELEGRAM_CONFIG" rm -f "$GOTELEGRAM_CONFIG"
log_success "Прокси удалён" log_success "Прокси удалён"
@@ -434,8 +646,8 @@ menu_remove() {
remove_telemt remove_telemt
local mode local mode
mode=$(config_get mode 2>/dev/null) mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "stealth" ]; then if [ "$mode" = "pro" ]; then
remove_stealth_mode remove_pro_mode
fi fi
rm -f "$GOTELEGRAM_CONFIG" rm -f "$GOTELEGRAM_CONFIG"
# Бот # Бот
@@ -477,21 +689,21 @@ menu_bot() {
running) running)
echo -e " Статус: ${GREEN}● Работает${NC}" echo -e " Статус: ${GREEN}● Работает${NC}"
echo "" echo ""
echo -e " ${CYAN}1)${NC} 📊 Статус бота" echo -e " ${CYAN}1${NC}) 📊 Статус бота"
echo -e " ${CYAN}2)${NC} 📋 Логи бота" echo -e " ${CYAN}2${NC}) 📋 Логи бота"
echo -e " ${CYAN}3)${NC} 🔄 Перезапустить бота" echo -e " ${CYAN}3${NC}) 🔄 Перезапустить бота"
echo -e " ${CYAN}4)${NC} ⏹ Остановить бота" echo -e " ${CYAN}4${NC}) ⏹ Остановить бота"
echo -e " ${CYAN}5)${NC} ⚙️ Настройки (.env)" echo -e " ${CYAN}5${NC}) ⚙️ Настройки (.env)"
echo -e " ${CYAN}6)${NC} 🗑 Удалить бота" echo -e " ${CYAN}6${NC}) 🗑 Удалить бота"
;; ;;
stopped) stopped)
echo -e " Статус: ${YELLOW}○ Остановлен${NC}" echo -e " Статус: ${YELLOW}○ Остановлен${NC}"
echo "" echo ""
echo -e " ${CYAN}1)${NC} 📊 Статус бота" echo -e " ${CYAN}1${NC}) 📊 Статус бота"
echo -e " ${CYAN}2)${NC} 📋 Логи бота" echo -e " ${CYAN}2${NC}) 📋 Логи бота"
echo -e " ${CYAN}3)${NC} ▶️ Запустить бота" echo -e " ${CYAN}3${NC}) ▶️ Запустить бота"
echo -e " ${CYAN}5)${NC} ⚙️ Настройки (.env)" echo -e " ${CYAN}5${NC}) ⚙️ Настройки (.env)"
echo -e " ${CYAN}6)${NC} 🗑 Удалить бота" echo -e " ${CYAN}6${NC}) 🗑 Удалить бота"
;; ;;
*) *)
echo -e " Статус: ${RED}Не установлен${NC}" echo -e " Статус: ${RED}Не установлен${NC}"
@@ -499,11 +711,11 @@ menu_bot() {
echo -e " ${DIM}Бот позволяет управлять прокси прямо из Telegram:${NC}" echo -e " ${DIM}Бот позволяет управлять прокси прямо из Telegram:${NC}"
echo -e " ${DIM}статус, перезапуск, смена режима, бекап, QR-код.${NC}" echo -e " ${DIM}статус, перезапуск, смена режима, бекап, QR-код.${NC}"
echo "" echo ""
echo -e " ${CYAN}1)${NC} 🔧 Установить бота" echo -e " ${CYAN}1${NC}) 🔧 Установить бота"
;; ;;
esac esac
echo -e " ${CYAN}0)${NC} « Назад" echo -e " ${CYAN}0${NC}) « Назад"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -ne " ${WHITE}Выбор:${NC} " echo -ne " ${WHITE}Выбор:${NC} "
read -r ch read -r ch
@@ -587,14 +799,25 @@ bot_install() {
[ -z "$token" ] && log_error "Токен не может быть пустым" [ -z "$token" ] && log_error "Токен не может быть пустым"
done done
echo -ne " ${WHITE}ID администратора (Enter = доступ для всех):${NC} " echo ""
read -r admin_id echo -e " ${WHITE}Как добавить администратора?${NC}"
echo -e " ${CYAN}1${NC}) Автоматически — бот определит ID при первом /start"
echo -e " ${CYAN}2${NC}) Вручную — ввести ID сейчас"
echo -ne " ${WHITE}Выбор [1]:${NC} "
read -r admin_mode
admin_mode="${admin_mode:-1}"
local admin_ids=""
if [ "$admin_mode" = "2" ]; then
echo -ne " ${WHITE}ID администраторов (через пробел/запятую):${NC} "
read -r admin_ids
admin_ids=$(echo "$admin_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
fi
{ {
echo "BOT_TOKEN=$token" echo "BOT_TOKEN=$token"
[ -n "$admin_id" ] && echo "ALLOWED_IDS=$admin_id" [ -n "$admin_ids" ] && echo "ALLOWED_IDS=$admin_ids"
} > "$BOT_DIR/.env" } > "$BOT_DIR/.env"
chmod 600 "$BOT_DIR/.env" chmod 600 "$BOT_DIR/.env"
log_success ".env создан" log_success ".env создан"
else else
@@ -623,6 +846,59 @@ SVCEOF
systemctl enable "$BOT_SERVICE" &>/dev/null systemctl enable "$BOT_SERVICE" &>/dev/null
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE" systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
# Если авто-режим — ждём пока бот словит первого админа
local has_ids
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
if [ -z "$has_ids" ]; then
echo ""
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
echo -e " ${YELLOW}${NC} ${BOLD}Ожидание администратора${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Откройте бота в Telegram и отправьте ${CYAN}/start${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Бот автоматически назначит вас администратором ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${DIM}Нажмите Ctrl+C чтобы пропустить${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
echo ""
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
local waited=0
local max_wait=300 # 5 минут максимум
# Ловим Ctrl+C чтобы выйти из ожидания без убийства скрипта
local interrupted=0
trap 'interrupted=1' INT
while [ $waited -lt $max_wait ] && [ $interrupted -eq 0 ]; do
printf "\r ${CYAN}${frames[$i]}${NC} Ожидание... напишите /start боту (%d сек) " "$waited" >&2
i=$(( (i+1) % ${#frames[@]} ))
sleep 1
waited=$((waited + 1))
# Проверяем появился ли ALLOWED_IDS
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
if [ -n "$has_ids" ]; then
break
fi
done
trap - INT
printf "\r\033[K" >&2 # очистить строку со спиннером
if [ -n "$has_ids" ]; then
echo ""
log_success "Администратор назначен!"
echo -e " ${WHITE}ID:${NC} ${GREEN}${has_ids}${NC}"
elif [ $interrupted -eq 1 ]; then
echo ""
log_warning "Пропущено. Добавить админа позже: меню → Telegram-бот → Настройки"
else
echo ""
log_warning "Таймаут (5 мин). Добавить админа: меню → Telegram-бот → Настройки"
fi
fi
echo "" echo ""
log_success "Бот установлен и запущен!" log_success "Бот установлен и запущен!"
echo -e " ${DIM}Проверка: systemctl status $BOT_SERVICE${NC}" echo -e " ${DIM}Проверка: systemctl status $BOT_SERVICE${NC}"
@@ -676,9 +952,9 @@ bot_edit_config() {
fi fi
echo "" echo ""
echo -e " ${CYAN}1)${NC} Сменить BOT_TOKEN" echo -e " ${CYAN}1${NC}) Сменить BOT_TOKEN"
echo -e " ${CYAN}2)${NC} Изменить ALLOWED_IDS" echo -e " ${CYAN}2${NC}) Изменить ALLOWED_IDS"
echo -e " ${CYAN}0)${NC} Назад" echo -e " ${CYAN}0${NC}) Назад"
echo -ne " ${WHITE}Выбор:${NC} " echo -ne " ${WHITE}Выбор:${NC} "
read -r ch read -r ch
@@ -696,9 +972,10 @@ bot_edit_config() {
fi fi
;; ;;
2) 2)
echo -ne " ${WHITE}ALLOWED_IDS (через запятую, пусто = все):${NC} " echo -ne " ${WHITE}ALLOWED_IDS (через пробел/запятую, пусто = авто):${NC} "
read -r new_ids read -r new_ids
new_ids=$(echo "$new_ids" | tr -d '[:space:]') # Нормализуем: пробелы и запятые → запятые, убираем лишнее
new_ids=$(echo "$new_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then
if [ -n "$new_ids" ]; then if [ -n "$new_ids" ]; then
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env" sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
@@ -733,16 +1010,91 @@ bot_remove() {
menu_promo() { menu_promo() {
echo "" echo ""
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}" echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
echo -e " ${YELLOW}${NC} ${BOLD}💰 ХОСТИНГ СО СКИДКОЙ ДО -60%${NC} ${YELLOW}${NC}" echo -e " ${YELLOW}${NC} ${BOLD}💰 ХОСТИНГ #1 — СКИДКА ДО 60%${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}${NC}" echo -e " ${YELLOW}${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}" echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Промокоды: OFF60, antenka20, antenka6, antenka12 ${YELLOW}${NC}" echo -e " ${YELLOW}${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${WHITE}antenka20${NC} — 20% + 3% при оплате за 3 месяца ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${WHITE}antenka6${NC} — 15% + 5% при оплате за 6 месяцев ${YELLOW}${NC}"
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
echo -e " ${YELLOW}${NC} ${BOLD}💰 ХОСТИНГ #2 — СКИДКА ДО 60%${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Ссылка: ${CYAN}https://vk.cc/cUxAhj${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}" echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Донат: ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC} ${YELLOW}${NC}" echo -e " ${YELLOW}${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}${NC}"
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
echo -e " ${YELLOW}${NC} ${BOLD}☕ Донат / Чаевые${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}" echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
echo "" echo ""
} }
# ── Проверка: показывать ли промо (раз в сутки) ────────────────────────────
should_show_promo() {
local stamp_file="$GOTELEGRAM_DIR/.promo_last_shown"
if [ ! -f "$stamp_file" ]; then
return 0 # никогда не показывали
fi
local last_shown now diff
last_shown=$(cat "$stamp_file" 2>/dev/null || echo "0")
last_shown="${last_shown//[^0-9]/}"
last_shown="${last_shown:-0}"
now=$(date +%s)
diff=$(( now - last_shown ))
# 86400 = 24 часа
[ "$diff" -ge 86400 ]
}
mark_promo_shown() {
mkdir -p "$GOTELEGRAM_DIR"
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
}
# ── Промо с QR и задержкой (при установке + раз в сутки) ───────────────────
show_promo_with_qr() {
echo ""
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
echo -e " ${YELLOW}${NC} ${BOLD}💰 ХОСТИНГ #1 — СКИДКА ДО 60%${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${WHITE}antenka20${NC} — 20% + 3% при оплате за 3 месяца ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${WHITE}antenka6${NC} — 15% + 5% при оплате за 6 месяцев ${YELLOW}${NC}"
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
echo -e " ${YELLOW}${NC} ${BOLD}💰 ХОСТИНГ #2 — СКИДКА ДО 60%${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Ссылка: ${CYAN}https://vk.cc/cUxAhj${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}${NC}"
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
echo ""
# QR-коды
if command -v qrencode &>/dev/null; then
echo -e " ${DIM}── QR: Хостинг #1 ──${NC}"
qrencode -t UTF8 -m 1 "https://vk.cc/ct29NQ" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
echo ""
echo -e " ${DIM}── QR: Хостинг #2 ──${NC}"
qrencode -t UTF8 -m 1 "https://vk.cc/cUxAhj" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
echo ""
echo -e " ${DIM}── QR: Чаевые / Донат ──${NC}"
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
fi
mark_promo_shown
# 5-секундная задержка с обратным отсчётом
for i in 5 4 3 2 1; do
echo -ne "\r ${DIM}Меню через ${i} сек...${NC} "
sleep 1
done
echo -ne "\r \r"
}
# ── Точка входа ────────────────────────────────────────────────────────────── # ── Точка входа ──────────────────────────────────────────────────────────────
main() { main() {
check_root check_root
@@ -753,31 +1105,113 @@ main() {
check_os check_os
check_disk_space 500 check_disk_space 500
while true; do # Промо раз в сутки
show_main_menu if should_show_promo; then
read -r choice show_promo_with_qr
case "$choice" in fi
1) menu_install ;;
2) menu_status ;;
3) menu_link ;;
4) menu_share ;;
5) menu_restart ;;
6) menu_logs ;;
7) menu_change_mode ;;
8) interactive_backup ;;
9) interactive_restore ;;
10) update_telemt ;;
11) menu_website ;;
12) menu_bot ;;
13) menu_remove ;;
14) menu_promo ;;
0|q|exit) echo ""; log_info "До встречи! 👋"; exit 0 ;;
*) log_error "Неверный выбор" ;;
esac
echo "" while true; do
echo -ne " ${DIM}Нажмите Enter для возврата в меню...${NC}" clear
read -r show_main_menu
# Auto-refresh: 30 sec timeout
if read -t 30 -r choice; then
case "$choice" in
1) submenu_proxy ;;
2) submenu_stats ;;
3) submenu_manage ;;
4) menu_bot ;;
5) submenu_about ;;
0|q|exit) echo ""; log_info "До встречи! 👋"; exit 0 ;;
*) log_error "Неверный выбор" ;;
esac
# Пауза после подменю (кроме статистики — у неё свой цикл)
if [ "$choice" != "2" ]; then
echo ""
echo -ne " ${DIM}Нажмите Enter для возврата в меню...${NC}"
read -r
fi
fi
# If read timed out, loop refreshes the dashboard
done
}
# ── Статистика (авто-обновление 1 сек, без мерцания) ───────────────────────
submenu_stats() {
# Инициализируем статистику при первом входе
if type stats_init &>/dev/null; then
stats_init 2>/dev/null
fi
local line2; line2=$(printf '─%.0s' {1..54})
local first_draw=1
# Скрываем курсор для плавного обновления
tput civis 2>/dev/null
# Восстанавливаем курсор при выходе из функции
trap 'tput cnorm 2>/dev/null; trap - RETURN' RETURN
while true; do
if [ "$first_draw" -eq 1 ]; then
clear
first_draw=0
else
# Перемещаем курсор в начало экрана вместо clear — нет мерцания
tput cup 0 0 2>/dev/null || printf '\033[H'
fi
# Рисуем весь экран поверх старого содержимого
echo -e "\033[J" # очищаем от курсора до конца (убирает хвосты)
echo -e " ${BOLD}${WHITE}📊 Статистика трафика${NC}"
echo -e " ${DIM}${line2}${NC}"
if type show_traffic_stats &>/dev/null; then
show_traffic_stats
else
echo -e " ${DIM}Модуль статистики не загружен.${NC}"
echo -e " ${DIM}Файл lib/stats.sh не найден.${NC}"
echo ""
fi
echo -e " ${DIM}${line2}${NC}"
local stats_on="вкл"
if type toggle_stats &>/dev/null; then
local cfg_val
cfg_val=$(config_get stats_enabled 2>/dev/null || echo "true")
[ "$cfg_val" = "false" ] && stats_on="выкл"
fi
echo -e " ${CYAN}1${NC}) Вкл/Выкл подсчёт (сейчас: ${stats_on})"
echo -e " ${CYAN}2${NC}) Установить/обновить сборщик статистики"
echo -e " ${CYAN}0${NC}) ${DIM}← Назад${NC}"
echo -e " ${DIM}${line2}${NC}"
echo -e " ${DIM}Обновление каждые 3 сек${NC}"
# Показываем курсор для ввода, потом снова скрываем
tput cnorm 2>/dev/null
echo -ne " ${WHITE}${NC}"
if read -t 3 -r ch; then
tput civis 2>/dev/null
case "$ch" in
1)
if type toggle_stats &>/dev/null; then
toggle_stats
echo -ne " ${DIM}Нажмите Enter...${NC}"; read -r
first_draw=1 # полная перерисовка после действия
fi
;;
2)
if type install_stats_collector &>/dev/null; then
install_stats_collector
echo -ne " ${DIM}Нажмите Enter...${NC}"; read -r
first_draw=1
fi
;;
0|"") return ;;
esac
fi
tput civis 2>/dev/null
done done
} }

0
install_gotelegram_bot.sh Normal file → Executable file
View File

0
lib/backup.sh Normal file → Executable file
View File

6
lib/common.sh Normal file → Executable file
View File

@@ -1,9 +1,9 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.2 — Общие утилиты # GoTelegram v2.3 — Общие утилиты
# Цвета, логирование, спиннер, системные функции, совместимость с v1 # Цвета, логирование, спиннер, системные функции, совместимость с v1
# ── Версия ──────────────────────────────────────────────────────────────────── # ── Версия ────────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.2.1" GOTELEGRAM_VERSION="2.3.1"
GOTELEGRAM_NAME="GoTelegram" GOTELEGRAM_NAME="GoTelegram"
# ── Пути ────────────────────────────────────────────────────────────────────── # ── Пути ──────────────────────────────────────────────────────────────────────
@@ -270,7 +270,7 @@ save_gotelegram_config() {
{ {
"version": "$GOTELEGRAM_VERSION", "version": "$GOTELEGRAM_VERSION",
"engine": "${1:-telemt}", "engine": "${1:-telemt}",
"mode": "${2:-quick}", "mode": "${2:-lite}",
"port": ${3:-443}, "port": ${3:-443},
"secret": "${4:-}", "secret": "${4:-}",
"mask_host": "${5:-google.com}", "mask_host": "${5:-google.com}",

406
lib/stats.sh Executable file
View File

@@ -0,0 +1,406 @@
#!/bin/bash
# stats.sh — Traffic statistics module for GoTelegram
# Tracks proxy (telemt port 443) and site (nginx port 8443) traffic
# Uses iptables counters + real-time snapshots + historical CSV
# Color codes (from common.sh)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
STATS_DIR="/run/gotelegram"
HISTORY_FILE="/opt/gotelegram/stats_history.csv"
SNAPSHOTS_DIR="$STATS_DIR/snapshots"
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
CONFIG_FILE="/opt/gotelegram/config.json"
# Initialize stats infrastructure
stats_init() {
# Create runtime directory
mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
chmod 755 "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
# Create iptables chain if not exists
if ! iptables -L GOTELEGRAM_STATS -n >/dev/null 2>&1; then
iptables -N GOTELEGRAM_STATS 2>/dev/null
fi
# Add chain to INPUT if not already present
if ! iptables -C INPUT -j GOTELEGRAM_STATS 2>/dev/null; then
iptables -I INPUT -j GOTELEGRAM_STATS 2>/dev/null
fi
# Add rule for proxy traffic (port 443, TCP)
if ! iptables -C GOTELEGRAM_STATS -p tcp --dport 443 2>/dev/null; then
iptables -A GOTELEGRAM_STATS -p tcp --dport 443 2>/dev/null
fi
# Add rule for site traffic (loopback, port 8443, TCP)
if ! iptables -C GOTELEGRAM_STATS -i lo -p tcp --dport 8443 2>/dev/null; then
iptables -A GOTELEGRAM_STATS -i lo -p tcp --dport 8443 2>/dev/null
fi
# Initialize CSV header if file doesn't exist
if [[ ! -f "$HISTORY_FILE" ]]; then
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
fi
# Write initial snapshot
stats_collect
}
# Collect current traffic statistics from iptables
stats_collect() {
local proxy_bytes=0 proxy_pkts=0 site_bytes=0 site_pkts=0
local ts=$(date +%s)
local temp_file=$(mktemp)
# Parse iptables output: format is "pkts bytes target"
# We need to extract bytes (2nd column) for each rule
local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null)
# Extract counters for port 443 (proxy)
proxy_bytes=$(echo "$iptables_output" | grep "dpt:443" | grep -v "lo" | awk '{print $2}')
proxy_pkts=$(echo "$iptables_output" | grep "dpt:443" | grep -v "lo" | awk '{print $1}')
# Extract counters for port 8443 on loopback (site)
site_bytes=$(echo "$iptables_output" | grep "dpt:8443" | awk '{print $2}')
site_pkts=$(echo "$iptables_output" | grep "dpt:8443" | awk '{print $1}')
# Default to 0 if not found
proxy_bytes=${proxy_bytes:-0}
proxy_pkts=${proxy_pkts:-0}
site_bytes=${site_bytes:-0}
site_pkts=${site_pkts:-0}
# Write current snapshot as JSON
if command -v jq &>/dev/null; then
echo "{\"ts\":$ts,\"proxy_bytes\":$proxy_bytes,\"proxy_pkts\":$proxy_pkts,\"site_bytes\":$site_bytes,\"site_pkts\":$site_pkts}" > "$CURRENT_SNAPSHOT" 2>/dev/null
else
cat > "$CURRENT_SNAPSHOT" 2>/dev/null <<EOF
{"ts":$ts,"proxy_bytes":$proxy_bytes,"proxy_pkts":$proxy_pkts,"site_bytes":$site_bytes,"site_pkts":$site_pkts}
EOF
fi
# Save snapshot for rate calculation (one per minute)
local minute_key
minute_key=$(date +%Y%m%d%H%M 2>/dev/null)
local snapshot_file="$SNAPSHOTS_DIR/snap_${minute_key}.json"
cp "$CURRENT_SNAPSHOT" "$snapshot_file" 2>/dev/null
# Append to history CSV (once per minute, check if last entry is fresh)
if [[ -f "$HISTORY_FILE" ]]; then
local last_ts
last_ts=$(grep -E '^[0-9]' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f1)
last_ts="${last_ts:-0}"
local current_minute=$((ts - (ts % 60)))
if [[ "$last_ts" -eq 0 ]] || [[ $((current_minute - last_ts)) -ge 60 ]]; then
echo "$current_minute,$proxy_bytes,$site_bytes" >> "$HISTORY_FILE" 2>/dev/null
# Cleanup old entries (keep only 365 days)
stats_cleanup_history
fi
fi
rm -f "$temp_file" 2>/dev/null
}
# Read current snapshot as JSON
stats_read_current() {
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
cat "$CURRENT_SNAPSHOT"
else
echo "{}"
fi
}
# Extract value from JSON (fallback if jq not available)
json_get() {
local json="$1"
local key="$2"
if command -v jq &>/dev/null; then
echo "$json" | jq -r ".${key}" 2>/dev/null || echo "0"
else
echo "$json" | grep -o "\"$key\":[^,}]*" | cut -d: -f2 | tr -d ' "' || echo "0"
fi
}
# Convert bytes to human-readable format
format_bytes() {
local bytes=$1
if (( bytes < 1024 )); then
printf "%.0f B" "$bytes"
elif (( bytes < 1024 * 1024 )); then
printf "%.1f KB" "$(echo "scale=1; $bytes / 1024" | bc 2>/dev/null || echo "$((bytes / 1024))")"
elif (( bytes < 1024 * 1024 * 1024 )); then
printf "%.1f MB" "$(echo "scale=1; $bytes / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024))")"
elif (( bytes < 1024 * 1024 * 1024 * 1024 )); then
printf "%.1f GB" "$(echo "scale=1; $bytes / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024 / 1024))")"
else
printf "%.1f TB" "$(echo "scale=1; $bytes / 1024 / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024 / 1024 / 1024))")"
fi
}
# Convert bytes/sec to human-readable rate
format_rate() {
local bytes_per_sec=$1
if (( bytes_per_sec < 1024 )); then
printf "%.0f B/s" "$bytes_per_sec"
elif (( bytes_per_sec < 1024 * 1024 )); then
printf "%.1f KB/s" "$(echo "scale=1; $bytes_per_sec / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024))")"
elif (( bytes_per_sec < 1024 * 1024 * 1024 )); then
printf "%.1f MB/s" "$(echo "scale=1; $bytes_per_sec / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024 / 1024))")"
else
printf "%.1f GB/s" "$(echo "scale=1; $bytes_per_sec / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024 / 1024 / 1024))")"
fi
}
# Safely convert value to integer (returns 0 for empty/non-numeric)
_to_int() {
local val="${1:-0}"
# Strip non-numeric chars, default to 0
val="${val//[^0-9]/}"
echo "${val:-0}"
}
# Calculate diff safely (never negative, never crashes on empty)
_safe_diff() {
local a=$(_to_int "$1")
local b=$(_to_int "$2")
local d=$((a - b))
(( d < 0 )) && d=0
echo "$d"
}
# Calculate traffic rates and totals from history
stats_calculate_rates() {
local traffic_type="$1" # "proxy" or "site"
local col_idx=2 # proxy_bytes is column 2
[[ "$traffic_type" == "site" ]] && col_idx=3
local now
now=$(date +%s)
# Get latest data line (skip header with grep -E '^[0-9]')
local bytes_now
bytes_now=$(_to_int "$(grep -E '^[0-9]' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f"$col_idx")")
local periods="60 300 3600 86400 604800 2592000 31536000"
local results=""
for secs in $periods; do
local target_ts=$((now - secs))
# Find closest entry at or after target timestamp (skip header)
local old_val
old_val=$(_to_int "$(awk -F, -v ts="$target_ts" '$1 ~ /^[0-9]/ && $1 <= ts' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f"$col_idx")")
local diff
diff=$(_safe_diff "$bytes_now" "$old_val")
local rate=$(( secs > 0 ? diff / secs : 0 ))
local bytes_fmt rate_fmt
bytes_fmt=$(format_bytes "$diff")
rate_fmt=$(format_rate "$rate")
if [ -z "$results" ]; then
results="${bytes_fmt}|${rate_fmt}"
else
results="${results}|${bytes_fmt}|${rate_fmt}"
fi
done
echo "$results"
}
# Main display function for traffic statistics
show_traffic_stats() {
# Ensure stats are collected
stats_collect
# Get current counters
local current_json=$(stats_read_current)
local proxy_pkts=$(json_get "$current_json" "proxy_pkts")
local site_pkts=$(json_get "$current_json" "site_pkts")
# Calculate rates for proxy
local proxy_rates=$(stats_calculate_rates "proxy")
IFS='|' read -r p1m p1mr p5m p5mr p60m p60mr p1d p1dr p7d p7dr p30d p30dr p365d p365dr <<< "$proxy_rates"
# Calculate rates for site
local site_rates=$(stats_calculate_rates "site")
IFS='|' read -r s1m s1mr s5m s5mr s60m s60mr s1d s1dr s7d s7dr s30d s30dr s365d s365dr <<< "$site_rates"
# Display proxy stats
{
echo ""
echo -e "${BLUE} Proxy (telemt, порт 443):${NC}"
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
printf " %-9s │ %14s │ %s\n" "1 мин" "$p1m" "$p1mr"
printf " %-9s │ %14s │ %s\n" "5 мин" "$p5m" "$p5mr"
printf " %-9s │ %14s │ %s\n" "60 мин" "$p60m" "$p60mr"
printf " %-9s │ %14s │ %s\n" "1 день" "$p1d" "$p1dr"
printf " %-9s │ %14s │ %s\n" "7 дней" "$p7d" "$p7dr"
printf " %-9s │ %14s │ %s\n" "30 дней" "$p30d" "$p30dr"
printf " %-9s │ %14s │ %s\n" "365 дней" "$p365d" "$p365dr"
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
printf " Пакетов: %d\n\n" "$proxy_pkts"
echo -e "${BLUE} Сайт (nginx, порт 8443):${NC}"
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
printf " %-9s │ %14s │ %s\n" "1 мин" "$s1m" "$s1mr"
printf " %-9s │ %14s │ %s\n" "5 мин" "$s5m" "$s5mr"
printf " %-9s │ %14s │ %s\n" "60 мин" "$s60m" "$s60mr"
printf " %-9s │ %14s │ %s\n" "1 день" "$s1d" "$s1dr"
printf " %-9s │ %14s │ %s\n" "7 дней" "$s7d" "$s7dr"
printf " %-9s │ %14s │ %s\n" "30 дней" "$s30d" "$s30dr"
printf " %-9s │ %14s │ %s\n" "365 дней" "$s365d" "$s365dr"
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
printf " Пакетов: %d\n" "$site_pkts"
echo ""
} >&2
}
# Clean up history older than 365 days
stats_cleanup_history() {
if [[ ! -f "$HISTORY_FILE" ]]; then
return
fi
local now=$(date +%s)
local ts_365d=$((now - 31536000))
local temp_file=$(mktemp)
# Keep header + entries from last 365 days
{
head -1 "$HISTORY_FILE"
awk -F, -v ts="$ts_365d" '$1 >= ts' "$HISTORY_FILE" | tail -n +2
} > "$temp_file" 2>/dev/null
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
}
# Toggle stats collection on/off
toggle_stats() {
local current_state="false"
# Read current state from config
if [[ -f "$CONFIG_FILE" ]] && command -v jq &>/dev/null; then
current_state=$(jq -r '.stats_enabled // false' "$CONFIG_FILE" 2>/dev/null)
fi
# Toggle
if [[ "$current_state" == "true" ]]; then
# Disable stats
if [[ -f "$CONFIG_FILE" ]]; then
if command -v jq &>/dev/null; then
jq '.stats_enabled = false' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" 2>/dev/null
mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
fi
fi
# Remove iptables rules
iptables -D INPUT -j GOTELEGRAM_STATS 2>/dev/null
iptables -F GOTELEGRAM_STATS 2>/dev/null
iptables -X GOTELEGRAM_STATS 2>/dev/null
# Clean up directories
rm -rf "$STATS_DIR" 2>/dev/null
echo "Сбор статистики ОТКЛЮЧЕН" >&2
else
# Enable stats
if [[ -f "$CONFIG_FILE" ]]; then
if command -v jq &>/dev/null; then
jq '.stats_enabled = true' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" 2>/dev/null
mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
fi
fi
# Initialize stats collection
stats_init
echo "Сбор статистики ВКЛЮЧЕН" >&2
fi
}
# Install systemd service for stats collection
install_stats_collector() {
local service_file="/etc/systemd/system/gotelegram-stats.service"
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "Требуется root для установки сервиса" >&2
return 1
fi
# Get script directory (resolve symlinks)
local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
local lib_dir=$(dirname "$script_dir")
# Create systemd service file
cat > "$service_file" <<'EOF'
[Unit]
Description=GoTelegram Traffic Stats Collector
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/bin/bash -c 'source /opt/gotelegram/lib/common.sh; source /opt/gotelegram/lib/stats.sh; stats_init; while true; do stats_collect; sleep 1; done'
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
chmod 644 "$service_file"
systemctl daemon-reload
systemctl enable gotelegram-stats.service
systemctl start gotelegram-stats.service
echo "Сервис gotelegram-stats установлен и запущен" >&2
}
# Remove stats collector service
remove_stats_collector() {
if [[ $EUID -ne 0 ]]; then
echo "Требуется root для удаления сервиса" >&2
return 1
fi
systemctl stop gotelegram-stats.service 2>/dev/null
systemctl disable gotelegram-stats.service 2>/dev/null
rm -f /etc/systemd/system/gotelegram-stats.service
systemctl daemon-reload
# Remove iptables rules
iptables -D INPUT -j GOTELEGRAM_STATS 2>/dev/null
iptables -F GOTELEGRAM_STATS 2>/dev/null
iptables -X GOTELEGRAM_STATS 2>/dev/null
# Clean up directories and files
rm -rf "$STATS_DIR" 2>/dev/null
rm -f "$HISTORY_FILE" 2>/dev/null
echo "Сервис статистики удалён" >&2
}
# Export functions for external use
export -f stats_init stats_collect stats_read_current stats_calculate_rates
export -f show_traffic_stats format_bytes format_rate toggle_stats
export -f stats_cleanup_history install_stats_collector remove_stats_collector
export -f json_get

3
lib/telemt.sh Normal file → Executable file
View File

@@ -174,9 +174,8 @@ LimitNOFILE=65535
# Безопасность # Безопасность
NoNewPrivileges=true NoNewPrivileges=true
ProtectSystem=strict ProtectSystem=full
ProtectHome=true ProtectHome=true
ReadWritePaths=/etc/telemt /var/log
PrivateTmp=true PrivateTmp=true
[Install] [Install]

120
lib/telemt_config.sh Normal file → Executable file
View File

@@ -25,52 +25,49 @@ QUICK_DOMAINS=(
"zoom.us" "zoom.us"
) )
# ── Генерация TOML конфига ─────────────────────────────────────────────────── # ── Генерация TOML конфига (telemt v3 формат) ───────────────────────────────
generate_telemt_toml() { generate_telemt_toml() {
local secret="$1" local secret="$1"
local port="${2:-443}" local port="${2:-443}"
local mask_mode="${3:-quick}" # quick | stealth local mask_mode="${3:-lite}" # lite | pro
local mask_host="${4:-google.com}" local mask_domain="${4:-google.com}"
local mask_port="${5:-443}" local mask_port="${5:-443}"
local output="${6:-$TELEMT_CONFIG}" local output="${6:-$TELEMT_CONFIG}"
mkdir -p "$(dirname "$output")" mkdir -p "$(dirname "$output")"
# В stealth-режиме telemt слушает только localhost (трафик идёт через nginx) # DNS override для pro: домен резолвится в 127.0.0.1
# В quick-режиме — на всех интерфейсах (клиенты подключаются напрямую) # чтобы mask-трафик шёл на локальный nginx, а не в интернет
local bind_addr="0.0.0.0" local dns_line=""
[ "$mask_mode" = "stealth" ] && bind_addr="127.0.0.1" if [ "$mask_mode" = "pro" ]; then
dns_line="dns_overrides = [\"${mask_domain}:${mask_port}:127.0.0.1\"]"
fi
cat > "$output" << EOTOML cat > "$output" << EOTOML
# GoTelegram v${GOTELEGRAM_VERSION} — telemt configuration # GoTelegram v${GOTELEGRAM_VERSION} — telemt v3 configuration
# Сгенерировано: $(date -Iseconds) # Сгенерировано: $(date -Iseconds)
# Режим: ${mask_mode} # Режим: ${mask_mode}
# ── Основные настройки ─────────────────────────────────────────────────────── [server]
[stats] port = ${port}
statsd_address = "" listen_addr_ipv4 = "0.0.0.0"
# ── Секреты ────────────────────────────────────────────────────────────────── [censorship]
[[users]] tls_domain = "${mask_domain}"
name = "main" mask = true
secret = "${secret}" mask_port = ${mask_port}
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
# ── Привязка ───────────────────────────────────────────────────────────────── [access.users]
[listen] main = "${secret}"
# quick: 0.0.0.0 (клиенты напрямую) | stealth: 127.0.0.1 (только через nginx)
bind_to = "${bind_addr}:${port}"
# ── TLS маскировка ───────────────────────────────────────────────────────────
[security]
# Маскировочный хост — куда перенаправлять неопознанные подключения
# quick: внешний сайт | stealth: локальный nginx
host = "${mask_host}:${mask_port}"
[network]
${dns_line}
EOTOML EOTOML
chmod 600 "$output" chmod 600 "$output"
log_success "Конфиг telemt записан: $output" log_success "Конфиг telemt записан: $output"
log_dim "Режим: $mask_mode, маскировка: $mask_host:$mask_port" log_dim "Режим: $mask_mode, домен: $mask_domain, порт mask: $mask_port"
} }
# ── Добавление дополнительного секрета ─────────────────────────────────────── # ── Добавление дополнительного секрета ───────────────────────────────────────
@@ -95,7 +92,7 @@ EOSECRET
log_success "Добавлен секрет: $name" log_success "Добавлен секрет: $name"
} }
# ── Чтение текущего конфига ────────────────────────────────────────────────── # ── Чтение текущего конфига (telemt v3 формат) ──────────────────────────────
get_config_value() { get_config_value() {
local key="$1" local key="$1"
local config="${2:-$TELEMT_CONFIG}" local config="${2:-$TELEMT_CONFIG}"
@@ -104,13 +101,19 @@ get_config_value() {
case "$key" in case "$key" in
secret) secret)
grep -m1 'secret\s*=' "$config" | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' ' # [access.users] main = "..."
grep -A5 '\[access.users\]' "$config" | grep -m1 '=' | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' '
;; ;;
port) port)
grep 'bind_to\s*=' "$config" | sed 's/.*:\([0-9]*\)".*/\1/' # [server] port = 443
grep -A5 '\[server\]' "$config" | grep 'port\s*=' | head -1 | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
;; ;;
mask_host) mask_host|tls_domain)
grep -A10 '\[security\]' "$config" | grep 'host\s*=' | sed 's/.*=\s*"\(.*\)".*/\1/' # [censorship] tls_domain = "..."
grep -A10 '\[censorship\]' "$config" | grep 'tls_domain\s*=' | sed 's/.*=\s*"\(.*\)"/\1/'
;;
mask_port)
grep -A10 '\[censorship\]' "$config" | grep 'mask_port\s*=' | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
;; ;;
*) *)
grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "' grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "'
@@ -151,7 +154,7 @@ validate_telemt_config() {
fi fi
if [ -z "$host" ]; then if [ -z "$host" ]; then
log_error "Не задан маскировочный хост (security.host)" log_error "Не задан маскировочный хост (censorship.tls_domain)"
((errors++)) ((errors++))
fi fi
@@ -254,11 +257,21 @@ show_proxy_info() {
port=$(get_config_value port "$config") port=$(get_config_value port "$config")
mask_host=$(get_config_value mask_host "$config") mask_host=$(get_config_value mask_host "$config")
ip=$(get_server_ip) ip=$(get_server_ip)
link=$(generate_proxy_link "$ip" "$port" "$secret")
status=$(telemt_status) status=$(telemt_status)
local mode local mode domain
mode=$(config_get mode 2>/dev/null || echo "quick") mode=$(config_get mode 2>/dev/null || echo "lite")
domain=$(config_get domain 2>/dev/null || echo "")
# Pro-режим: ссылка с доменом и fake-TLS секретом
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
local domain_hex faketls_secret
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
faketls_secret="ee${secret}${domain_hex}"
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
else
link=$(generate_proxy_link "$ip" "$port" "$secret")
fi
local status_icon status_text local status_icon status_text
case "$status" in case "$status" in
@@ -271,7 +284,11 @@ show_proxy_info() {
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}" echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)" echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}" if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
else
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
fi
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}" echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}" echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}" echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
@@ -286,3 +303,34 @@ show_proxy_info() {
qrencode -t UTF8 -m 2 "$link" 2>/dev/null qrencode -t UTF8 -m 2 "$link" 2>/dev/null
fi fi
} }
# ── Вывод информации о прокси (Pro-режим) ──────────────────────────────────
# В pro-режиме ссылка содержит домен (не IP) и fake-TLS секрет (ee...)
show_proxy_info_pro() {
local domain="$1"
local faketls_secret="$2"
local link="tg://proxy?server=${domain}&port=443&secret=${faketls_secret}"
echo ""
echo -e " ${BOLD}${WHITE}✅ Pro-прокси настроен${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
echo -e " ${WHITE}Порт:${NC} ${CYAN}443${NC} (внешний, telemt)"
echo -e " ${WHITE}Режим:${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC} (внутренний)"
echo -e " ${WHITE}Secret:${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${WHITE}Ссылка для Telegram:${NC}"
echo -e " ${GREEN}${link}${NC}"
echo ""
echo -e " ${DIM}Провайдер видит: HTTPS-трафик к ${domain}:443${NC}"
echo -e " ${DIM}Telegram-клиент маскирует соединение под TLS${NC}"
echo ""
# QR если доступен
if command -v qrencode &>/dev/null; then
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
fi
}

0
lib/templates_catalog.sh Normal file → Executable file
View File

26
lib/website.sh Normal file → Executable file
View File

@@ -39,8 +39,9 @@ generate_nginx_config() {
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cat > "$NGINX_SITE_CONF" << 'EONGINX' cat > "$NGINX_SITE_CONF" << 'EONGINX'
# GoTelegram v2.2 — nginx config # GoTelegram v2.3 — nginx config
# Обслуживает сайт-маскировку для telemt stealth mode # Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
server { server {
listen 80; listen 80;
@@ -60,8 +61,7 @@ server {
} }
server { server {
listen SSL_PORT_PLACEHOLDER ssl http2; listen 127.0.0.1:SSL_PORT_PLACEHOLDER ssl http2;
listen [::]:SSL_PORT_PLACEHOLDER ssl http2;
server_name DOMAIN_PLACEHOLDER; server_name DOMAIN_PLACEHOLDER;
# SSL сертификаты # SSL сертификаты
@@ -113,7 +113,7 @@ EONGINX
local escaped_domain local escaped_domain
escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g') escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g')
sed -i "s|DOMAIN_PLACEHOLDER|${escaped_domain}|g" "$NGINX_SITE_CONF" sed -i "s|DOMAIN_PLACEHOLDER|${escaped_domain}|g" "$NGINX_SITE_CONF"
sed -i "s|SSL_PORT_PLACEHOLDER|443|g" "$NGINX_SITE_CONF" sed -i "s|SSL_PORT_PLACEHOLDER|${proxy_port}|g" "$NGINX_SITE_CONF"
# Активируем сайт # Активируем сайт
rm -f /etc/nginx/sites-enabled/default 2>/dev/null rm -f /etc/nginx/sites-enabled/default 2>/dev/null
@@ -258,14 +258,14 @@ deploy_template_to_nginx() {
log_success "Шаблон развёрнут в $WEBSITE_ROOT" log_success "Шаблон развёрнут в $WEBSITE_ROOT"
} }
# ── Полная установка stealth-режима ────────────────────────────────────────── # ── Полная установка pro-режима ──────────────────────────────────────────────
setup_stealth_mode() { setup_pro_mode() {
local domain="$1" local domain="$1"
local template_dir="$2" local template_dir="$2"
local proxy_port="${3:-443}" local proxy_port="${3:-443}"
local email="${4:-}" local email="${4:-}"
log_step "Настройка stealth-режима" log_step "Настройка pro-режима"
# 1. Устанавливаем nginx # 1. Устанавливаем nginx
run_with_spinner "Установка nginx" install_nginx || return 1 run_with_spinner "Установка nginx" install_nginx || return 1
@@ -298,7 +298,7 @@ setup_stealth_mode() {
# 8. Показываем благодарности авторам шаблонов # 8. Показываем благодарности авторам шаблонов
show_credits show_credits
log_success "Stealth-режим настроен: https://${domain}" log_success "Pro-режим настроен: https://${domain}"
return 0 return 0
} }
@@ -322,13 +322,13 @@ restart_nginx() {
fi fi
} }
# ── Удаление stealth-режима ────────────────────────────────────────────────── # ── Удаление pro-режима ──────────────────────────────────────────────────────
remove_stealth_mode() { remove_pro_mode() {
log_info "Удаление stealth-режима..." log_info "Удаление pro-режима..."
rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK" rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
rm -rf "$WEBSITE_ROOT" rm -rf "$WEBSITE_ROOT"
systemctl restart nginx 2>/dev/null systemctl restart nginx 2>/dev/null
log_success "Stealth-режим удалён (nginx оставлен)" log_success "Pro-режим удалён (nginx оставлен)"
} }
# ── Смена шаблона ──────────────────────────────────────────────────────────── # ── Смена шаблона ────────────────────────────────────────────────────────────