mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-06-13 17:12:47 +00:00
Compare commits
16 Commits
16ef3564f9
...
v2.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d70e046035 | ||
|
|
c70cb36a2b | ||
|
|
b36fb5cf10 | ||
|
|
8b4b4892a4 | ||
|
|
046a08fdb6 | ||
|
|
a21d2ebea2 | ||
|
|
96cbd243d9 | ||
|
|
3f136ec8a0 | ||
|
|
a24d64d33c | ||
|
|
52912e0ead | ||
|
|
0dae922d1b | ||
|
|
6ec2123f83 | ||
|
|
364501d66d | ||
|
|
f445f7a27e | ||
|
|
6e32ca9d12 | ||
|
|
7f81c21d8e |
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal 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
140
bootstrap.sh
Normal file → Executable file
@@ -1,63 +1,115 @@
|
||||
#!/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
|
||||
|
||||
TOKEN="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"
|
||||
REPO="anten-ka/gotelegram_pro"
|
||||
BRANCH="test"
|
||||
API="https://api.github.com/repos/$REPO"
|
||||
PAT="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"
|
||||
INSTALL_DIR="/opt/gotelegram"
|
||||
API="https://api.github.com/repos/${REPO}/contents"
|
||||
|
||||
echo -e "\033[1;36m"
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ GoTelegram v2.2.1 — Установка ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo -e "\033[0m"
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
BOLD='\033[1m'
|
||||
|
||||
# Функция загрузки файла из приватного репо
|
||||
dl() {
|
||||
local path="$1" dest="$2"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
curl -sfL -H "Authorization: token $TOKEN" \
|
||||
-H "Accept: application/vnd.github.v3.raw" \
|
||||
"$API/contents/$path?ref=$BRANCH" -o "$dest"
|
||||
sed -i 's/\r$//' "$dest"
|
||||
echo " ✓ $path"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e " ${YELLOW}║${NC} ${BOLD}GoTelegram Pro — Установка${NC} ${YELLOW}║${NC}"
|
||||
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||
echo -e " ${YELLOW}║${NC} MTProxy менеджер с Telegram-ботом ${YELLOW}║${NC}"
|
||||
echo -e " ${YELLOW}║${NC} Stealth-режим, 1800+ шаблонов сайтов ${YELLOW}║${NC}"
|
||||
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# 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 "📦 Скачиваю файлы..."
|
||||
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/gotelegram-bot"
|
||||
# File list
|
||||
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"
|
||||
)
|
||||
|
||||
# Основные файлы
|
||||
dl "install.sh" "$INSTALL_DIR/install.sh"
|
||||
dl "install_gotelegram_bot.sh" "$INSTALL_DIR/install_gotelegram_bot.sh"
|
||||
dl "templates_catalog.json" "$INSTALL_DIR/templates_catalog.json"
|
||||
echo -e " ${CYAN}↻${NC} Загрузка файлов в ${INSTALL_DIR}..."
|
||||
mkdir -p "${INSTALL_DIR}/lib" "${INSTALL_DIR}/gotelegram-bot"
|
||||
|
||||
# Библиотеки
|
||||
dl "lib/common.sh" "$INSTALL_DIR/lib/common.sh"
|
||||
dl "lib/telemt.sh" "$INSTALL_DIR/lib/telemt.sh"
|
||||
dl "lib/telemt_config.sh" "$INSTALL_DIR/lib/telemt_config.sh"
|
||||
dl "lib/backup.sh" "$INSTALL_DIR/lib/backup.sh"
|
||||
dl "lib/website.sh" "$INSTALL_DIR/lib/website.sh"
|
||||
dl "lib/templates_catalog.sh" "$INSTALL_DIR/lib/templates_catalog.sh"
|
||||
failed=0
|
||||
for f in "${FILES[@]}"; do
|
||||
if download_file "$f" "${INSTALL_DIR}/${f}"; then
|
||||
echo -e " ${GREEN}✓${NC} ${f}"
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Бот
|
||||
dl "gotelegram-bot/bot.py" "$INSTALL_DIR/gotelegram-bot/bot.py"
|
||||
dl "gotelegram-bot/config.example.env" "$INSTALL_DIR/gotelegram-bot/config.example.env"
|
||||
dl "gotelegram-bot/requirements.txt" "$INSTALL_DIR/gotelegram-bot/requirements.txt"
|
||||
dl "gotelegram-bot/README.md" "$INSTALL_DIR/gotelegram-bot/README.md"
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e " ${RED}✗${NC} Не удалось загрузить ${failed} файл(ов)"
|
||||
echo -e " ${YELLOW}Проверьте токен доступа и подключение к сети${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Права
|
||||
chmod +x "$INSTALL_DIR/install.sh" "$INSTALL_DIR/install_gotelegram_bot.sh"
|
||||
# Fix permissions and line endings
|
||||
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
|
||||
ln -sf "$INSTALL_DIR/install.sh" /usr/local/bin/gotelegram
|
||||
chmod +x /usr/local/bin/gotelegram
|
||||
# Create symlink
|
||||
ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
|
||||
echo -e " ${GREEN}✓${NC} Команда ${CYAN}gotelegram${NC} доступна"
|
||||
|
||||
echo ""
|
||||
echo -e "\033[1;32m✅ Все 13 файлов скачаны в $INSTALL_DIR\033[0m"
|
||||
echo -e "\033[1;33m💡 Команда для запуска меню: gotelegram\033[0m"
|
||||
echo -e " ${GREEN}✓${NC} Установка завершена! Запуск..."
|
||||
echo ""
|
||||
|
||||
# Запускаем меню
|
||||
exec bash "$INSTALL_DIR/install.sh"
|
||||
# Launch
|
||||
exec bash "${INSTALL_DIR}/install.sh"
|
||||
|
||||
@@ -6,14 +6,17 @@ Uses python-telegram-bot v21+
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import toml
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Optional, List, Dict, Any
|
||||
|
||||
@@ -47,7 +50,7 @@ logger = logging.getLogger(__name__)
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
GOTELEGRAM_VERSION = "2.2.0"
|
||||
GOTELEGRAM_VERSION = "2.3.1"
|
||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||
TELEMT_SERVICE = "telemt"
|
||||
@@ -55,21 +58,75 @@ WEBSITE_ROOT = "/var/www/gotelegram-site"
|
||||
BACKUP_DIR = "/opt/gotelegram/backups"
|
||||
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"
|
||||
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
|
||||
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||
ALLOWED_IDS_STR = os.getenv("ALLOWED_IDS", "")
|
||||
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}")
|
||||
ENV_FILE = "/opt/gotelegram-bot/.env"
|
||||
|
||||
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",
|
||||
"microsoft.com",
|
||||
"cloudflare.com",
|
||||
@@ -225,21 +282,41 @@ async def check_old_container() -> Optional[str]:
|
||||
|
||||
|
||||
def is_user_allowed(user_id: int) -> bool:
|
||||
"""Check if user ID is in ALLOWED_IDS."""
|
||||
if not ALLOWED_IDS:
|
||||
return True
|
||||
"""Check if user ID is in ALLOWED_IDS. If list is empty — waiting for admin."""
|
||||
if _WAITING_FOR_ADMIN:
|
||||
return False # Никому не даём доступ пока не назначен админ
|
||||
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:
|
||||
"""Check authorization and send error if not allowed."""
|
||||
if not is_user_allowed(update.effective_user.id):
|
||||
await update.message.reply_text(
|
||||
f"Access denied. Your ID: {update.effective_user.id}"
|
||||
)
|
||||
logger.warning(
|
||||
f"Unauthorized access attempt from user {update.effective_user.id}"
|
||||
)
|
||||
user_id = update.effective_user.id
|
||||
|
||||
# Режим ожидания первого админа — обрабатывается в cmd_start
|
||||
if _WAITING_FOR_ADMIN:
|
||||
return False
|
||||
|
||||
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 True
|
||||
|
||||
@@ -274,13 +351,19 @@ def get_main_menu() -> InlineKeyboardMarkup:
|
||||
],
|
||||
[
|
||||
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("👤 Админы", callback_data="menu_admins"),
|
||||
InlineKeyboardButton("ℹ️ Credits", callback_data="menu_credits"),
|
||||
],
|
||||
[InlineKeyboardButton("❌ Close", callback_data="close_menu")],
|
||||
[
|
||||
InlineKeyboardButton("❌ Close", callback_data="close_menu"),
|
||||
],
|
||||
]
|
||||
return InlineKeyboardMarkup(buttons)
|
||||
|
||||
@@ -291,8 +374,37 @@ def get_main_menu() -> InlineKeyboardMarkup:
|
||||
|
||||
|
||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Start command - show main menu."""
|
||||
if not await require_auth(update, context):
|
||||
"""Start command - show main menu, promo once per day.
|
||||
|
||||
Если 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
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
# Промо раз в сутки
|
||||
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:
|
||||
"""Help command - show available commands."""
|
||||
@@ -312,12 +431,14 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
return
|
||||
|
||||
help_text = (
|
||||
"<b>GoTelegram Bot Commands</b>\n\n"
|
||||
"/start - Show main menu\n"
|
||||
"/help - Show this help message\n"
|
||||
"/status - Quick status check\n"
|
||||
"/logs - Show recent logs\n\n"
|
||||
"Use the inline menu for all other operations."
|
||||
"<b>GoTelegram Bot — Команды</b>\n\n"
|
||||
"/start — Главное меню\n"
|
||||
"/help — Эта справка\n"
|
||||
"/status — Быстрый статус\n"
|
||||
"/logs — Последние логи\n"
|
||||
"/addadmin ID — Добавить админа\n"
|
||||
"/deladmin ID — Удалить админа\n\n"
|
||||
"Используйте кнопки меню для остальных операций."
|
||||
)
|
||||
await update.message.reply_text(help_text, parse_mode="HTML")
|
||||
|
||||
@@ -403,19 +524,135 @@ async def get_status_text() -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Status callback."""
|
||||
async def get_traffic_stats() -> str:
|
||||
"""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
|
||||
await query.answer()
|
||||
|
||||
await safe_edit_message(query,"⏳ Checking status...")
|
||||
stats_text = await get_traffic_stats()
|
||||
|
||||
status_text = await get_status_text()
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("« Back", callback_data="menu_main")]]
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("🔄 Обновить", callback_data="menu_stats")],
|
||||
[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:
|
||||
"""Install mode selection menu."""
|
||||
buttons = [
|
||||
[InlineKeyboardButton("⚡ Quick Mode", callback_data="install_mode_quick")],
|
||||
[InlineKeyboardButton("🔒 Stealth Mode", callback_data="install_mode_stealth")],
|
||||
[InlineKeyboardButton("⚡ Lite", callback_data="install_mode_lite")],
|
||||
[InlineKeyboardButton("🛡 Pro", callback_data="install_mode_pro")],
|
||||
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
||||
]
|
||||
return InlineKeyboardMarkup(buttons)
|
||||
@@ -452,7 +689,7 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
)
|
||||
buttons = [
|
||||
[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")],
|
||||
]
|
||||
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:
|
||||
"""Quick mode domain selection."""
|
||||
async def cb_install_mode_lite(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Lite mode domain selection."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
# Show domains with pagination (4 per row, 2 rows)
|
||||
buttons = []
|
||||
for i in range(0, len(QUICK_DOMAINS), 2):
|
||||
for i in range(0, len(LITE_DOMAINS), 2):
|
||||
row = []
|
||||
for j in range(2):
|
||||
if i + j < len(QUICK_DOMAINS):
|
||||
domain = QUICK_DOMAINS[i + j]
|
||||
if i + j < len(LITE_DOMAINS):
|
||||
domain = LITE_DOMAINS[i + j]
|
||||
row.append(
|
||||
InlineKeyboardButton(
|
||||
domain, callback_data=f"quick_dom_{i+j}"
|
||||
domain, callback_data=f"lite_dom_{i+j}"
|
||||
)
|
||||
)
|
||||
buttons.append(row)
|
||||
|
||||
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)
|
||||
await safe_edit_message(query,text, reply_markup=keyboard)
|
||||
|
||||
|
||||
async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Quick domain selection callback."""
|
||||
async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Lite domain selection callback."""
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
try:
|
||||
domain_idx = int(data.split("_")[-1])
|
||||
domain = QUICK_DOMAINS[domain_idx]
|
||||
domain = LITE_DOMAINS[domain_idx]
|
||||
except (ValueError, IndexError):
|
||||
await query.answer("Invalid domain selection")
|
||||
return
|
||||
@@ -507,7 +744,7 @@ async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
# Simulate installation (in real scenario, call install script)
|
||||
config = {
|
||||
"mode": "quick",
|
||||
"mode": "lite",
|
||||
"domain": domain,
|
||||
"port": 443,
|
||||
"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):
|
||||
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>Mode:</b> Quick\n\n"
|
||||
f"<b>Mode:</b> Lite\n\n"
|
||||
f"Service starting... Check status in 10 seconds."
|
||||
)
|
||||
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:
|
||||
"""Stealth mode - show template categories."""
|
||||
async def cb_install_mode_pro(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Pro mode - show template categories."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
@@ -555,22 +792,22 @@ async def cb_install_mode_stealth(update: Update, context: ContextTypes.DEFAULT_
|
||||
buttons.append(
|
||||
[
|
||||
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")])
|
||||
|
||||
text = "Stealth Mode - Select Template Category:"
|
||||
text = "Pro Mode - Select Template Category:"
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
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."""
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
cat_id = data.removeprefix("stealth_cat_")
|
||||
cat_id = data.removeprefix("pro_cat_")
|
||||
|
||||
await query.answer()
|
||||
|
||||
@@ -597,22 +834,22 @@ async def cb_stealth_category(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
buttons.append(
|
||||
[
|
||||
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>:"
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
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."""
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
tpl_id = data.removeprefix("stealth_tpl_")
|
||||
tpl_id = data.removeprefix("pro_tpl_")
|
||||
|
||||
await query.answer()
|
||||
|
||||
@@ -651,26 +888,26 @@ async def cb_stealth_template(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
buttons = [
|
||||
[
|
||||
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)
|
||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||
|
||||
|
||||
async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Confirm and install stealth template."""
|
||||
async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Confirm and install pro template."""
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
tpl_id = data.removeprefix("stealth_confirm_")
|
||||
tpl_id = data.removeprefix("pro_confirm_")
|
||||
|
||||
await query.answer()
|
||||
await safe_edit_message(query,"⏳ Installing template...")
|
||||
|
||||
config = {
|
||||
"mode": "stealth",
|
||||
"mode": "pro",
|
||||
"template": tpl_id,
|
||||
"port": 443,
|
||||
"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):
|
||||
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>Mode:</b> Stealth\n\n"
|
||||
f"<b>Mode:</b> Pro\n\n"
|
||||
f"Service starting... Check status in 10 seconds."
|
||||
)
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
@@ -704,7 +941,7 @@ async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
||||
|
||||
|
||||
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)
|
||||
if not config:
|
||||
return None
|
||||
@@ -720,12 +957,20 @@ async def get_proxy_link() -> Optional[str]:
|
||||
if not secret:
|
||||
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")
|
||||
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}"
|
||||
|
||||
|
||||
@@ -1074,8 +1319,8 @@ async def cb_menu_change(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
await query.answer()
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton("⚡ Switch to Quick Mode", callback_data="change_quick")],
|
||||
[InlineKeyboardButton("🔒 Switch to Stealth Mode", callback_data="change_stealth")],
|
||||
[InlineKeyboardButton("⚡ Switch to Lite Mode", callback_data="change_lite")],
|
||||
[InlineKeyboardButton("🛡 Switch to Pro Mode", callback_data="change_pro")],
|
||||
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
||||
]
|
||||
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:
|
||||
"""Switch to quick mode — show domain selection."""
|
||||
async def cb_change_lite(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Switch to lite mode — show domain selection."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
# Reuse the quick mode domain selection flow
|
||||
await cb_install_mode_quick(update, context)
|
||||
# Reuse the lite mode domain selection flow
|
||||
await cb_install_mode_lite(update, context)
|
||||
|
||||
|
||||
async def cb_change_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Switch to stealth mode — show template categories."""
|
||||
async def cb_change_pro(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Switch to pro mode — show template categories."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
# Reuse the stealth mode template selection flow
|
||||
await cb_install_mode_stealth(update, context)
|
||||
# Reuse the pro mode template selection flow
|
||||
await cb_install_mode_pro(update, context)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 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
|
||||
# ============================================================================
|
||||
|
||||
|
||||
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:
|
||||
"""Promo information."""
|
||||
"""Promo information — always shown from menu."""
|
||||
query = update.callback_query
|
||||
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(
|
||||
[[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:
|
||||
@@ -1290,9 +1677,43 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
query = update.callback_query
|
||||
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
|
||||
if not is_user_allowed(update.effective_user.id):
|
||||
await query.answer("Access denied")
|
||||
await query.answer("Доступ запрещён")
|
||||
return
|
||||
|
||||
# Main menu
|
||||
@@ -1327,28 +1748,30 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
"menu_website": cb_menu_website,
|
||||
"menu_promo": cb_menu_promo,
|
||||
"menu_credits": cb_menu_credits,
|
||||
"menu_admins": cb_menu_admins,
|
||||
"menu_remove": cb_menu_remove,
|
||||
"install_mode_quick": cb_install_mode_quick,
|
||||
"install_mode_stealth": cb_install_mode_stealth,
|
||||
"install_mode_lite": cb_install_mode_lite,
|
||||
"install_mode_pro": cb_install_mode_pro,
|
||||
"backup_create": cb_backup_create,
|
||||
"backup_list": cb_backup_list,
|
||||
"ssl_renew": cb_ssl_renew,
|
||||
"ssl_status": cb_ssl_status,
|
||||
"remove_confirm": cb_remove_confirm,
|
||||
"change_quick": cb_change_quick,
|
||||
"change_stealth": cb_change_stealth,
|
||||
"change_lite": cb_change_lite,
|
||||
"change_pro": cb_change_pro,
|
||||
"install_migrate": cb_install_migrate,
|
||||
"menu_stats": cb_menu_stats,
|
||||
}
|
||||
|
||||
# Pattern-based handlers
|
||||
if data.startswith("quick_dom_"):
|
||||
await cb_quick_domain(update, context)
|
||||
elif data.startswith("stealth_cat_"):
|
||||
await cb_stealth_category(update, context)
|
||||
elif data.startswith("stealth_tpl_"):
|
||||
await cb_stealth_template(update, context)
|
||||
elif data.startswith("stealth_confirm_"):
|
||||
await cb_stealth_confirm(update, context)
|
||||
if data.startswith("lite_dom_"):
|
||||
await cb_lite_domain(update, context)
|
||||
elif data.startswith("pro_cat_"):
|
||||
await cb_pro_category(update, context)
|
||||
elif data.startswith("pro_tpl_"):
|
||||
await cb_pro_template(update, context)
|
||||
elif data.startswith("pro_confirm_"):
|
||||
await cb_pro_confirm(update, context)
|
||||
elif data.startswith("restore_idx_"):
|
||||
await cb_restore_backup(update, context)
|
||||
elif data in handlers:
|
||||
@@ -1386,6 +1809,8 @@ def main() -> None:
|
||||
application.add_handler(CommandHandler("help", cmd_help))
|
||||
application.add_handler(CommandHandler("status", cmd_status))
|
||||
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)
|
||||
application.add_handler(CallbackQueryHandler(handle_callback))
|
||||
|
||||
728
install.sh
Normal file → Executable file
728
install.sh
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
#!/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
|
||||
#
|
||||
# Установка:
|
||||
@@ -20,48 +20,225 @@ source "$LIB_DIR/telemt_config.sh"
|
||||
source "$LIB_DIR/website.sh"
|
||||
source "$LIB_DIR/templates_catalog.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() {
|
||||
local proxy_status bot_status
|
||||
local proxy_status bot_status nginx_st mode domain secret port ip link ssl_expiry
|
||||
proxy_status=$(telemt_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
|
||||
case "$proxy_status" in
|
||||
running) proxy_badge="${GREEN}● Работает${NC}" ;;
|
||||
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
|
||||
local W=54
|
||||
local line; line=$(printf '━%.0s' $(seq 1 $W))
|
||||
local line2; line2=$(printf '─%.0s' $(seq 1 $W))
|
||||
|
||||
# ── Заголовок (без правого бордера — ANSI ломает выравнивание) ──
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}Главное меню${NC} │ Proxy: ${proxy_badge} │ ${bot_badge}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
echo -e " ${DIM}── Прокси ──${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 " ${DIM}── Управление ──${NC}"
|
||||
echo -e " ${CYAN} 8)${NC} 💾 Бекап конфигурации"
|
||||
echo -e " ${CYAN} 9)${NC} 📦 Восстановить из бекапа"
|
||||
echo -e " ${CYAN}10)${NC} ⬆️ Обновить telemt"
|
||||
echo -e " ${CYAN}11)${NC} 🌐 Управление сайтом (SSL)"
|
||||
echo -e " ${DIM}── Бот и прочее ──${NC}"
|
||||
echo -e " ${CYAN}12)${NC} 🤖 Telegram-бот"
|
||||
echo -e " ${CYAN}13)${NC} 🗑 Удалить всё"
|
||||
echo -e " ${CYAN}14)${NC} 🏷 Промо"
|
||||
echo -e " ${CYAN} 0)${NC} 🚪 Выход"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
echo -ne " ${WHITE}Выбор:${NC} "
|
||||
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||||
echo -e " ${BOLD}${WHITE} GoTelegram v${GOTELEGRAM_VERSION}${NC} ${DIM}— Панель управления${NC}"
|
||||
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||||
|
||||
# ── Здоровье сервисов ──
|
||||
echo ""
|
||||
echo -e " ${DIM}${line2}${NC}"
|
||||
|
||||
# Proxy
|
||||
local proxy_icon proxy_color
|
||||
case "$proxy_status" in
|
||||
running) proxy_icon="●"; proxy_color="${GREEN}" ;;
|
||||
stopped) proxy_icon="○"; proxy_color="${YELLOW}" ;;
|
||||
*) proxy_icon="✗"; proxy_color="${RED}" ;;
|
||||
esac
|
||||
echo -e " ${proxy_color}${proxy_icon}${NC} Прокси ${proxy_color}${proxy_status}${NC} ${DIM}(telemt ${mode})${NC}"
|
||||
|
||||
# nginx
|
||||
local nginx_icon nginx_color
|
||||
case "$nginx_st" in
|
||||
running) nginx_icon="●"; nginx_color="${GREEN}" ;;
|
||||
*) 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 -e " ${BOLD}${WHITE}🎭 Выберите режим маскировки:${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}под выбранный сайт (google.com и т.д.)${NC}"
|
||||
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}DPI видит реальный сайт с реальным сертификатом.${NC}"
|
||||
echo -e " ${DIM}Требует: домен, направленный на этот сервер.${NC}"
|
||||
echo -e " ${DIM}Требует: домен, направленный на этот сервер.{{NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -ne " ${WHITE}Выбор (1/2):${NC} "
|
||||
read -r mode_choice
|
||||
mode_choice="${mode_choice:-}"
|
||||
|
||||
case "$mode_choice" in
|
||||
1) install_quick_mode ;;
|
||||
2) install_stealth_mode ;;
|
||||
1) install_lite_mode ;;
|
||||
2) install_pro_mode ;;
|
||||
*) log_error "Неверный выбор: ${mode_choice:-<пусто>}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Quick-режим ──────────────────────────────────────────────────────────────
|
||||
install_quick_mode() {
|
||||
log_step "Установка Quick-режима"
|
||||
# ── Lite-режим ───────────────────────────────────────────────────────────────
|
||||
install_lite_mode() {
|
||||
log_step "Установка Lite-режима"
|
||||
|
||||
# Выбор домена
|
||||
local domain
|
||||
@@ -126,7 +303,7 @@ install_quick_mode() {
|
||||
echo -e " IP: ${CYAN}${ip}${NC}"
|
||||
echo -e " Порт: ${CYAN}${port}${NC}"
|
||||
echo -e " Маскировка: ${CYAN}${domain}${NC}"
|
||||
echo -e " Режим: ${GREEN}Quick${NC}"
|
||||
echo -e " Режим: ${GREEN}Lite${NC}"
|
||||
echo ""
|
||||
|
||||
if ! confirm "Установить прокси?"; then
|
||||
@@ -138,7 +315,7 @@ install_quick_mode() {
|
||||
install_telemt_full || return
|
||||
|
||||
# Генерируем конфиг telemt
|
||||
generate_telemt_toml "$secret" "$port" "quick" "$domain" "443"
|
||||
generate_telemt_toml "$secret" "$port" "lite" "$domain" "443"
|
||||
|
||||
# Валидация
|
||||
validate_telemt_config || return
|
||||
@@ -147,19 +324,19 @@ install_quick_mode() {
|
||||
start_telemt || return
|
||||
|
||||
# Сохраняем GoTelegram конфиг
|
||||
save_gotelegram_config "telemt" "quick" "$port" "$secret" "$domain" "" ""
|
||||
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
|
||||
|
||||
# Благодарности
|
||||
show_credits
|
||||
|
||||
# Результат
|
||||
show_proxy_info
|
||||
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Quick-режим)"
|
||||
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Lite-режим)"
|
||||
}
|
||||
|
||||
# ── Stealth-режим ────────────────────────────────────────────────────────────
|
||||
install_stealth_mode() {
|
||||
log_step "Установка Stealth-режима"
|
||||
# ── Pro-режим ────────────────────────────────────────────────────────────────
|
||||
install_pro_mode() {
|
||||
log_step "Установка Pro-режима"
|
||||
|
||||
# Ввод домена
|
||||
echo ""
|
||||
@@ -192,24 +369,32 @@ install_stealth_mode() {
|
||||
template_dir=$(interactive_template_selection)
|
||||
[ $? -ne 0 ] && return
|
||||
|
||||
# Внутренний порт для telemt (только localhost, не открыт наружу)
|
||||
# nginx принимает весь трафик на внешнем 443 и проксирует MTProxy-соединения на localhost:8443
|
||||
# Внешний порт 8443 НЕ затрагивается и НЕ открывается — telemt слушает только 127.0.0.1
|
||||
local telemt_port=8443
|
||||
# Архитектура Pro:
|
||||
# telemt слушает на 0.0.0.0:443 (принимает ВСЕ подключения)
|
||||
# nginx слушает на 127.0.0.1:8443 с SSL (обслуживает сайт)
|
||||
# MTProxy клиент → :443 → telemt (проксирует)
|
||||
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
||||
# Провайдер видит только HTTPS на 443 к домену
|
||||
local nginx_internal_port=8443
|
||||
echo ""
|
||||
echo -e " ${DIM}telemt будет слушать на localhost:$telemt_port (только внутренний, не открыт наружу)${NC}"
|
||||
echo -e " ${DIM}nginx принимает трафик на 443 (внешний) и проксирует к telemt${NC}"
|
||||
echo -e " ${DIM}telemt принимает весь трафик на 443 (маскировка под HTTPS)${NC}"
|
||||
echo -e " ${DIM}nginx обслуживает сайт на внутреннем порту $nginx_internal_port${NC}"
|
||||
echo -e " ${DIM}Провайдер видит только HTTPS-трафик к ${user_domain}:443${NC}"
|
||||
|
||||
# Генерация секрета
|
||||
local secret
|
||||
secret=$(generate_hex 32)
|
||||
# Генерация fake-TLS секрета (ee + secret + hex domain)
|
||||
# Префикс ee говорит Telegram-клиенту маскировать трафик под TLS к домену
|
||||
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 -e " ${BOLD}${WHITE}📋 Конфигурация:${NC}"
|
||||
echo -e " Домен: ${CYAN}${user_domain}${NC}"
|
||||
echo -e " Порт: ${CYAN}443 (nginx) → $telemt_port (telemt)${NC}"
|
||||
echo -e " Режим: ${MAGENTA}Stealth${NC}"
|
||||
echo -e " Порт: ${CYAN}443 (telemt + nginx внутри)${NC}"
|
||||
echo -e " Режим: ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||
echo ""
|
||||
|
||||
if ! confirm "Установить прокси + сайт?"; then
|
||||
@@ -220,11 +405,15 @@ install_stealth_mode() {
|
||||
ensure_deps
|
||||
install_telemt_full || return
|
||||
|
||||
# Конфиг telemt: маскировка на localhost (nginx)
|
||||
generate_telemt_toml "$secret" "$telemt_port" "stealth" "127.0.0.1" "443"
|
||||
# Конфиг telemt: слушает 443, маскировка на локальный nginx через dns_override
|
||||
generate_telemt_toml "$raw_secret" "443" "pro" "$user_domain" "$nginx_internal_port"
|
||||
|
||||
# Настройка сайта (nginx + certbot + шаблон)
|
||||
setup_stealth_mode "$user_domain" "$template_dir" "$telemt_port" "$ssl_email" || return
|
||||
# Настройка сайта (nginx на внутреннем порту + certbot + шаблон)
|
||||
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
|
||||
start_telemt || return
|
||||
@@ -232,22 +421,22 @@ install_stealth_mode() {
|
||||
# Сохраняем конфиг
|
||||
local tpl_id
|
||||
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"
|
||||
|
||||
# Результат
|
||||
show_proxy_info
|
||||
# Результат — используем домен и fake-TLS ссылку
|
||||
show_proxy_info_pro "$user_domain" "$faketls_secret"
|
||||
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() {
|
||||
show_proxy_info
|
||||
|
||||
# Дополнительно для stealth
|
||||
# Дополнительно для pro
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null)
|
||||
if [ "$mode" = "stealth" ]; then
|
||||
if [ "$mode" = "pro" ]; then
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null)
|
||||
if [ -n "$domain" ]; then
|
||||
@@ -265,11 +454,21 @@ menu_status() {
|
||||
|
||||
# ── Ссылка ───────────────────────────────────────────────────────────────────
|
||||
menu_link() {
|
||||
local secret port ip link
|
||||
local secret port ip link mode domain
|
||||
secret=$(get_config_value secret)
|
||||
port=$(get_config_value port)
|
||||
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 -e " ${BOLD}${WHITE}🔗 Ссылка для подключения:${NC}"
|
||||
@@ -284,18 +483,30 @@ menu_link() {
|
||||
|
||||
# ── Поделиться ───────────────────────────────────────────────────────────────
|
||||
menu_share() {
|
||||
local secret port ip link
|
||||
local secret port ip link mode domain server_display
|
||||
secret=$(get_config_value secret)
|
||||
port=$(get_config_value port)
|
||||
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 -e " ${BOLD}📤 Перешлите это сообщение:${NC}"
|
||||
echo ""
|
||||
echo "🔐 MTProxy для Telegram (GoTelegram v${GOTELEGRAM_VERSION})"
|
||||
echo ""
|
||||
echo "🌍 Сервер: $ip"
|
||||
echo "🌍 Сервер: $server_display"
|
||||
echo "🔌 Порт: $port"
|
||||
echo ""
|
||||
echo "👉 Подключиться одним нажатием:"
|
||||
@@ -310,7 +521,7 @@ menu_restart() {
|
||||
restart_telemt
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null)
|
||||
if [ "$mode" = "stealth" ]; then
|
||||
if [ "$mode" = "pro" ]; then
|
||||
restart_nginx
|
||||
fi
|
||||
}
|
||||
@@ -331,16 +542,16 @@ menu_change_mode() {
|
||||
echo ""
|
||||
echo -e " ${WHITE}Текущий режим:${NC} ${CYAN}${current_mode}${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}1)${NC} Сменить шаблон сайта (только stealth)"
|
||||
echo -e " ${CYAN}2)${NC} Переключить режим (quick ↔ stealth)"
|
||||
echo -e " ${CYAN}0)${NC} Назад"
|
||||
echo -e " ${CYAN}1${NC}) Сменить шаблон сайта (только pro)"
|
||||
echo -e " ${CYAN}2${NC}) Переключить режим (lite ↔ pro)"
|
||||
echo -e " ${CYAN}0${NC}) Назад"
|
||||
echo -ne " ${WHITE}Выбор:${NC} "
|
||||
read -r ch
|
||||
|
||||
case "$ch" in
|
||||
1)
|
||||
if [ "$current_mode" != "stealth" ]; then
|
||||
log_error "Смена шаблона доступна только в stealth-режиме"
|
||||
if [ "$current_mode" != "pro" ]; then
|
||||
log_error "Смена шаблона доступна только в pro-режиме"
|
||||
return
|
||||
fi
|
||||
local template_dir
|
||||
@@ -357,12 +568,13 @@ menu_change_mode() {
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Управление сайтом ───────────────────────────────────────────────────────
|
||||
# ── Управление сайтом ────────────────────────────────────────────────────────
|
||||
menu_website() {
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null)
|
||||
if [ "$mode" != "stealth" ]; then
|
||||
log_info "Управление сайтом доступно только в stealth-режиме"
|
||||
|
||||
if [ "$mode" != "pro" ]; then
|
||||
log_info "Управление сайтом доступно только в pro-режиме"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -374,10 +586,10 @@ menu_website() {
|
||||
echo -e " Домен: ${CYAN}${domain}${NC}"
|
||||
echo -e " SSL до: $(get_ssl_expiry "$domain")"
|
||||
echo ""
|
||||
echo -e " ${CYAN}1)${NC} Обновить SSL сертификат"
|
||||
echo -e " ${CYAN}2)${NC} Перезапустить nginx"
|
||||
echo -e " ${CYAN}3)${NC} Сменить шаблон"
|
||||
echo -e " ${CYAN}0)${NC} Назад"
|
||||
echo -e " ${CYAN}1${NC}) Обновить SSL сертификат"
|
||||
echo -e " ${CYAN}2${NC}) Перезапустить nginx"
|
||||
echo -e " ${CYAN}3${NC}) Сменить шаблон"
|
||||
echo -e " ${CYAN}0${NC}) Назад"
|
||||
echo -ne " ${WHITE}Выбор:${NC} "
|
||||
read -r ch
|
||||
|
||||
@@ -398,10 +610,10 @@ menu_remove() {
|
||||
echo ""
|
||||
echo -e " ${BOLD}${RED}🗑 Удаление GoTelegram${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${CYAN}1)${NC} Удалить только прокси (telemt)"
|
||||
echo -e " ${CYAN}2)${NC} Удалить только Telegram-бота"
|
||||
echo -e " ${CYAN}3)${NC} Удалить всё (прокси + бот + настройки)"
|
||||
echo -e " ${CYAN}0)${NC} Назад"
|
||||
echo -e " ${CYAN}1${NC}) Удалить только прокси (telemt)"
|
||||
echo -e " ${CYAN}2${NC}) Удалить только Telegram-бота"
|
||||
echo -e " ${CYAN}3${NC}) Удалить всё (прокси + бот + настройки)"
|
||||
echo -e " ${CYAN}0${NC}) Назад"
|
||||
echo -ne " ${WHITE}Выбор:${NC} "
|
||||
read -r rm_choice
|
||||
|
||||
@@ -415,8 +627,8 @@ menu_remove() {
|
||||
remove_telemt
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null)
|
||||
if [ "$mode" = "stealth" ]; then
|
||||
remove_stealth_mode
|
||||
if [ "$mode" = "pro" ]; then
|
||||
remove_pro_mode
|
||||
fi
|
||||
rm -f "$GOTELEGRAM_CONFIG"
|
||||
log_success "Прокси удалён"
|
||||
@@ -434,8 +646,8 @@ menu_remove() {
|
||||
remove_telemt
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null)
|
||||
if [ "$mode" = "stealth" ]; then
|
||||
remove_stealth_mode
|
||||
if [ "$mode" = "pro" ]; then
|
||||
remove_pro_mode
|
||||
fi
|
||||
rm -f "$GOTELEGRAM_CONFIG"
|
||||
# Бот
|
||||
@@ -477,21 +689,21 @@ menu_bot() {
|
||||
running)
|
||||
echo -e " Статус: ${GREEN}● Работает${NC}"
|
||||
echo ""
|
||||
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} ⚙️ Настройки (.env)"
|
||||
echo -e " ${CYAN}6)${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}) ⚙️ Настройки (.env)"
|
||||
echo -e " ${CYAN}6${NC}) 🗑 Удалить бота"
|
||||
;;
|
||||
stopped)
|
||||
echo -e " Статус: ${YELLOW}○ Остановлен${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}1)${NC} 📊 Статус бота"
|
||||
echo -e " ${CYAN}2)${NC} 📋 Логи бота"
|
||||
echo -e " ${CYAN}3)${NC} ▶️ Запустить бота"
|
||||
echo -e " ${CYAN}5)${NC} ⚙️ Настройки (.env)"
|
||||
echo -e " ${CYAN}6)${NC} 🗑 Удалить бота"
|
||||
echo -e " ${CYAN}1${NC}) 📊 Статус бота"
|
||||
echo -e " ${CYAN}2${NC}) 📋 Логи бота"
|
||||
echo -e " ${CYAN}3${NC}) ▶️ Запустить бота"
|
||||
echo -e " ${CYAN}5${NC}) ⚙️ Настройки (.env)"
|
||||
echo -e " ${CYAN}6${NC}) 🗑 Удалить бота"
|
||||
;;
|
||||
*)
|
||||
echo -e " Статус: ${RED}✗ Не установлен${NC}"
|
||||
@@ -499,11 +711,11 @@ menu_bot() {
|
||||
echo -e " ${DIM}Бот позволяет управлять прокси прямо из Telegram:${NC}"
|
||||
echo -e " ${DIM}статус, перезапуск, смена режима, бекап, QR-код.${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}1)${NC} 🔧 Установить бота"
|
||||
echo -e " ${CYAN}1${NC}) 🔧 Установить бота"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e " ${CYAN}0)${NC} « Назад"
|
||||
echo -e " ${CYAN}0${NC}) « Назад"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -ne " ${WHITE}Выбор:${NC} "
|
||||
read -r ch
|
||||
@@ -587,14 +799,25 @@ bot_install() {
|
||||
[ -z "$token" ] && log_error "Токен не может быть пустым"
|
||||
done
|
||||
|
||||
echo -ne " ${WHITE}ID администратора (Enter = доступ для всех):${NC} "
|
||||
read -r admin_id
|
||||
echo ""
|
||||
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"
|
||||
[ -n "$admin_id" ] && echo "ALLOWED_IDS=$admin_id"
|
||||
[ -n "$admin_ids" ] && echo "ALLOWED_IDS=$admin_ids"
|
||||
} > "$BOT_DIR/.env"
|
||||
|
||||
chmod 600 "$BOT_DIR/.env"
|
||||
log_success ".env создан"
|
||||
else
|
||||
@@ -623,6 +846,59 @@ SVCEOF
|
||||
systemctl enable "$BOT_SERVICE" &>/dev/null
|
||||
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 ""
|
||||
log_success "Бот установлен и запущен!"
|
||||
echo -e " ${DIM}Проверка: systemctl status $BOT_SERVICE${NC}"
|
||||
@@ -676,9 +952,9 @@ bot_edit_config() {
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${CYAN}1)${NC} Сменить BOT_TOKEN"
|
||||
echo -e " ${CYAN}2)${NC} Изменить ALLOWED_IDS"
|
||||
echo -e " ${CYAN}0)${NC} Назад"
|
||||
echo -e " ${CYAN}1${NC}) Сменить BOT_TOKEN"
|
||||
echo -e " ${CYAN}2${NC}) Изменить ALLOWED_IDS"
|
||||
echo -e " ${CYAN}0${NC}) Назад"
|
||||
echo -ne " ${WHITE}Выбор:${NC} "
|
||||
read -r ch
|
||||
|
||||
@@ -696,9 +972,10 @@ bot_edit_config() {
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
echo -ne " ${WHITE}ALLOWED_IDS (через запятую, пусто = все):${NC} "
|
||||
echo -ne " ${WHITE}ALLOWED_IDS (через пробел/запятую, пусто = авто):${NC} "
|
||||
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 [ -n "$new_ids" ]; then
|
||||
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
|
||||
@@ -733,16 +1010,91 @@ bot_remove() {
|
||||
menu_promo() {
|
||||
echo ""
|
||||
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} ${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} Донат: ${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 ""
|
||||
}
|
||||
|
||||
# ── Проверка: показывать ли промо (раз в сутки) ────────────────────────────
|
||||
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() {
|
||||
check_root
|
||||
@@ -753,31 +1105,113 @@ main() {
|
||||
check_os
|
||||
check_disk_space 500
|
||||
|
||||
while true; do
|
||||
show_main_menu
|
||||
read -r choice
|
||||
case "$choice" in
|
||||
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
|
||||
# Промо раз в сутки
|
||||
if should_show_promo; then
|
||||
show_promo_with_qr
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -ne " ${DIM}Нажмите Enter для возврата в меню...${NC}"
|
||||
read -r
|
||||
while true; do
|
||||
clear
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
0
install_gotelegram_bot.sh
Normal file → Executable file
0
install_gotelegram_bot.sh
Normal file → Executable file
0
lib/backup.sh
Normal file → Executable file
0
lib/backup.sh
Normal file → Executable file
6
lib/common.sh
Normal file → Executable file
6
lib/common.sh
Normal file → Executable file
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Общие утилиты
|
||||
# GoTelegram v2.3 — Общие утилиты
|
||||
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
||||
|
||||
# ── Версия ────────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.2.1"
|
||||
GOTELEGRAM_VERSION="2.3.1"
|
||||
GOTELEGRAM_NAME="GoTelegram"
|
||||
|
||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||
@@ -270,7 +270,7 @@ save_gotelegram_config() {
|
||||
{
|
||||
"version": "$GOTELEGRAM_VERSION",
|
||||
"engine": "${1:-telemt}",
|
||||
"mode": "${2:-quick}",
|
||||
"mode": "${2:-lite}",
|
||||
"port": ${3:-443},
|
||||
"secret": "${4:-}",
|
||||
"mask_host": "${5:-google.com}",
|
||||
|
||||
406
lib/stats.sh
Executable file
406
lib/stats.sh
Executable 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
3
lib/telemt.sh
Normal file → Executable file
@@ -174,9 +174,8 @@ LimitNOFILE=65535
|
||||
|
||||
# Безопасность
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/etc/telemt /var/log
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
|
||||
120
lib/telemt_config.sh
Normal file → Executable file
120
lib/telemt_config.sh
Normal file → Executable file
@@ -25,52 +25,49 @@ QUICK_DOMAINS=(
|
||||
"zoom.us"
|
||||
)
|
||||
|
||||
# ── Генерация TOML конфига ───────────────────────────────────────────────────
|
||||
# ── Генерация TOML конфига (telemt v3 формат) ───────────────────────────────
|
||||
generate_telemt_toml() {
|
||||
local secret="$1"
|
||||
local port="${2:-443}"
|
||||
local mask_mode="${3:-quick}" # quick | stealth
|
||||
local mask_host="${4:-google.com}"
|
||||
local mask_mode="${3:-lite}" # lite | pro
|
||||
local mask_domain="${4:-google.com}"
|
||||
local mask_port="${5:-443}"
|
||||
local output="${6:-$TELEMT_CONFIG}"
|
||||
|
||||
mkdir -p "$(dirname "$output")"
|
||||
|
||||
# В stealth-режиме telemt слушает только localhost (трафик идёт через nginx)
|
||||
# В quick-режиме — на всех интерфейсах (клиенты подключаются напрямую)
|
||||
local bind_addr="0.0.0.0"
|
||||
[ "$mask_mode" = "stealth" ] && bind_addr="127.0.0.1"
|
||||
# DNS override для pro: домен резолвится в 127.0.0.1
|
||||
# чтобы mask-трафик шёл на локальный nginx, а не в интернет
|
||||
local dns_line=""
|
||||
if [ "$mask_mode" = "pro" ]; then
|
||||
dns_line="dns_overrides = [\"${mask_domain}:${mask_port}:127.0.0.1\"]"
|
||||
fi
|
||||
|
||||
cat > "$output" << EOTOML
|
||||
# GoTelegram v${GOTELEGRAM_VERSION} — telemt configuration
|
||||
# GoTelegram v${GOTELEGRAM_VERSION} — telemt v3 configuration
|
||||
# Сгенерировано: $(date -Iseconds)
|
||||
# Режим: ${mask_mode}
|
||||
|
||||
# ── Основные настройки ───────────────────────────────────────────────────────
|
||||
[stats]
|
||||
statsd_address = ""
|
||||
[server]
|
||||
port = ${port}
|
||||
listen_addr_ipv4 = "0.0.0.0"
|
||||
|
||||
# ── Секреты ──────────────────────────────────────────────────────────────────
|
||||
[[users]]
|
||||
name = "main"
|
||||
secret = "${secret}"
|
||||
[censorship]
|
||||
tls_domain = "${mask_domain}"
|
||||
mask = true
|
||||
mask_port = ${mask_port}
|
||||
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
|
||||
|
||||
# ── Привязка ─────────────────────────────────────────────────────────────────
|
||||
[listen]
|
||||
# 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}"
|
||||
[access.users]
|
||||
main = "${secret}"
|
||||
|
||||
[network]
|
||||
${dns_line}
|
||||
EOTOML
|
||||
|
||||
chmod 600 "$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"
|
||||
}
|
||||
|
||||
# ── Чтение текущего конфига ──────────────────────────────────────────────────
|
||||
# ── Чтение текущего конфига (telemt v3 формат) ──────────────────────────────
|
||||
get_config_value() {
|
||||
local key="$1"
|
||||
local config="${2:-$TELEMT_CONFIG}"
|
||||
@@ -104,13 +101,19 @@ get_config_value() {
|
||||
|
||||
case "$key" in
|
||||
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)
|
||||
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)
|
||||
grep -A10 '\[security\]' "$config" | grep 'host\s*=' | sed 's/.*=\s*"\(.*\)".*/\1/'
|
||||
mask_host|tls_domain)
|
||||
# [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 ' "'
|
||||
@@ -151,7 +154,7 @@ validate_telemt_config() {
|
||||
fi
|
||||
|
||||
if [ -z "$host" ]; then
|
||||
log_error "Не задан маскировочный хост (security.host)"
|
||||
log_error "Не задан маскировочный хост (censorship.tls_domain)"
|
||||
((errors++))
|
||||
fi
|
||||
|
||||
@@ -254,11 +257,21 @@ show_proxy_info() {
|
||||
port=$(get_config_value port "$config")
|
||||
mask_host=$(get_config_value mask_host "$config")
|
||||
ip=$(get_server_ip)
|
||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||
status=$(telemt_status)
|
||||
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null || echo "quick")
|
||||
local mode domain
|
||||
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
|
||||
case "$status" in
|
||||
@@ -271,7 +284,11 @@ show_proxy_info() {
|
||||
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||
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}${mode}${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
|
||||
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
0
lib/templates_catalog.sh
Normal file → Executable file
26
lib/website.sh
Normal file → Executable file
26
lib/website.sh
Normal file → Executable file
@@ -39,8 +39,9 @@ generate_nginx_config() {
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
|
||||
cat > "$NGINX_SITE_CONF" << 'EONGINX'
|
||||
# GoTelegram v2.2 — nginx config
|
||||
# Обслуживает сайт-маскировку для telemt stealth mode
|
||||
# GoTelegram v2.3 — nginx config
|
||||
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
|
||||
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
@@ -60,8 +61,7 @@ server {
|
||||
}
|
||||
|
||||
server {
|
||||
listen SSL_PORT_PLACEHOLDER ssl http2;
|
||||
listen [::]:SSL_PORT_PLACEHOLDER ssl http2;
|
||||
listen 127.0.0.1:SSL_PORT_PLACEHOLDER ssl http2;
|
||||
server_name DOMAIN_PLACEHOLDER;
|
||||
|
||||
# SSL сертификаты
|
||||
@@ -113,7 +113,7 @@ EONGINX
|
||||
local escaped_domain
|
||||
escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g')
|
||||
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
|
||||
@@ -258,14 +258,14 @@ deploy_template_to_nginx() {
|
||||
log_success "Шаблон развёрнут в $WEBSITE_ROOT"
|
||||
}
|
||||
|
||||
# ── Полная установка stealth-режима ──────────────────────────────────────────
|
||||
setup_stealth_mode() {
|
||||
# ── Полная установка pro-режима ──────────────────────────────────────────────
|
||||
setup_pro_mode() {
|
||||
local domain="$1"
|
||||
local template_dir="$2"
|
||||
local proxy_port="${3:-443}"
|
||||
local email="${4:-}"
|
||||
|
||||
log_step "Настройка stealth-режима"
|
||||
log_step "Настройка pro-режима"
|
||||
|
||||
# 1. Устанавливаем nginx
|
||||
run_with_spinner "Установка nginx" install_nginx || return 1
|
||||
@@ -298,7 +298,7 @@ setup_stealth_mode() {
|
||||
# 8. Показываем благодарности авторам шаблонов
|
||||
show_credits
|
||||
|
||||
log_success "Stealth-режим настроен: https://${domain}"
|
||||
log_success "Pro-режим настроен: https://${domain}"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -322,13 +322,13 @@ restart_nginx() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Удаление stealth-режима ──────────────────────────────────────────────────
|
||||
remove_stealth_mode() {
|
||||
log_info "Удаление stealth-режима..."
|
||||
# ── Удаление pro-режима ──────────────────────────────────────────────────────
|
||||
remove_pro_mode() {
|
||||
log_info "Удаление pro-режима..."
|
||||
rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||
rm -rf "$WEBSITE_ROOT"
|
||||
systemctl restart nginx 2>/dev/null
|
||||
log_success "Stealth-режим удалён (nginx оставлен)"
|
||||
log_success "Pro-режим удалён (nginx оставлен)"
|
||||
}
|
||||
|
||||
# ── Смена шаблона ────────────────────────────────────────────────────────────
|
||||
|
||||
32676
templates_catalog.json
32676
templates_catalog.json
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user