mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-06-13 22:52:46 +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
|
#!/bin/bash
|
||||||
# GoTelegram v2.2.1 — Bootstrap installer (private repo)
|
# GoTelegram Pro — Bootstrap installer for private repo
|
||||||
|
# Downloads all files and launches install.sh
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
TOKEN="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"
|
|
||||||
REPO="anten-ka/gotelegram_pro"
|
REPO="anten-ka/gotelegram_pro"
|
||||||
BRANCH="test"
|
BRANCH="test"
|
||||||
API="https://api.github.com/repos/$REPO"
|
PAT="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"
|
||||||
INSTALL_DIR="/opt/gotelegram"
|
INSTALL_DIR="/opt/gotelegram"
|
||||||
|
API="https://api.github.com/repos/${REPO}/contents"
|
||||||
|
|
||||||
echo -e "\033[1;36m"
|
RED='\033[0;31m'
|
||||||
echo "╔══════════════════════════════════════════╗"
|
GREEN='\033[0;32m'
|
||||||
echo "║ GoTelegram v2.2.1 — Установка ║"
|
CYAN='\033[0;36m'
|
||||||
echo "╚══════════════════════════════════════════╝"
|
YELLOW='\033[1;33m'
|
||||||
echo -e "\033[0m"
|
NC='\033[0m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
|
||||||
# Функция загрузки файла из приватного репо
|
echo ""
|
||||||
dl() {
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
local path="$1" dest="$2"
|
echo -e " ${YELLOW}║${NC} ${BOLD}GoTelegram Pro — Установка${NC} ${YELLOW}║${NC}"
|
||||||
mkdir -p "$(dirname "$dest")"
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
curl -sfL -H "Authorization: token $TOKEN" \
|
echo -e " ${YELLOW}║${NC} MTProxy менеджер с Telegram-ботом ${YELLOW}║${NC}"
|
||||||
-H "Accept: application/vnd.github.v3.raw" \
|
echo -e " ${YELLOW}║${NC} Stealth-режим, 1800+ шаблонов сайтов ${YELLOW}║${NC}"
|
||||||
"$API/contents/$path?ref=$BRANCH" -o "$dest"
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
sed -i 's/\r$//' "$dest"
|
echo ""
|
||||||
echo " ✓ $path"
|
|
||||||
|
# Check root
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo -e " ${RED}✗${NC} Запустите от root: ${CYAN}sudo bash bootstrap.sh${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
for cmd in curl jq; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
echo -e " ${CYAN}↻${NC} Установка $cmd..."
|
||||||
|
apt-get update -qq && apt-get install -y -qq "$cmd" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
download_file() {
|
||||||
|
local remote_path="$1"
|
||||||
|
local local_path="$2"
|
||||||
|
local dir
|
||||||
|
dir=$(dirname "$local_path")
|
||||||
|
mkdir -p "$dir"
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -sL -w "%{http_code}" -o "$local_path" \
|
||||||
|
-H "Authorization: token ${PAT}" \
|
||||||
|
-H "Accept: application/vnd.github.raw" \
|
||||||
|
"${API}/${remote_path}?ref=${BRANCH}")
|
||||||
|
|
||||||
|
if [ "$http_code" != "200" ]; then
|
||||||
|
echo -e " ${RED}✗${NC} Ошибка загрузки ${remote_path} (HTTP ${http_code})"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "📦 Скачиваю файлы..."
|
# File list
|
||||||
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/gotelegram-bot"
|
FILES=(
|
||||||
|
"install.sh"
|
||||||
|
"install_gotelegram_bot.sh"
|
||||||
|
"templates_catalog.json"
|
||||||
|
"lib/common.sh"
|
||||||
|
"lib/telemt.sh"
|
||||||
|
"lib/telemt_config.sh"
|
||||||
|
"lib/backup.sh"
|
||||||
|
"lib/website.sh"
|
||||||
|
"lib/templates_catalog.sh"
|
||||||
|
"lib/stats.sh"
|
||||||
|
"gotelegram-bot/bot.py"
|
||||||
|
"gotelegram-bot/config.example.env"
|
||||||
|
"gotelegram-bot/requirements.txt"
|
||||||
|
"gotelegram-bot/README.md"
|
||||||
|
)
|
||||||
|
|
||||||
# Основные файлы
|
echo -e " ${CYAN}↻${NC} Загрузка файлов в ${INSTALL_DIR}..."
|
||||||
dl "install.sh" "$INSTALL_DIR/install.sh"
|
mkdir -p "${INSTALL_DIR}/lib" "${INSTALL_DIR}/gotelegram-bot"
|
||||||
dl "install_gotelegram_bot.sh" "$INSTALL_DIR/install_gotelegram_bot.sh"
|
|
||||||
dl "templates_catalog.json" "$INSTALL_DIR/templates_catalog.json"
|
|
||||||
|
|
||||||
# Библиотеки
|
failed=0
|
||||||
dl "lib/common.sh" "$INSTALL_DIR/lib/common.sh"
|
for f in "${FILES[@]}"; do
|
||||||
dl "lib/telemt.sh" "$INSTALL_DIR/lib/telemt.sh"
|
if download_file "$f" "${INSTALL_DIR}/${f}"; then
|
||||||
dl "lib/telemt_config.sh" "$INSTALL_DIR/lib/telemt_config.sh"
|
echo -e " ${GREEN}✓${NC} ${f}"
|
||||||
dl "lib/backup.sh" "$INSTALL_DIR/lib/backup.sh"
|
else
|
||||||
dl "lib/website.sh" "$INSTALL_DIR/lib/website.sh"
|
failed=$((failed + 1))
|
||||||
dl "lib/templates_catalog.sh" "$INSTALL_DIR/lib/templates_catalog.sh"
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Бот
|
if [ "$failed" -gt 0 ]; then
|
||||||
dl "gotelegram-bot/bot.py" "$INSTALL_DIR/gotelegram-bot/bot.py"
|
echo ""
|
||||||
dl "gotelegram-bot/config.example.env" "$INSTALL_DIR/gotelegram-bot/config.example.env"
|
echo -e " ${RED}✗${NC} Не удалось загрузить ${failed} файл(ов)"
|
||||||
dl "gotelegram-bot/requirements.txt" "$INSTALL_DIR/gotelegram-bot/requirements.txt"
|
echo -e " ${YELLOW}Проверьте токен доступа и подключение к сети${NC}"
|
||||||
dl "gotelegram-bot/README.md" "$INSTALL_DIR/gotelegram-bot/README.md"
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Права
|
# Fix permissions and line endings
|
||||||
chmod +x "$INSTALL_DIR/install.sh" "$INSTALL_DIR/install_gotelegram_bot.sh"
|
echo -e " ${CYAN}↻${NC} Настройка прав..."
|
||||||
|
chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh"
|
||||||
|
chmod +x "${INSTALL_DIR}"/lib/*.sh
|
||||||
|
sed -i 's/\r$//' "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh" "${INSTALL_DIR}"/lib/*.sh 2>/dev/null
|
||||||
|
|
||||||
# Команда gotelegram
|
# Create symlink
|
||||||
ln -sf "$INSTALL_DIR/install.sh" /usr/local/bin/gotelegram
|
ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
|
||||||
chmod +x /usr/local/bin/gotelegram
|
echo -e " ${GREEN}✓${NC} Команда ${CYAN}gotelegram${NC} доступна"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "\033[1;32m✅ Все 13 файлов скачаны в $INSTALL_DIR\033[0m"
|
echo -e " ${GREEN}✓${NC} Установка завершена! Запуск..."
|
||||||
echo -e "\033[1;33m💡 Команда для запуска меню: gotelegram\033[0m"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Запускаем меню
|
# Launch
|
||||||
exec bash "$INSTALL_DIR/install.sh"
|
exec bash "${INSTALL_DIR}/install.sh"
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ Uses python-telegram-bot v21+
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import csv
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
import toml
|
import toml
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple, Optional, List, Dict, Any
|
from typing import Tuple, Optional, List, Dict, Any
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
GOTELEGRAM_VERSION = "2.2.0"
|
GOTELEGRAM_VERSION = "2.3.1"
|
||||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||||
TELEMT_SERVICE = "telemt"
|
TELEMT_SERVICE = "telemt"
|
||||||
@@ -55,21 +58,75 @@ WEBSITE_ROOT = "/var/www/gotelegram-site"
|
|||||||
BACKUP_DIR = "/opt/gotelegram/backups"
|
BACKUP_DIR = "/opt/gotelegram/backups"
|
||||||
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
||||||
|
|
||||||
PROMO_LINK = "https://vk.cc/ct29NQ"
|
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
|
||||||
|
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
|
||||||
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
||||||
|
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
|
||||||
|
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
ALLOWED_IDS_STR = os.getenv("ALLOWED_IDS", "")
|
ENV_FILE = "/opt/gotelegram-bot/.env"
|
||||||
ALLOWED_IDS: set = set()
|
|
||||||
for _id_str in ALLOWED_IDS_STR.split(","):
|
|
||||||
_id_str = _id_str.strip()
|
|
||||||
if _id_str:
|
|
||||||
try:
|
|
||||||
ALLOWED_IDS.add(int(_id_str))
|
|
||||||
except ValueError:
|
|
||||||
logging.warning(f"Invalid ALLOWED_IDS entry: {_id_str}")
|
|
||||||
|
|
||||||
QUICK_DOMAINS = [
|
# ── Загрузка ALLOWED_IDS ────────────────────────────────────────────────────
|
||||||
|
# Поддерживает запятую, пробел, или их комбинацию как разделитель
|
||||||
|
ALLOWED_IDS: set = set()
|
||||||
|
_WAITING_FOR_ADMIN = False # True если список пуст → ждём первого админа
|
||||||
|
|
||||||
|
|
||||||
|
def _load_allowed_ids() -> None:
|
||||||
|
"""Загрузить ALLOWED_IDS из переменной окружения."""
|
||||||
|
global ALLOWED_IDS, _WAITING_FOR_ADMIN
|
||||||
|
raw = os.getenv("ALLOWED_IDS", "")
|
||||||
|
ALLOWED_IDS = set()
|
||||||
|
# Разделители: запятая, пробел, или оба
|
||||||
|
for part in re.split(r'[,\s]+', raw):
|
||||||
|
part = part.strip()
|
||||||
|
if part:
|
||||||
|
try:
|
||||||
|
ALLOWED_IDS.add(int(part))
|
||||||
|
except ValueError:
|
||||||
|
logging.warning(f"Invalid ALLOWED_IDS entry: {part}")
|
||||||
|
_WAITING_FOR_ADMIN = len(ALLOWED_IDS) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _save_allowed_ids() -> None:
|
||||||
|
"""Сохранить ALLOWED_IDS в .env файл и обновить os.environ."""
|
||||||
|
global _WAITING_FOR_ADMIN
|
||||||
|
ids_str = ",".join(str(i) for i in sorted(ALLOWED_IDS))
|
||||||
|
os.environ["ALLOWED_IDS"] = ids_str
|
||||||
|
_WAITING_FOR_ADMIN = len(ALLOWED_IDS) == 0
|
||||||
|
|
||||||
|
if not os.path.exists(ENV_FILE):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(ENV_FILE, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
found = False
|
||||||
|
new_lines = []
|
||||||
|
for line in lines:
|
||||||
|
if line.strip().startswith("ALLOWED_IDS="):
|
||||||
|
if ids_str:
|
||||||
|
new_lines.append(f"ALLOWED_IDS={ids_str}\n")
|
||||||
|
# Если пусто — удаляем строку
|
||||||
|
found = True
|
||||||
|
else:
|
||||||
|
new_lines.append(line)
|
||||||
|
|
||||||
|
if not found and ids_str:
|
||||||
|
new_lines.append(f"ALLOWED_IDS={ids_str}\n")
|
||||||
|
|
||||||
|
with open(ENV_FILE, "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
|
||||||
|
logger.info(f"ALLOWED_IDS updated in .env: {ids_str or '(empty)'}")
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Failed to update .env: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
_load_allowed_ids()
|
||||||
|
|
||||||
|
LITE_DOMAINS = [
|
||||||
"google.com",
|
"google.com",
|
||||||
"microsoft.com",
|
"microsoft.com",
|
||||||
"cloudflare.com",
|
"cloudflare.com",
|
||||||
@@ -225,21 +282,41 @@ async def check_old_container() -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def is_user_allowed(user_id: int) -> bool:
|
def is_user_allowed(user_id: int) -> bool:
|
||||||
"""Check if user ID is in ALLOWED_IDS."""
|
"""Check if user ID is in ALLOWED_IDS. If list is empty — waiting for admin."""
|
||||||
if not ALLOWED_IDS:
|
if _WAITING_FOR_ADMIN:
|
||||||
return True
|
return False # Никому не даём доступ пока не назначен админ
|
||||||
return user_id in ALLOWED_IDS
|
return user_id in ALLOWED_IDS
|
||||||
|
|
||||||
|
|
||||||
|
def add_admin(user_id: int) -> None:
|
||||||
|
"""Добавить администратора и сохранить в .env."""
|
||||||
|
ALLOWED_IDS.add(user_id)
|
||||||
|
_save_allowed_ids()
|
||||||
|
logger.info(f"Admin added: {user_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_admin(user_id: int) -> None:
|
||||||
|
"""Убрать администратора и сохранить в .env."""
|
||||||
|
ALLOWED_IDS.discard(user_id)
|
||||||
|
_save_allowed_ids()
|
||||||
|
logger.info(f"Admin removed: {user_id}")
|
||||||
|
|
||||||
|
|
||||||
async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
||||||
"""Check authorization and send error if not allowed."""
|
"""Check authorization and send error if not allowed."""
|
||||||
if not is_user_allowed(update.effective_user.id):
|
user_id = update.effective_user.id
|
||||||
await update.message.reply_text(
|
|
||||||
f"Access denied. Your ID: {update.effective_user.id}"
|
# Режим ожидания первого админа — обрабатывается в cmd_start
|
||||||
)
|
if _WAITING_FOR_ADMIN:
|
||||||
logger.warning(
|
return False
|
||||||
f"Unauthorized access attempt from user {update.effective_user.id}"
|
|
||||||
)
|
if not is_user_allowed(user_id):
|
||||||
|
if update.message:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{user_id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
logger.warning(f"Unauthorized access attempt from user {user_id}")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -274,13 +351,19 @@ def get_main_menu() -> InlineKeyboardMarkup:
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"),
|
InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"),
|
||||||
InlineKeyboardButton("🎁 Promo", callback_data="menu_promo"),
|
InlineKeyboardButton("🎁 Промо", callback_data="menu_promo"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
InlineKeyboardButton("📊 Traffic Stats", callback_data="menu_stats"),
|
||||||
InlineKeyboardButton("🗑️ Remove", callback_data="menu_remove"),
|
InlineKeyboardButton("🗑️ Remove", callback_data="menu_remove"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("👤 Админы", callback_data="menu_admins"),
|
||||||
InlineKeyboardButton("ℹ️ Credits", callback_data="menu_credits"),
|
InlineKeyboardButton("ℹ️ Credits", callback_data="menu_credits"),
|
||||||
],
|
],
|
||||||
[InlineKeyboardButton("❌ Close", callback_data="close_menu")],
|
[
|
||||||
|
InlineKeyboardButton("❌ Close", callback_data="close_menu"),
|
||||||
|
],
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(buttons)
|
return InlineKeyboardMarkup(buttons)
|
||||||
|
|
||||||
@@ -291,8 +374,37 @@ def get_main_menu() -> InlineKeyboardMarkup:
|
|||||||
|
|
||||||
|
|
||||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Start command - show main menu."""
|
"""Start command - show main menu, promo once per day.
|
||||||
if not await require_auth(update, context):
|
|
||||||
|
Если ALLOWED_IDS пуст — режим авто-регистрации первого админа.
|
||||||
|
"""
|
||||||
|
user = update.effective_user
|
||||||
|
user_id = user.id
|
||||||
|
|
||||||
|
# ── Режим ожидания первого админа ──
|
||||||
|
if _WAITING_FOR_ADMIN:
|
||||||
|
name = user.full_name or user.username or str(user_id)
|
||||||
|
text = (
|
||||||
|
f"<b>👋 Привет, {html.escape(name)}!</b>\n\n"
|
||||||
|
f"Бот ещё не настроен.\n"
|
||||||
|
f"Ваш Telegram ID: <code>{user_id}</code>\n\n"
|
||||||
|
f"Назначить вас администратором?"
|
||||||
|
)
|
||||||
|
keyboard = InlineKeyboardMarkup([
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("✅ Да", callback_data=f"admin_confirm_{user_id}"),
|
||||||
|
InlineKeyboardButton("❌ Нет", callback_data="admin_cancel"),
|
||||||
|
]
|
||||||
|
])
|
||||||
|
await update.message.reply_text(text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Проверка доступа ──
|
||||||
|
if not is_user_allowed(user_id):
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{user_id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
welcome = (
|
welcome = (
|
||||||
@@ -305,6 +417,13 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
welcome, reply_markup=get_main_menu(), parse_mode="HTML"
|
welcome, reply_markup=get_main_menu(), parse_mode="HTML"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Промо раз в сутки
|
||||||
|
if should_show_promo_bot():
|
||||||
|
mark_promo_shown_bot()
|
||||||
|
await update.message.reply_text(
|
||||||
|
get_promo_text(), parse_mode="HTML"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Help command - show available commands."""
|
"""Help command - show available commands."""
|
||||||
@@ -312,12 +431,14 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
help_text = (
|
help_text = (
|
||||||
"<b>GoTelegram Bot Commands</b>\n\n"
|
"<b>GoTelegram Bot — Команды</b>\n\n"
|
||||||
"/start - Show main menu\n"
|
"/start — Главное меню\n"
|
||||||
"/help - Show this help message\n"
|
"/help — Эта справка\n"
|
||||||
"/status - Quick status check\n"
|
"/status — Быстрый статус\n"
|
||||||
"/logs - Show recent logs\n\n"
|
"/logs — Последние логи\n"
|
||||||
"Use the inline menu for all other operations."
|
"/addadmin ID — Добавить админа\n"
|
||||||
|
"/deladmin ID — Удалить админа\n\n"
|
||||||
|
"Используйте кнопки меню для остальных операций."
|
||||||
)
|
)
|
||||||
await update.message.reply_text(help_text, parse_mode="HTML")
|
await update.message.reply_text(help_text, parse_mode="HTML")
|
||||||
|
|
||||||
@@ -403,19 +524,135 @@ async def get_status_text() -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def get_traffic_stats() -> str:
|
||||||
"""Status callback."""
|
"""Get formatted traffic statistics."""
|
||||||
|
# Read current snapshot
|
||||||
|
current_file = "/run/gotelegram/stats_current.json"
|
||||||
|
history_file = "/opt/gotelegram/stats_history.csv"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(current_file, "r") as f:
|
||||||
|
current = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return "📊 <b>Статистика</b>\n\n<i>Данные недоступны. Убедитесь что модуль статистики включён.</i>"
|
||||||
|
|
||||||
|
# Read history
|
||||||
|
history = []
|
||||||
|
try:
|
||||||
|
with open(history_file, "r") as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
for row in reader:
|
||||||
|
if len(row) >= 3:
|
||||||
|
history.append({
|
||||||
|
"ts": int(row[0]),
|
||||||
|
"proxy": int(row[1]),
|
||||||
|
"site": int(row[2]),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
now = int(time.time())
|
||||||
|
|
||||||
|
def format_bytes(b):
|
||||||
|
if b < 1024:
|
||||||
|
return f"{b} B"
|
||||||
|
if b < 1048576:
|
||||||
|
return f"{b/1024:.1f} KB"
|
||||||
|
if b < 1073741824:
|
||||||
|
return f"{b/1048576:.1f} MB"
|
||||||
|
return f"{b/1073741824:.1f} GB"
|
||||||
|
|
||||||
|
def format_rate(bps):
|
||||||
|
if bps < 1024:
|
||||||
|
return f"{bps:.0f} B/s"
|
||||||
|
if bps < 1048576:
|
||||||
|
return f"{bps/1024:.1f} KB/s"
|
||||||
|
return f"{bps/1048576:.1f} MB/s"
|
||||||
|
|
||||||
|
def calc_for_period(secs, key):
|
||||||
|
target_ts = now - secs
|
||||||
|
# Find closest snapshot to target_ts
|
||||||
|
closest = None
|
||||||
|
for h in history:
|
||||||
|
if h["ts"] <= target_ts:
|
||||||
|
if closest is None or h["ts"] > closest["ts"]:
|
||||||
|
closest = h
|
||||||
|
if closest is None:
|
||||||
|
return "—", "—"
|
||||||
|
|
||||||
|
current_val = current.get(f"{key}_bytes", 0)
|
||||||
|
diff = current_val - closest[key]
|
||||||
|
if diff < 0:
|
||||||
|
diff = 0
|
||||||
|
elapsed = now - closest["ts"]
|
||||||
|
if elapsed <= 0:
|
||||||
|
elapsed = 1
|
||||||
|
rate = diff / elapsed
|
||||||
|
return format_bytes(diff), format_rate(rate)
|
||||||
|
|
||||||
|
periods = [
|
||||||
|
("1 мин", 60),
|
||||||
|
("5 мин", 300),
|
||||||
|
("60 мин", 3600),
|
||||||
|
("1 день", 86400),
|
||||||
|
("7 дней", 604800),
|
||||||
|
("30 дней", 2592000),
|
||||||
|
("365 дней", 31536000),
|
||||||
|
]
|
||||||
|
|
||||||
|
lines = ["📊 <b>Статистика трафика</b>\n"]
|
||||||
|
|
||||||
|
for label, key in [("Proxy (telemt)", "proxy"), ("Сайт (nginx)", "site")]:
|
||||||
|
lines.append(f"\n<b>{label}:</b>")
|
||||||
|
lines.append("<pre>")
|
||||||
|
lines.append(f"{'Период':<10} │ {'Трафик':>10} │ {'Скорость':>10}")
|
||||||
|
lines.append("─" * 36)
|
||||||
|
for name, secs in periods:
|
||||||
|
total, rate = calc_for_period(secs, key)
|
||||||
|
lines.append(f"{name:<10} │ {total:>10} │ {rate:>10}")
|
||||||
|
lines.append("</pre>")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
async def cb_menu_stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Show traffic statistics."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
await safe_edit_message(query,"⏳ Checking status...")
|
stats_text = await get_traffic_stats()
|
||||||
|
|
||||||
status_text = await get_status_text()
|
keyboard = [
|
||||||
keyboard = InlineKeyboardMarkup(
|
[InlineKeyboardButton("🔄 Обновить", callback_data="menu_stats")],
|
||||||
[[InlineKeyboardButton("« Back", callback_data="menu_main")]]
|
[InlineKeyboardButton("« Меню", callback_data="menu_main")],
|
||||||
|
]
|
||||||
|
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
stats_text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(keyboard),
|
||||||
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
await safe_edit_message(query,
|
|
||||||
status_text, reply_markup=keyboard, parse_mode="HTML"
|
|
||||||
|
async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Status callback — show detailed proxy/server status."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
if not await require_auth(update, context):
|
||||||
|
return
|
||||||
|
|
||||||
|
text = await get_status_text()
|
||||||
|
keyboard = [
|
||||||
|
[InlineKeyboardButton("🔄 Обновить", callback_data="menu_status")],
|
||||||
|
[InlineKeyboardButton("« Меню", callback_data="menu_main")],
|
||||||
|
]
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(keyboard),
|
||||||
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -427,8 +664,8 @@ async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
def get_install_mode_menu() -> InlineKeyboardMarkup:
|
def get_install_mode_menu() -> InlineKeyboardMarkup:
|
||||||
"""Install mode selection menu."""
|
"""Install mode selection menu."""
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton("⚡ Quick Mode", callback_data="install_mode_quick")],
|
[InlineKeyboardButton("⚡ Lite", callback_data="install_mode_lite")],
|
||||||
[InlineKeyboardButton("🔒 Stealth Mode", callback_data="install_mode_stealth")],
|
[InlineKeyboardButton("🛡 Pro", callback_data="install_mode_pro")],
|
||||||
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(buttons)
|
return InlineKeyboardMarkup(buttons)
|
||||||
@@ -452,7 +689,7 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
)
|
)
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton("🔄 Migrate from v1", callback_data="install_migrate")],
|
[InlineKeyboardButton("🔄 Migrate from v1", callback_data="install_migrate")],
|
||||||
[InlineKeyboardButton("✨ Fresh Install", callback_data="install_mode_quick")],
|
[InlineKeyboardButton("✨ Fresh Install", callback_data="install_mode_lite")],
|
||||||
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
||||||
]
|
]
|
||||||
keyboard = InlineKeyboardMarkup(buttons)
|
keyboard = InlineKeyboardMarkup(buttons)
|
||||||
@@ -465,39 +702,39 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def cb_install_mode_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_install_mode_lite(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Quick mode domain selection."""
|
"""Lite mode domain selection."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
# Show domains with pagination (4 per row, 2 rows)
|
# Show domains with pagination (4 per row, 2 rows)
|
||||||
buttons = []
|
buttons = []
|
||||||
for i in range(0, len(QUICK_DOMAINS), 2):
|
for i in range(0, len(LITE_DOMAINS), 2):
|
||||||
row = []
|
row = []
|
||||||
for j in range(2):
|
for j in range(2):
|
||||||
if i + j < len(QUICK_DOMAINS):
|
if i + j < len(LITE_DOMAINS):
|
||||||
domain = QUICK_DOMAINS[i + j]
|
domain = LITE_DOMAINS[i + j]
|
||||||
row.append(
|
row.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
domain, callback_data=f"quick_dom_{i+j}"
|
domain, callback_data=f"lite_dom_{i+j}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
buttons.append(row)
|
buttons.append(row)
|
||||||
|
|
||||||
buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")])
|
buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")])
|
||||||
|
|
||||||
text = "Select a domain for quick mode:"
|
text = "Select a domain for Lite mode:"
|
||||||
keyboard = InlineKeyboardMarkup(buttons)
|
keyboard = InlineKeyboardMarkup(buttons)
|
||||||
await safe_edit_message(query,text, reply_markup=keyboard)
|
await safe_edit_message(query,text, reply_markup=keyboard)
|
||||||
|
|
||||||
|
|
||||||
async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Quick domain selection callback."""
|
"""Lite domain selection callback."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
try:
|
try:
|
||||||
domain_idx = int(data.split("_")[-1])
|
domain_idx = int(data.split("_")[-1])
|
||||||
domain = QUICK_DOMAINS[domain_idx]
|
domain = LITE_DOMAINS[domain_idx]
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
await query.answer("Invalid domain selection")
|
await query.answer("Invalid domain selection")
|
||||||
return
|
return
|
||||||
@@ -507,7 +744,7 @@ async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
# Simulate installation (in real scenario, call install script)
|
# Simulate installation (in real scenario, call install script)
|
||||||
config = {
|
config = {
|
||||||
"mode": "quick",
|
"mode": "lite",
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"port": 443,
|
"port": 443,
|
||||||
"installed_at": datetime.now().isoformat(),
|
"installed_at": datetime.now().isoformat(),
|
||||||
@@ -515,9 +752,9 @@ async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
if save_json(GOTELEGRAM_CONFIG, config):
|
if save_json(GOTELEGRAM_CONFIG, config):
|
||||||
text = (
|
text = (
|
||||||
f"✅ <b>Quick mode installed!</b>\n\n"
|
f"✅ <b>Lite mode installed!</b>\n\n"
|
||||||
f"<b>Domain:</b> {domain}\n"
|
f"<b>Domain:</b> {domain}\n"
|
||||||
f"<b>Mode:</b> Quick\n\n"
|
f"<b>Mode:</b> Lite\n\n"
|
||||||
f"Service starting... Check status in 10 seconds."
|
f"Service starting... Check status in 10 seconds."
|
||||||
)
|
)
|
||||||
keyboard = InlineKeyboardMarkup(
|
keyboard = InlineKeyboardMarkup(
|
||||||
@@ -535,8 +772,8 @@ async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def cb_install_mode_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_install_mode_pro(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Stealth mode - show template categories."""
|
"""Pro mode - show template categories."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
@@ -555,22 +792,22 @@ async def cb_install_mode_stealth(update: Update, context: ContextTypes.DEFAULT_
|
|||||||
buttons.append(
|
buttons.append(
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
f"📁 {cat['name']}", callback_data=f"stealth_cat_{cat['id']}"
|
f"📁 {cat['name']}", callback_data=f"pro_cat_{cat['id']}"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")])
|
buttons.append([InlineKeyboardButton("« Back", callback_data="menu_install")])
|
||||||
|
|
||||||
text = "Stealth Mode - Select Template Category:"
|
text = "Pro Mode - Select Template Category:"
|
||||||
keyboard = InlineKeyboardMarkup(buttons)
|
keyboard = InlineKeyboardMarkup(buttons)
|
||||||
await safe_edit_message(query,text, reply_markup=keyboard)
|
await safe_edit_message(query,text, reply_markup=keyboard)
|
||||||
|
|
||||||
|
|
||||||
async def cb_stealth_category(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_pro_category(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Show templates in category."""
|
"""Show templates in category."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
cat_id = data.removeprefix("stealth_cat_")
|
cat_id = data.removeprefix("pro_cat_")
|
||||||
|
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
@@ -597,22 +834,22 @@ async def cb_stealth_category(update: Update, context: ContextTypes.DEFAULT_TYPE
|
|||||||
buttons.append(
|
buttons.append(
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
f"🎨 {tpl['name']}", callback_data=f"stealth_tpl_{tpl['id']}"
|
f"🎨 {tpl['name']}", callback_data=f"pro_tpl_{tpl['id']}"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
buttons.append([InlineKeyboardButton("« Back", callback_data="install_mode_stealth")])
|
buttons.append([InlineKeyboardButton("« Back", callback_data="install_mode_pro")])
|
||||||
|
|
||||||
text = f"Select template from <b>{html.escape(category['name'])}</b>:"
|
text = f"Select template from <b>{html.escape(category['name'])}</b>:"
|
||||||
keyboard = InlineKeyboardMarkup(buttons)
|
keyboard = InlineKeyboardMarkup(buttons)
|
||||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
async def cb_stealth_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_pro_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Show template preview and confirm."""
|
"""Show template preview and confirm."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
tpl_id = data.removeprefix("stealth_tpl_")
|
tpl_id = data.removeprefix("pro_tpl_")
|
||||||
|
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
@@ -651,26 +888,26 @@ async def cb_stealth_template(update: Update, context: ContextTypes.DEFAULT_TYPE
|
|||||||
buttons = [
|
buttons = [
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
"✅ Install", callback_data=f"stealth_confirm_{tpl_id}"
|
"✅ Install", callback_data=f"pro_confirm_{tpl_id}"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[InlineKeyboardButton("« Back", callback_data="install_mode_stealth")],
|
[InlineKeyboardButton("« Back", callback_data="install_mode_pro")],
|
||||||
]
|
]
|
||||||
keyboard = InlineKeyboardMarkup(buttons)
|
keyboard = InlineKeyboardMarkup(buttons)
|
||||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Confirm and install stealth template."""
|
"""Confirm and install pro template."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
tpl_id = data.removeprefix("stealth_confirm_")
|
tpl_id = data.removeprefix("pro_confirm_")
|
||||||
|
|
||||||
await query.answer()
|
await query.answer()
|
||||||
await safe_edit_message(query,"⏳ Installing template...")
|
await safe_edit_message(query,"⏳ Installing template...")
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"mode": "stealth",
|
"mode": "pro",
|
||||||
"template": tpl_id,
|
"template": tpl_id,
|
||||||
"port": 443,
|
"port": 443,
|
||||||
"installed_at": datetime.now().isoformat(),
|
"installed_at": datetime.now().isoformat(),
|
||||||
@@ -678,9 +915,9 @@ async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
|
|
||||||
if save_json(GOTELEGRAM_CONFIG, config):
|
if save_json(GOTELEGRAM_CONFIG, config):
|
||||||
text = (
|
text = (
|
||||||
f"✅ <b>Stealth mode installed!</b>\n\n"
|
f"✅ <b>Pro mode installed!</b>\n\n"
|
||||||
f"<b>Template:</b> {html.escape(tpl_id)}\n"
|
f"<b>Template:</b> {html.escape(tpl_id)}\n"
|
||||||
f"<b>Mode:</b> Stealth\n\n"
|
f"<b>Mode:</b> Pro\n\n"
|
||||||
f"Service starting... Check status in 10 seconds."
|
f"Service starting... Check status in 10 seconds."
|
||||||
)
|
)
|
||||||
keyboard = InlineKeyboardMarkup(
|
keyboard = InlineKeyboardMarkup(
|
||||||
@@ -704,7 +941,7 @@ async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
|
|
||||||
|
|
||||||
async def get_proxy_link() -> Optional[str]:
|
async def get_proxy_link() -> Optional[str]:
|
||||||
"""Generate proxy link from config."""
|
"""Generate proxy link from config. Pro-mode uses domain + fake-TLS secret."""
|
||||||
config = load_json(GOTELEGRAM_CONFIG)
|
config = load_json(GOTELEGRAM_CONFIG)
|
||||||
if not config:
|
if not config:
|
||||||
return None
|
return None
|
||||||
@@ -720,12 +957,20 @@ async def get_proxy_link() -> Optional[str]:
|
|||||||
if not secret:
|
if not secret:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get server IP
|
mode = config.get("mode", "lite")
|
||||||
|
domain = config.get("domain", "")
|
||||||
|
port = config.get("port", 443)
|
||||||
|
|
||||||
|
# Pro-режим: ссылка с доменом и fake-TLS секретом (ee + secret + hex domain)
|
||||||
|
if mode == "pro" and domain:
|
||||||
|
domain_hex = domain.encode().hex()
|
||||||
|
faketls_secret = f"ee{secret}{domain_hex}"
|
||||||
|
return f"tg://proxy?server={domain}&port={port}&secret={faketls_secret}"
|
||||||
|
|
||||||
|
# Lite-режим: IP
|
||||||
code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
|
code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
|
||||||
server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
|
server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
|
||||||
|
|
||||||
port = config.get("port", 443)
|
|
||||||
|
|
||||||
return f"tg://proxy?server={server}&port={port}&secret={secret}"
|
return f"tg://proxy?server={server}&port={port}&secret={secret}"
|
||||||
|
|
||||||
|
|
||||||
@@ -1074,8 +1319,8 @@ async def cb_menu_change(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton("⚡ Switch to Quick Mode", callback_data="change_quick")],
|
[InlineKeyboardButton("⚡ Switch to Lite Mode", callback_data="change_lite")],
|
||||||
[InlineKeyboardButton("🔒 Switch to Stealth Mode", callback_data="change_stealth")],
|
[InlineKeyboardButton("🛡 Switch to Pro Mode", callback_data="change_pro")],
|
||||||
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
[InlineKeyboardButton("« Back", callback_data="menu_main")],
|
||||||
]
|
]
|
||||||
keyboard = InlineKeyboardMarkup(buttons)
|
keyboard = InlineKeyboardMarkup(buttons)
|
||||||
@@ -1084,20 +1329,20 @@ async def cb_menu_change(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def cb_change_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_change_lite(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Switch to quick mode — show domain selection."""
|
"""Switch to lite mode — show domain selection."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
# Reuse the quick mode domain selection flow
|
# Reuse the lite mode domain selection flow
|
||||||
await cb_install_mode_quick(update, context)
|
await cb_install_mode_lite(update, context)
|
||||||
|
|
||||||
|
|
||||||
async def cb_change_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_change_pro(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Switch to stealth mode — show template categories."""
|
"""Switch to pro mode — show template categories."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
# Reuse the stealth mode template selection flow
|
# Reuse the pro mode template selection flow
|
||||||
await cb_install_mode_stealth(update, context)
|
await cb_install_mode_pro(update, context)
|
||||||
|
|
||||||
|
|
||||||
async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
@@ -1183,28 +1428,170 @@ async def cb_ssl_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
|||||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ADMIN MANAGEMENT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def cb_menu_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Показать список админов и кнопки управления."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
if ALLOWED_IDS:
|
||||||
|
ids_list = "\n".join(f" • <code>{uid}</code>" for uid in sorted(ALLOWED_IDS))
|
||||||
|
text = f"<b>👤 Администраторы</b>\n\n{ids_list}\n"
|
||||||
|
else:
|
||||||
|
text = "<b>👤 Администраторы</b>\n\n<i>Список пуст — доступ для всех</i>\n"
|
||||||
|
|
||||||
|
text += (
|
||||||
|
f"\nВсего: {len(ALLOWED_IDS)}\n\n"
|
||||||
|
"Чтобы <b>добавить</b> — перешлите любое сообщение от нового админа, "
|
||||||
|
"или отправьте команду:\n"
|
||||||
|
"<code>/addadmin 123456789</code>\n\n"
|
||||||
|
"Чтобы <b>удалить</b>:\n"
|
||||||
|
"<code>/deladmin 123456789</code>"
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard = InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton("« Назад", callback_data="menu_main")],
|
||||||
|
])
|
||||||
|
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_addadmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/addadmin ID [ID2 ID3 ...] — добавить админа вручную."""
|
||||||
|
if not is_user_allowed(update.effective_user.id):
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{update.effective_user.id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
args = context.args or []
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Использование: <code>/addadmin ID [ID2 ID3 ...]</code>\n"
|
||||||
|
"Пример: <code>/addadmin 123456789 987654321</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
added = []
|
||||||
|
errors = []
|
||||||
|
for a in args:
|
||||||
|
a = a.strip().replace(",", "")
|
||||||
|
if not a:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
uid = int(a)
|
||||||
|
add_admin(uid)
|
||||||
|
added.append(str(uid))
|
||||||
|
except ValueError:
|
||||||
|
errors.append(a)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if added:
|
||||||
|
parts.append(f"✅ Добавлены: {', '.join(added)}")
|
||||||
|
if errors:
|
||||||
|
parts.append(f"❌ Ошибки: {', '.join(errors)}")
|
||||||
|
|
||||||
|
await update.message.reply_text("\n".join(parts), parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_deladmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/deladmin ID — удалить админа."""
|
||||||
|
if not is_user_allowed(update.effective_user.id):
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{update.effective_user.id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
args = context.args or []
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Использование: <code>/deladmin ID</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
removed = []
|
||||||
|
for a in args:
|
||||||
|
a = a.strip().replace(",", "")
|
||||||
|
try:
|
||||||
|
uid = int(a)
|
||||||
|
if uid == update.effective_user.id:
|
||||||
|
await update.message.reply_text("⚠️ Нельзя удалить себя!")
|
||||||
|
continue
|
||||||
|
if uid in ALLOWED_IDS:
|
||||||
|
remove_admin(uid)
|
||||||
|
removed.append(str(uid))
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(f"ID {uid} не найден в списке")
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text(f"❌ Некорректный ID: {html.escape(a)}")
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
await update.message.reply_text(f"✅ Удалены: {', '.join(removed)}")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PROMO & CREDITS
|
# PROMO & CREDITS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def get_promo_text() -> str:
|
||||||
|
"""Return promo text with 2 hosters + donate."""
|
||||||
|
return (
|
||||||
|
"<b>💰 Хостинг #1 — скидка до 60%</b>\n"
|
||||||
|
f"<a href='{PROMO_LINK_1}'>{PROMO_LINK_1}</a>\n\n"
|
||||||
|
"<b>Промокоды:</b>\n"
|
||||||
|
" <code>OFF60</code> — 60% на первый месяц\n"
|
||||||
|
" <code>antenka20</code> — 20% + 3% за 3 мес\n"
|
||||||
|
" <code>antenka6</code> — 15% + 5% за 6 мес\n\n"
|
||||||
|
"━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||||
|
"<b>💰 Хостинг #2 — скидка до 60%</b>\n"
|
||||||
|
f"<a href='{PROMO_LINK_2}'>{PROMO_LINK_2}</a>\n\n"
|
||||||
|
"<b>Промокод:</b>\n"
|
||||||
|
" <code>OFF60</code> — 60% на первый месяц\n\n"
|
||||||
|
"━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||||
|
"<b>☕ Донат / Чаевые</b>\n"
|
||||||
|
f"<a href='{TIP_LINK}'>{TIP_LINK}</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def should_show_promo_bot() -> bool:
|
||||||
|
"""Check if promo should be shown (once per 24h)."""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(PROMO_STAMP_FILE):
|
||||||
|
return True
|
||||||
|
with open(PROMO_STAMP_FILE, "r") as f:
|
||||||
|
last_ts = int(f.read().strip())
|
||||||
|
return (int(time.time()) - last_ts) >= 86400
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def mark_promo_shown_bot() -> None:
|
||||||
|
"""Mark promo as shown."""
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(PROMO_STAMP_FILE), exist_ok=True)
|
||||||
|
with open(PROMO_STAMP_FILE, "w") as f:
|
||||||
|
f.write(str(int(time.time())))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Promo information."""
|
"""Promo information — always shown from menu."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
text = (
|
|
||||||
f"<b>🎁 GoTelegram Promo</b>\n\n"
|
|
||||||
f"Share the love! Invite friends to use GoTelegram.\n\n"
|
|
||||||
f"<a href='{PROMO_LINK}'>Promo Link</a>\n\n"
|
|
||||||
f"Support development:\n"
|
|
||||||
f"<a href='{TIP_LINK}'>Send a Tip</a>"
|
|
||||||
)
|
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(
|
keyboard = InlineKeyboardMarkup(
|
||||||
[[InlineKeyboardButton("« Back", callback_data="menu_main")]]
|
[[InlineKeyboardButton("« Назад", callback_data="menu_main")]]
|
||||||
)
|
)
|
||||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
await safe_edit_message(query, get_promo_text(), reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
@@ -1290,9 +1677,43 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
|
|
||||||
|
# ── Авто-регистрация админа (до проверки доступа!) ──
|
||||||
|
if data.startswith("admin_confirm_"):
|
||||||
|
await query.answer()
|
||||||
|
try:
|
||||||
|
new_admin_id = int(data.split("_")[-1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
await safe_edit_message(query, "❌ Ошибка: некорректный ID")
|
||||||
|
return
|
||||||
|
# Безопасность: только тот кто нажал кнопку может стать админом
|
||||||
|
if update.effective_user.id != new_admin_id:
|
||||||
|
await query.answer("Эта кнопка не для вас", show_alert=True)
|
||||||
|
return
|
||||||
|
# Race condition: если кто-то уже стал админом
|
||||||
|
if not _WAITING_FOR_ADMIN:
|
||||||
|
await safe_edit_message(query, "ℹ️ Администратор уже назначен.")
|
||||||
|
return
|
||||||
|
add_admin(new_admin_id)
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
f"✅ <b>Вы назначены администратором!</b>\n\n"
|
||||||
|
f"ID: <code>{new_admin_id}</code>\n\n"
|
||||||
|
f"Нажмите /start чтобы открыть меню.",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if data == "admin_cancel":
|
||||||
|
await query.answer()
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
"👋 Ок. Напишите /start когда будете готовы.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Access control
|
# Access control
|
||||||
if not is_user_allowed(update.effective_user.id):
|
if not is_user_allowed(update.effective_user.id):
|
||||||
await query.answer("Access denied")
|
await query.answer("Доступ запрещён")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Main menu
|
# Main menu
|
||||||
@@ -1327,28 +1748,30 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
"menu_website": cb_menu_website,
|
"menu_website": cb_menu_website,
|
||||||
"menu_promo": cb_menu_promo,
|
"menu_promo": cb_menu_promo,
|
||||||
"menu_credits": cb_menu_credits,
|
"menu_credits": cb_menu_credits,
|
||||||
|
"menu_admins": cb_menu_admins,
|
||||||
"menu_remove": cb_menu_remove,
|
"menu_remove": cb_menu_remove,
|
||||||
"install_mode_quick": cb_install_mode_quick,
|
"install_mode_lite": cb_install_mode_lite,
|
||||||
"install_mode_stealth": cb_install_mode_stealth,
|
"install_mode_pro": cb_install_mode_pro,
|
||||||
"backup_create": cb_backup_create,
|
"backup_create": cb_backup_create,
|
||||||
"backup_list": cb_backup_list,
|
"backup_list": cb_backup_list,
|
||||||
"ssl_renew": cb_ssl_renew,
|
"ssl_renew": cb_ssl_renew,
|
||||||
"ssl_status": cb_ssl_status,
|
"ssl_status": cb_ssl_status,
|
||||||
"remove_confirm": cb_remove_confirm,
|
"remove_confirm": cb_remove_confirm,
|
||||||
"change_quick": cb_change_quick,
|
"change_lite": cb_change_lite,
|
||||||
"change_stealth": cb_change_stealth,
|
"change_pro": cb_change_pro,
|
||||||
"install_migrate": cb_install_migrate,
|
"install_migrate": cb_install_migrate,
|
||||||
|
"menu_stats": cb_menu_stats,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pattern-based handlers
|
# Pattern-based handlers
|
||||||
if data.startswith("quick_dom_"):
|
if data.startswith("lite_dom_"):
|
||||||
await cb_quick_domain(update, context)
|
await cb_lite_domain(update, context)
|
||||||
elif data.startswith("stealth_cat_"):
|
elif data.startswith("pro_cat_"):
|
||||||
await cb_stealth_category(update, context)
|
await cb_pro_category(update, context)
|
||||||
elif data.startswith("stealth_tpl_"):
|
elif data.startswith("pro_tpl_"):
|
||||||
await cb_stealth_template(update, context)
|
await cb_pro_template(update, context)
|
||||||
elif data.startswith("stealth_confirm_"):
|
elif data.startswith("pro_confirm_"):
|
||||||
await cb_stealth_confirm(update, context)
|
await cb_pro_confirm(update, context)
|
||||||
elif data.startswith("restore_idx_"):
|
elif data.startswith("restore_idx_"):
|
||||||
await cb_restore_backup(update, context)
|
await cb_restore_backup(update, context)
|
||||||
elif data in handlers:
|
elif data in handlers:
|
||||||
@@ -1386,6 +1809,8 @@ def main() -> None:
|
|||||||
application.add_handler(CommandHandler("help", cmd_help))
|
application.add_handler(CommandHandler("help", cmd_help))
|
||||||
application.add_handler(CommandHandler("status", cmd_status))
|
application.add_handler(CommandHandler("status", cmd_status))
|
||||||
application.add_handler(CommandHandler("logs", cmd_logs))
|
application.add_handler(CommandHandler("logs", cmd_logs))
|
||||||
|
application.add_handler(CommandHandler("addadmin", cmd_addadmin))
|
||||||
|
application.add_handler(CommandHandler("deladmin", cmd_deladmin))
|
||||||
|
|
||||||
# Callback query handler (buttons)
|
# Callback query handler (buttons)
|
||||||
application.add_handler(CallbackQueryHandler(handle_callback))
|
application.add_handler(CallbackQueryHandler(handle_callback))
|
||||||
|
|||||||
728
install.sh
Normal file → Executable file
728
install.sh
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
# GoTelegram v2.2.1 — MTProxy на ядре telemt (Rust + Tokio)
|
# GoTelegram v2.3.0 — MTProxy на ядре telemt (Rust + Tokio)
|
||||||
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance
|
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance
|
||||||
#
|
#
|
||||||
# Установка:
|
# Установка:
|
||||||
@@ -20,48 +20,225 @@ source "$LIB_DIR/telemt_config.sh"
|
|||||||
source "$LIB_DIR/website.sh"
|
source "$LIB_DIR/website.sh"
|
||||||
source "$LIB_DIR/templates_catalog.sh"
|
source "$LIB_DIR/templates_catalog.sh"
|
||||||
source "$LIB_DIR/backup.sh"
|
source "$LIB_DIR/backup.sh"
|
||||||
|
[ -f "$LIB_DIR/stats.sh" ] && source "$LIB_DIR/stats.sh"
|
||||||
|
|
||||||
# ── Главное меню ─────────────────────────────────────────────────────────────
|
# ── Главное меню (Compact Dashboard + 5 Top-Level Items) ──────────────────────
|
||||||
show_main_menu() {
|
show_main_menu() {
|
||||||
local proxy_status bot_status
|
local proxy_status bot_status nginx_st mode domain secret port ip link ssl_expiry
|
||||||
proxy_status=$(telemt_status)
|
proxy_status=$(telemt_status)
|
||||||
bot_status=$(bot_service_status)
|
bot_status=$(bot_service_status)
|
||||||
|
nginx_st=$(nginx_status 2>/dev/null || echo "stopped")
|
||||||
|
mode=$(config_get mode 2>/dev/null || echo "—")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
secret=$(get_config_value secret 2>/dev/null || echo "")
|
||||||
|
port=$(get_config_value port 2>/dev/null || echo "443")
|
||||||
|
ip=$(get_server_ip 2>/dev/null || echo "N/A")
|
||||||
|
|
||||||
local proxy_badge bot_badge
|
local W=54
|
||||||
case "$proxy_status" in
|
local line; line=$(printf '━%.0s' $(seq 1 $W))
|
||||||
running) proxy_badge="${GREEN}● Работает${NC}" ;;
|
local line2; line2=$(printf '─%.0s' $(seq 1 $W))
|
||||||
stopped) proxy_badge="${YELLOW}○ Остановлен${NC}" ;;
|
|
||||||
*) proxy_badge="${RED}✗ Не установлен${NC}" ;;
|
|
||||||
esac
|
|
||||||
case "$bot_status" in
|
|
||||||
running) bot_badge="${GREEN}● Бот работает${NC}" ;;
|
|
||||||
stopped) bot_badge="${YELLOW}○ Бот остановлен${NC}" ;;
|
|
||||||
*) bot_badge="${DIM}нет${NC}" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
|
# ── Заголовок (без правого бордера — ANSI ломает выравнивание) ──
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}${WHITE}Главное меню${NC} │ Proxy: ${proxy_badge} │ ${bot_badge}"
|
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
echo -e " ${BOLD}${WHITE} GoTelegram v${GOTELEGRAM_VERSION}${NC} ${DIM}— Панель управления${NC}"
|
||||||
echo -e " ${DIM}── Прокси ──${NC}"
|
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||||||
echo -e " ${CYAN} 1)${NC} 🔧 Установить / Обновить прокси"
|
|
||||||
echo -e " ${CYAN} 2)${NC} 📊 Статус"
|
# ── Здоровье сервисов ──
|
||||||
echo -e " ${CYAN} 3)${NC} 🔗 Ссылка для подключения"
|
echo ""
|
||||||
echo -e " ${CYAN} 4)${NC} 📤 Поделиться ключом"
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
echo -e " ${CYAN} 5)${NC} 🔄 Перезапуск"
|
|
||||||
echo -e " ${CYAN} 6)${NC} 📋 Логи"
|
# Proxy
|
||||||
echo -e " ${CYAN} 7)${NC} 🎭 Сменить режим / шаблон"
|
local proxy_icon proxy_color
|
||||||
echo -e " ${DIM}── Управление ──${NC}"
|
case "$proxy_status" in
|
||||||
echo -e " ${CYAN} 8)${NC} 💾 Бекап конфигурации"
|
running) proxy_icon="●"; proxy_color="${GREEN}" ;;
|
||||||
echo -e " ${CYAN} 9)${NC} 📦 Восстановить из бекапа"
|
stopped) proxy_icon="○"; proxy_color="${YELLOW}" ;;
|
||||||
echo -e " ${CYAN}10)${NC} ⬆️ Обновить telemt"
|
*) proxy_icon="✗"; proxy_color="${RED}" ;;
|
||||||
echo -e " ${CYAN}11)${NC} 🌐 Управление сайтом (SSL)"
|
esac
|
||||||
echo -e " ${DIM}── Бот и прочее ──${NC}"
|
echo -e " ${proxy_color}${proxy_icon}${NC} Прокси ${proxy_color}${proxy_status}${NC} ${DIM}(telemt ${mode})${NC}"
|
||||||
echo -e " ${CYAN}12)${NC} 🤖 Telegram-бот"
|
|
||||||
echo -e " ${CYAN}13)${NC} 🗑 Удалить всё"
|
# nginx
|
||||||
echo -e " ${CYAN}14)${NC} 🏷 Промо"
|
local nginx_icon nginx_color
|
||||||
echo -e " ${CYAN} 0)${NC} 🚪 Выход"
|
case "$nginx_st" in
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
running) nginx_icon="●"; nginx_color="${GREEN}" ;;
|
||||||
echo -ne " ${WHITE}Выбор:${NC} "
|
*) nginx_icon="✗"; nginx_color="${RED}" ;;
|
||||||
|
esac
|
||||||
|
echo -e " ${nginx_icon}${nginx_color}${NC} nginx ${nginx_color}${nginx_st}${NC} ${DIM}(127.0.0.1:8443)${NC}"
|
||||||
|
|
||||||
|
# Site (pro)
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local site_icon site_color
|
||||||
|
if curl -sk --max-time 3 "https://${domain}/" -o /dev/null 2>/dev/null; then
|
||||||
|
site_icon="●"; site_color="${GREEN}"
|
||||||
|
else
|
||||||
|
site_icon="✗"; site_color="${RED}"
|
||||||
|
fi
|
||||||
|
echo -e " ${site_color}${site_icon}${NC} Сайт ${site_color}https://${domain}${NC}"
|
||||||
|
|
||||||
|
ssl_expiry=$(get_ssl_expiry "$domain" 2>/dev/null || echo "N/A")
|
||||||
|
echo -e " ${GREEN}●${NC} SSL ${DIM}до ${ssl_expiry}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bot
|
||||||
|
case "$bot_status" in
|
||||||
|
running) echo -e " ${GREEN}●${NC} Бот ${GREEN}running${NC}" ;;
|
||||||
|
stopped) echo -e " ${YELLOW}○${NC} Бот ${YELLOW}stopped${NC}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
|
|
||||||
|
# ── Сетевые параметры ──
|
||||||
|
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC} ${WHITE}Порт:${NC} ${CYAN}${port}${NC} ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
||||||
|
if [ -n "$domain" ]; then
|
||||||
|
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
|
|
||||||
|
# ── Прокси-ссылка + QR ──
|
||||||
|
if [ -n "$secret" ] && [ "$proxy_status" = "running" ]; then
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local raw_secret faketls_secret domain_hex
|
||||||
|
raw_secret="$secret"
|
||||||
|
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||||
|
faketls_secret="ee${raw_secret}${domain_hex}"
|
||||||
|
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||||
|
else
|
||||||
|
link="tg://proxy?server=${ip}&port=${port}&secret=${secret}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e " ${BOLD}${WHITE}Ссылка для Telegram:${NC}"
|
||||||
|
echo -e " ${GREEN}${link}${NC}"
|
||||||
|
|
||||||
|
if command -v qrencode &>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
qrencode -t UTF8 -m 2 "$link" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
|
echo " ${qr_line}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e " ${DIM}Прокси не настроен. Выберите пункт 1.${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Меню ──
|
||||||
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
|
echo -e " ${CYAN}1${NC}) Прокси ▸"
|
||||||
|
echo -e " ${CYAN}2${NC}) Статистика ▸"
|
||||||
|
echo -e " ${CYAN}3${NC}) Управление ▸"
|
||||||
|
echo -e " ${CYAN}4${NC}) Telegram-бот ▸"
|
||||||
|
echo -e " ${CYAN}5${NC}) О программе ▸"
|
||||||
|
echo -e " ${CYAN}0${NC}) ${DIM}Выход${NC}"
|
||||||
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
|
echo -e " ${DIM}Обновление через 30 сек${NC}"
|
||||||
|
echo -ne " ${WHITE}▸ ${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Подменю: Прокси ──────────────────────────────────────────────────────────
|
||||||
|
submenu_proxy() {
|
||||||
|
while true; do
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}🚀 ПРОКСИ${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
|
echo -e " ${CYAN}1${NC}) Установить / Обновить"
|
||||||
|
echo -e " ${CYAN}2${NC}) Статус подробно"
|
||||||
|
echo -e " ${CYAN}3${NC}) Скопировать ссылку"
|
||||||
|
echo -e " ${CYAN}4${NC}) Поделиться ключом"
|
||||||
|
echo -e " ${CYAN}5${NC}) Перезапуск"
|
||||||
|
echo -e " ${CYAN}6${NC}) Логи"
|
||||||
|
echo -e " ${CYAN}7${NC}) Сменить режим / шаблон"
|
||||||
|
echo -e " ${CYAN}0${NC}) « Назад"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
|
read -r ch
|
||||||
|
|
||||||
|
case "$ch" in
|
||||||
|
1) menu_install ;;
|
||||||
|
2) menu_status ;;
|
||||||
|
3) menu_link ;;
|
||||||
|
4) menu_share ;;
|
||||||
|
5) menu_restart ;;
|
||||||
|
6) menu_logs ;;
|
||||||
|
7) menu_change_mode ;;
|
||||||
|
0) break ;;
|
||||||
|
*) log_error "Неверный выбор" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -ne " ${DIM}Нажмите Enter...${NC}"
|
||||||
|
read -r
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Подменю: Управление ──────────────────────────────────────────────────────
|
||||||
|
submenu_manage() {
|
||||||
|
while true; do
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}⚙️ УПРАВЛЕНИЕ${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
|
echo -e " ${CYAN}1${NC}) Бекап"
|
||||||
|
echo -e " ${CYAN}2${NC}) Восстановить"
|
||||||
|
echo -e " ${CYAN}3${NC}) Обновить telemt"
|
||||||
|
echo -e " ${CYAN}4${NC}) Сайт / SSL"
|
||||||
|
echo -e " ${CYAN}5${NC}) Удалить"
|
||||||
|
echo -e " ${CYAN}0${NC}) « Назад"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
|
read -r ch
|
||||||
|
|
||||||
|
case "$ch" in
|
||||||
|
1) interactive_backup ;;
|
||||||
|
2) interactive_restore ;;
|
||||||
|
3) update_telemt ;;
|
||||||
|
4) menu_website ;;
|
||||||
|
5) menu_remove ;;
|
||||||
|
0) break ;;
|
||||||
|
*) log_error "Неверный выбор" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -ne " ${DIM}Нажмите Enter...${NC}"
|
||||||
|
read -r
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Подменю: О программе ─────────────────────────────────────────────────────
|
||||||
|
submenu_about() {
|
||||||
|
while true; do
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}ℹ️ О ПРОГРАММЕ${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
|
echo -e " ${CYAN}1${NC}) Информация о версии"
|
||||||
|
echo -e " ${CYAN}2${NC}) Промо / Донат"
|
||||||
|
echo -e " ${CYAN}0${NC}) « Назад"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
|
read -r ch
|
||||||
|
|
||||||
|
case "$ch" in
|
||||||
|
1) menu_version ;;
|
||||||
|
2) menu_promo ;;
|
||||||
|
0) break ;;
|
||||||
|
*) log_error "Неверный выбор" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -ne " ${DIM}Нажмите Enter...${NC}"
|
||||||
|
read -r
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Информация о версии ──────────────────────────────────────────────────────
|
||||||
|
menu_version() {
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}🔍 Информация${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
|
echo -e " ${WHITE}GoTelegram:${NC} v${GOTELEGRAM_VERSION}"
|
||||||
|
echo -e " ${WHITE}Ядро:${NC} telemt (Rust + Tokio)"
|
||||||
|
echo -e " ${WHITE}Технология:${NC} Anti-DPI, Fake TLS, TCP Splice"
|
||||||
|
echo -e " ${WHITE}Лицензия:${NC} MIT"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Установка: выбор режима ──────────────────────────────────────────────────
|
# ── Установка: выбор режима ──────────────────────────────────────────────────
|
||||||
@@ -80,29 +257,29 @@ menu_install() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}${WHITE}🎭 Выберите режим маскировки:${NC}"
|
echo -e " ${BOLD}${WHITE}🎭 Выберите режим маскировки:${NC}"
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
echo -e " ${CYAN}1)${NC} ${GREEN}⚡ Quick${NC} — маскировка под популярный сайт"
|
echo -e " ${CYAN}1)${NC} ${GREEN}⚡ Lite${NC} — маскировка под популярный сайт"
|
||||||
echo -e " ${DIM}Быстро, без домена. telemt маскирует трафик${NC}"
|
echo -e " ${DIM}Быстро, без домена. telemt маскирует трафик${NC}"
|
||||||
echo -e " ${DIM}под выбранный сайт (google.com и т.д.)${NC}"
|
echo -e " ${DIM}под выбранный сайт (google.com и т.д.)${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}2)${NC} ${MAGENTA}🛡 Stealth${NC} — свой сайт + полная маскировка"
|
echo -e " ${CYAN}2)${NC} ${MAGENTA}🛡 Pro${NC} — свой сайт + полная маскировка"
|
||||||
echo -e " ${DIM}nginx + SSL + HTML-шаблон + telemt.${NC}"
|
echo -e " ${DIM}nginx + SSL + HTML-шаблон + telemt.${NC}"
|
||||||
echo -e " ${DIM}DPI видит реальный сайт с реальным сертификатом.${NC}"
|
echo -e " ${DIM}DPI видит реальный сайт с реальным сертификатом.${NC}"
|
||||||
echo -e " ${DIM}Требует: домен, направленный на этот сервер.${NC}"
|
echo -e " ${DIM}Требует: домен, направленный на этот сервер.{{NC}"
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
echo -ne " ${WHITE}Выбор (1/2):${NC} "
|
echo -ne " ${WHITE}Выбор (1/2):${NC} "
|
||||||
read -r mode_choice
|
read -r mode_choice
|
||||||
mode_choice="${mode_choice:-}"
|
mode_choice="${mode_choice:-}"
|
||||||
|
|
||||||
case "$mode_choice" in
|
case "$mode_choice" in
|
||||||
1) install_quick_mode ;;
|
1) install_lite_mode ;;
|
||||||
2) install_stealth_mode ;;
|
2) install_pro_mode ;;
|
||||||
*) log_error "Неверный выбор: ${mode_choice:-<пусто>}" ;;
|
*) log_error "Неверный выбор: ${mode_choice:-<пусто>}" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Quick-режим ──────────────────────────────────────────────────────────────
|
# ── Lite-режим ───────────────────────────────────────────────────────────────
|
||||||
install_quick_mode() {
|
install_lite_mode() {
|
||||||
log_step "Установка Quick-режима"
|
log_step "Установка Lite-режима"
|
||||||
|
|
||||||
# Выбор домена
|
# Выбор домена
|
||||||
local domain
|
local domain
|
||||||
@@ -126,7 +303,7 @@ install_quick_mode() {
|
|||||||
echo -e " IP: ${CYAN}${ip}${NC}"
|
echo -e " IP: ${CYAN}${ip}${NC}"
|
||||||
echo -e " Порт: ${CYAN}${port}${NC}"
|
echo -e " Порт: ${CYAN}${port}${NC}"
|
||||||
echo -e " Маскировка: ${CYAN}${domain}${NC}"
|
echo -e " Маскировка: ${CYAN}${domain}${NC}"
|
||||||
echo -e " Режим: ${GREEN}Quick${NC}"
|
echo -e " Режим: ${GREEN}Lite${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if ! confirm "Установить прокси?"; then
|
if ! confirm "Установить прокси?"; then
|
||||||
@@ -138,7 +315,7 @@ install_quick_mode() {
|
|||||||
install_telemt_full || return
|
install_telemt_full || return
|
||||||
|
|
||||||
# Генерируем конфиг telemt
|
# Генерируем конфиг telemt
|
||||||
generate_telemt_toml "$secret" "$port" "quick" "$domain" "443"
|
generate_telemt_toml "$secret" "$port" "lite" "$domain" "443"
|
||||||
|
|
||||||
# Валидация
|
# Валидация
|
||||||
validate_telemt_config || return
|
validate_telemt_config || return
|
||||||
@@ -147,19 +324,19 @@ install_quick_mode() {
|
|||||||
start_telemt || return
|
start_telemt || return
|
||||||
|
|
||||||
# Сохраняем GoTelegram конфиг
|
# Сохраняем GoTelegram конфиг
|
||||||
save_gotelegram_config "telemt" "quick" "$port" "$secret" "$domain" "" ""
|
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
|
||||||
|
|
||||||
# Благодарности
|
# Благодарности
|
||||||
show_credits
|
show_credits
|
||||||
|
|
||||||
# Результат
|
# Результат
|
||||||
show_proxy_info
|
show_proxy_info
|
||||||
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Quick-режим)"
|
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Lite-режим)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Stealth-режим ────────────────────────────────────────────────────────────
|
# ── Pro-режим ────────────────────────────────────────────────────────────────
|
||||||
install_stealth_mode() {
|
install_pro_mode() {
|
||||||
log_step "Установка Stealth-режима"
|
log_step "Установка Pro-режима"
|
||||||
|
|
||||||
# Ввод домена
|
# Ввод домена
|
||||||
echo ""
|
echo ""
|
||||||
@@ -192,24 +369,32 @@ install_stealth_mode() {
|
|||||||
template_dir=$(interactive_template_selection)
|
template_dir=$(interactive_template_selection)
|
||||||
[ $? -ne 0 ] && return
|
[ $? -ne 0 ] && return
|
||||||
|
|
||||||
# Внутренний порт для telemt (только localhost, не открыт наружу)
|
# Архитектура Pro:
|
||||||
# nginx принимает весь трафик на внешнем 443 и проксирует MTProxy-соединения на localhost:8443
|
# telemt слушает на 0.0.0.0:443 (принимает ВСЕ подключения)
|
||||||
# Внешний порт 8443 НЕ затрагивается и НЕ открывается — telemt слушает только 127.0.0.1
|
# nginx слушает на 127.0.0.1:8443 с SSL (обслуживает сайт)
|
||||||
local telemt_port=8443
|
# MTProxy клиент → :443 → telemt (проксирует)
|
||||||
|
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
||||||
|
# Провайдер видит только HTTPS на 443 к домену
|
||||||
|
local nginx_internal_port=8443
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${DIM}telemt будет слушать на localhost:$telemt_port (только внутренний, не открыт наружу)${NC}"
|
echo -e " ${DIM}telemt принимает весь трафик на 443 (маскировка под HTTPS)${NC}"
|
||||||
echo -e " ${DIM}nginx принимает трафик на 443 (внешний) и проксирует к telemt${NC}"
|
echo -e " ${DIM}nginx обслуживает сайт на внутреннем порту $nginx_internal_port${NC}"
|
||||||
|
echo -e " ${DIM}Провайдер видит только HTTPS-трафик к ${user_domain}:443${NC}"
|
||||||
|
|
||||||
# Генерация секрета
|
# Генерация fake-TLS секрета (ee + secret + hex domain)
|
||||||
local secret
|
# Префикс ee говорит Telegram-клиенту маскировать трафик под TLS к домену
|
||||||
secret=$(generate_hex 32)
|
local raw_secret
|
||||||
|
raw_secret=$(generate_hex 32)
|
||||||
|
local domain_hex
|
||||||
|
domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n')
|
||||||
|
local faketls_secret="ee${raw_secret}${domain_hex}"
|
||||||
|
|
||||||
# Подтверждение
|
# Подтверждение
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}${WHITE}📋 Конфигурация:${NC}"
|
echo -e " ${BOLD}${WHITE}📋 Конфигурация:${NC}"
|
||||||
echo -e " Домен: ${CYAN}${user_domain}${NC}"
|
echo -e " Домен: ${CYAN}${user_domain}${NC}"
|
||||||
echo -e " Порт: ${CYAN}443 (nginx) → $telemt_port (telemt)${NC}"
|
echo -e " Порт: ${CYAN}443 (telemt + nginx внутри)${NC}"
|
||||||
echo -e " Режим: ${MAGENTA}Stealth${NC}"
|
echo -e " Режим: ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if ! confirm "Установить прокси + сайт?"; then
|
if ! confirm "Установить прокси + сайт?"; then
|
||||||
@@ -220,11 +405,15 @@ install_stealth_mode() {
|
|||||||
ensure_deps
|
ensure_deps
|
||||||
install_telemt_full || return
|
install_telemt_full || return
|
||||||
|
|
||||||
# Конфиг telemt: маскировка на localhost (nginx)
|
# Конфиг telemt: слушает 443, маскировка на локальный nginx через dns_override
|
||||||
generate_telemt_toml "$secret" "$telemt_port" "stealth" "127.0.0.1" "443"
|
generate_telemt_toml "$raw_secret" "443" "pro" "$user_domain" "$nginx_internal_port"
|
||||||
|
|
||||||
# Настройка сайта (nginx + certbot + шаблон)
|
# Настройка сайта (nginx на внутреннем порту + certbot + шаблон)
|
||||||
setup_stealth_mode "$user_domain" "$template_dir" "$telemt_port" "$ssl_email" || return
|
setup_pro_mode "$user_domain" "$template_dir" "$nginx_internal_port" "$ssl_email" || return
|
||||||
|
|
||||||
|
# Останавливаем nginx на 443 перед запуском telemt (telemt займёт 443)
|
||||||
|
# nginx уже перенастроен на внутренний порт
|
||||||
|
systemctl restart nginx 2>/dev/null
|
||||||
|
|
||||||
# Запуск telemt
|
# Запуск telemt
|
||||||
start_telemt || return
|
start_telemt || return
|
||||||
@@ -232,22 +421,22 @@ install_stealth_mode() {
|
|||||||
# Сохраняем конфиг
|
# Сохраняем конфиг
|
||||||
local tpl_id
|
local tpl_id
|
||||||
tpl_id=$(basename "$template_dir")
|
tpl_id=$(basename "$template_dir")
|
||||||
save_gotelegram_config "telemt" "stealth" "443" "$secret" "127.0.0.1" "$user_domain" "$tpl_id"
|
save_gotelegram_config "telemt" "pro" "443" "$raw_secret" "$user_domain" "$user_domain" "$tpl_id"
|
||||||
|
|
||||||
# Результат
|
# Результат — используем домен и fake-TLS ссылку
|
||||||
show_proxy_info
|
show_proxy_info_pro "$user_domain" "$faketls_secret"
|
||||||
echo -e " ${WHITE}Сайт:${NC} ${GREEN}https://${user_domain}${NC}"
|
echo -e " ${WHITE}Сайт:${NC} ${GREEN}https://${user_domain}${NC}"
|
||||||
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Stealth-режим)"
|
log_success "GoTelegram v${GOTELEGRAM_VERSION} установлен! (Pro-режим)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Статус ───────────────────────────────────────────────────────────────────
|
# ── Статус ───────────────────────────────────────────────────────────────────
|
||||||
menu_status() {
|
menu_status() {
|
||||||
show_proxy_info
|
show_proxy_info
|
||||||
|
|
||||||
# Дополнительно для stealth
|
# Дополнительно для pro
|
||||||
local mode
|
local mode
|
||||||
mode=$(config_get mode 2>/dev/null)
|
mode=$(config_get mode 2>/dev/null)
|
||||||
if [ "$mode" = "stealth" ]; then
|
if [ "$mode" = "pro" ]; then
|
||||||
local domain
|
local domain
|
||||||
domain=$(config_get domain 2>/dev/null)
|
domain=$(config_get domain 2>/dev/null)
|
||||||
if [ -n "$domain" ]; then
|
if [ -n "$domain" ]; then
|
||||||
@@ -265,11 +454,21 @@ menu_status() {
|
|||||||
|
|
||||||
# ── Ссылка ───────────────────────────────────────────────────────────────────
|
# ── Ссылка ───────────────────────────────────────────────────────────────────
|
||||||
menu_link() {
|
menu_link() {
|
||||||
local secret port ip link
|
local secret port ip link mode domain
|
||||||
secret=$(get_config_value secret)
|
secret=$(get_config_value secret)
|
||||||
port=$(get_config_value port)
|
port=$(get_config_value port)
|
||||||
ip=$(get_server_ip)
|
ip=$(get_server_ip)
|
||||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local domain_hex faketls_secret
|
||||||
|
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||||
|
faketls_secret="ee${secret}${domain_hex}"
|
||||||
|
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||||
|
else
|
||||||
|
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}${WHITE}🔗 Ссылка для подключения:${NC}"
|
echo -e " ${BOLD}${WHITE}🔗 Ссылка для подключения:${NC}"
|
||||||
@@ -284,18 +483,30 @@ menu_link() {
|
|||||||
|
|
||||||
# ── Поделиться ───────────────────────────────────────────────────────────────
|
# ── Поделиться ───────────────────────────────────────────────────────────────
|
||||||
menu_share() {
|
menu_share() {
|
||||||
local secret port ip link
|
local secret port ip link mode domain server_display
|
||||||
secret=$(get_config_value secret)
|
secret=$(get_config_value secret)
|
||||||
port=$(get_config_value port)
|
port=$(get_config_value port)
|
||||||
ip=$(get_server_ip)
|
ip=$(get_server_ip)
|
||||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local domain_hex faketls_secret
|
||||||
|
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||||
|
faketls_secret="ee${secret}${domain_hex}"
|
||||||
|
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||||
|
server_display="$domain"
|
||||||
|
else
|
||||||
|
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||||
|
server_display="$ip"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}📤 Перешлите это сообщение:${NC}"
|
echo -e " ${BOLD}📤 Перешлите это сообщение:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔐 MTProxy для Telegram (GoTelegram v${GOTELEGRAM_VERSION})"
|
echo "🔐 MTProxy для Telegram (GoTelegram v${GOTELEGRAM_VERSION})"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🌍 Сервер: $ip"
|
echo "🌍 Сервер: $server_display"
|
||||||
echo "🔌 Порт: $port"
|
echo "🔌 Порт: $port"
|
||||||
echo ""
|
echo ""
|
||||||
echo "👉 Подключиться одним нажатием:"
|
echo "👉 Подключиться одним нажатием:"
|
||||||
@@ -310,7 +521,7 @@ menu_restart() {
|
|||||||
restart_telemt
|
restart_telemt
|
||||||
local mode
|
local mode
|
||||||
mode=$(config_get mode 2>/dev/null)
|
mode=$(config_get mode 2>/dev/null)
|
||||||
if [ "$mode" = "stealth" ]; then
|
if [ "$mode" = "pro" ]; then
|
||||||
restart_nginx
|
restart_nginx
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -331,16 +542,16 @@ menu_change_mode() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e " ${WHITE}Текущий режим:${NC} ${CYAN}${current_mode}${NC}"
|
echo -e " ${WHITE}Текущий режим:${NC} ${CYAN}${current_mode}${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1)${NC} Сменить шаблон сайта (только stealth)"
|
echo -e " ${CYAN}1${NC}) Сменить шаблон сайта (только pro)"
|
||||||
echo -e " ${CYAN}2)${NC} Переключить режим (quick ↔ stealth)"
|
echo -e " ${CYAN}2${NC}) Переключить режим (lite ↔ pro)"
|
||||||
echo -e " ${CYAN}0)${NC} Назад"
|
echo -e " ${CYAN}0${NC}) Назад"
|
||||||
echo -ne " ${WHITE}Выбор:${NC} "
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
read -r ch
|
read -r ch
|
||||||
|
|
||||||
case "$ch" in
|
case "$ch" in
|
||||||
1)
|
1)
|
||||||
if [ "$current_mode" != "stealth" ]; then
|
if [ "$current_mode" != "pro" ]; then
|
||||||
log_error "Смена шаблона доступна только в stealth-режиме"
|
log_error "Смена шаблона доступна только в pro-режиме"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local template_dir
|
local template_dir
|
||||||
@@ -357,12 +568,13 @@ menu_change_mode() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Управление сайтом ───────────────────────────────────────────────────────
|
# ── Управление сайтом ────────────────────────────────────────────────────────
|
||||||
menu_website() {
|
menu_website() {
|
||||||
local mode
|
local mode
|
||||||
mode=$(config_get mode 2>/dev/null)
|
mode=$(config_get mode 2>/dev/null)
|
||||||
if [ "$mode" != "stealth" ]; then
|
|
||||||
log_info "Управление сайтом доступно только в stealth-режиме"
|
if [ "$mode" != "pro" ]; then
|
||||||
|
log_info "Управление сайтом доступно только в pro-режиме"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -374,10 +586,10 @@ menu_website() {
|
|||||||
echo -e " Домен: ${CYAN}${domain}${NC}"
|
echo -e " Домен: ${CYAN}${domain}${NC}"
|
||||||
echo -e " SSL до: $(get_ssl_expiry "$domain")"
|
echo -e " SSL до: $(get_ssl_expiry "$domain")"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1)${NC} Обновить SSL сертификат"
|
echo -e " ${CYAN}1${NC}) Обновить SSL сертификат"
|
||||||
echo -e " ${CYAN}2)${NC} Перезапустить nginx"
|
echo -e " ${CYAN}2${NC}) Перезапустить nginx"
|
||||||
echo -e " ${CYAN}3)${NC} Сменить шаблон"
|
echo -e " ${CYAN}3${NC}) Сменить шаблон"
|
||||||
echo -e " ${CYAN}0)${NC} Назад"
|
echo -e " ${CYAN}0${NC}) Назад"
|
||||||
echo -ne " ${WHITE}Выбор:${NC} "
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
read -r ch
|
read -r ch
|
||||||
|
|
||||||
@@ -398,10 +610,10 @@ menu_remove() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}${RED}🗑 Удаление GoTelegram${NC}"
|
echo -e " ${BOLD}${RED}🗑 Удаление GoTelegram${NC}"
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
echo -e " ${CYAN}1)${NC} Удалить только прокси (telemt)"
|
echo -e " ${CYAN}1${NC}) Удалить только прокси (telemt)"
|
||||||
echo -e " ${CYAN}2)${NC} Удалить только Telegram-бота"
|
echo -e " ${CYAN}2${NC}) Удалить только Telegram-бота"
|
||||||
echo -e " ${CYAN}3)${NC} Удалить всё (прокси + бот + настройки)"
|
echo -e " ${CYAN}3${NC}) Удалить всё (прокси + бот + настройки)"
|
||||||
echo -e " ${CYAN}0)${NC} Назад"
|
echo -e " ${CYAN}0${NC}) Назад"
|
||||||
echo -ne " ${WHITE}Выбор:${NC} "
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
read -r rm_choice
|
read -r rm_choice
|
||||||
|
|
||||||
@@ -415,8 +627,8 @@ menu_remove() {
|
|||||||
remove_telemt
|
remove_telemt
|
||||||
local mode
|
local mode
|
||||||
mode=$(config_get mode 2>/dev/null)
|
mode=$(config_get mode 2>/dev/null)
|
||||||
if [ "$mode" = "stealth" ]; then
|
if [ "$mode" = "pro" ]; then
|
||||||
remove_stealth_mode
|
remove_pro_mode
|
||||||
fi
|
fi
|
||||||
rm -f "$GOTELEGRAM_CONFIG"
|
rm -f "$GOTELEGRAM_CONFIG"
|
||||||
log_success "Прокси удалён"
|
log_success "Прокси удалён"
|
||||||
@@ -434,8 +646,8 @@ menu_remove() {
|
|||||||
remove_telemt
|
remove_telemt
|
||||||
local mode
|
local mode
|
||||||
mode=$(config_get mode 2>/dev/null)
|
mode=$(config_get mode 2>/dev/null)
|
||||||
if [ "$mode" = "stealth" ]; then
|
if [ "$mode" = "pro" ]; then
|
||||||
remove_stealth_mode
|
remove_pro_mode
|
||||||
fi
|
fi
|
||||||
rm -f "$GOTELEGRAM_CONFIG"
|
rm -f "$GOTELEGRAM_CONFIG"
|
||||||
# Бот
|
# Бот
|
||||||
@@ -477,21 +689,21 @@ menu_bot() {
|
|||||||
running)
|
running)
|
||||||
echo -e " Статус: ${GREEN}● Работает${NC}"
|
echo -e " Статус: ${GREEN}● Работает${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1)${NC} 📊 Статус бота"
|
echo -e " ${CYAN}1${NC}) 📊 Статус бота"
|
||||||
echo -e " ${CYAN}2)${NC} 📋 Логи бота"
|
echo -e " ${CYAN}2${NC}) 📋 Логи бота"
|
||||||
echo -e " ${CYAN}3)${NC} 🔄 Перезапустить бота"
|
echo -e " ${CYAN}3${NC}) 🔄 Перезапустить бота"
|
||||||
echo -e " ${CYAN}4)${NC} ⏹ Остановить бота"
|
echo -e " ${CYAN}4${NC}) ⏹ Остановить бота"
|
||||||
echo -e " ${CYAN}5)${NC} ⚙️ Настройки (.env)"
|
echo -e " ${CYAN}5${NC}) ⚙️ Настройки (.env)"
|
||||||
echo -e " ${CYAN}6)${NC} 🗑 Удалить бота"
|
echo -e " ${CYAN}6${NC}) 🗑 Удалить бота"
|
||||||
;;
|
;;
|
||||||
stopped)
|
stopped)
|
||||||
echo -e " Статус: ${YELLOW}○ Остановлен${NC}"
|
echo -e " Статус: ${YELLOW}○ Остановлен${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1)${NC} 📊 Статус бота"
|
echo -e " ${CYAN}1${NC}) 📊 Статус бота"
|
||||||
echo -e " ${CYAN}2)${NC} 📋 Логи бота"
|
echo -e " ${CYAN}2${NC}) 📋 Логи бота"
|
||||||
echo -e " ${CYAN}3)${NC} ▶️ Запустить бота"
|
echo -e " ${CYAN}3${NC}) ▶️ Запустить бота"
|
||||||
echo -e " ${CYAN}5)${NC} ⚙️ Настройки (.env)"
|
echo -e " ${CYAN}5${NC}) ⚙️ Настройки (.env)"
|
||||||
echo -e " ${CYAN}6)${NC} 🗑 Удалить бота"
|
echo -e " ${CYAN}6${NC}) 🗑 Удалить бота"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e " Статус: ${RED}✗ Не установлен${NC}"
|
echo -e " Статус: ${RED}✗ Не установлен${NC}"
|
||||||
@@ -499,11 +711,11 @@ menu_bot() {
|
|||||||
echo -e " ${DIM}Бот позволяет управлять прокси прямо из Telegram:${NC}"
|
echo -e " ${DIM}Бот позволяет управлять прокси прямо из Telegram:${NC}"
|
||||||
echo -e " ${DIM}статус, перезапуск, смена режима, бекап, QR-код.${NC}"
|
echo -e " ${DIM}статус, перезапуск, смена режима, бекап, QR-код.${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1)${NC} 🔧 Установить бота"
|
echo -e " ${CYAN}1${NC}) 🔧 Установить бота"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo -e " ${CYAN}0)${NC} « Назад"
|
echo -e " ${CYAN}0${NC}) « Назад"
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
echo -ne " ${WHITE}Выбор:${NC} "
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
read -r ch
|
read -r ch
|
||||||
@@ -587,14 +799,25 @@ bot_install() {
|
|||||||
[ -z "$token" ] && log_error "Токен не может быть пустым"
|
[ -z "$token" ] && log_error "Токен не может быть пустым"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -ne " ${WHITE}ID администратора (Enter = доступ для всех):${NC} "
|
echo ""
|
||||||
read -r admin_id
|
echo -e " ${WHITE}Как добавить администратора?${NC}"
|
||||||
|
echo -e " ${CYAN}1${NC}) Автоматически — бот определит ID при первом /start"
|
||||||
|
echo -e " ${CYAN}2${NC}) Вручную — ввести ID сейчас"
|
||||||
|
echo -ne " ${WHITE}Выбор [1]:${NC} "
|
||||||
|
read -r admin_mode
|
||||||
|
admin_mode="${admin_mode:-1}"
|
||||||
|
|
||||||
|
local admin_ids=""
|
||||||
|
if [ "$admin_mode" = "2" ]; then
|
||||||
|
echo -ne " ${WHITE}ID администраторов (через пробел/запятую):${NC} "
|
||||||
|
read -r admin_ids
|
||||||
|
admin_ids=$(echo "$admin_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||||||
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "BOT_TOKEN=$token"
|
echo "BOT_TOKEN=$token"
|
||||||
[ -n "$admin_id" ] && echo "ALLOWED_IDS=$admin_id"
|
[ -n "$admin_ids" ] && echo "ALLOWED_IDS=$admin_ids"
|
||||||
} > "$BOT_DIR/.env"
|
} > "$BOT_DIR/.env"
|
||||||
|
|
||||||
chmod 600 "$BOT_DIR/.env"
|
chmod 600 "$BOT_DIR/.env"
|
||||||
log_success ".env создан"
|
log_success ".env создан"
|
||||||
else
|
else
|
||||||
@@ -623,6 +846,59 @@ SVCEOF
|
|||||||
systemctl enable "$BOT_SERVICE" &>/dev/null
|
systemctl enable "$BOT_SERVICE" &>/dev/null
|
||||||
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
|
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
|
||||||
|
|
||||||
|
# Если авто-режим — ждём пока бот словит первого админа
|
||||||
|
local has_ids
|
||||||
|
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||||||
|
if [ -z "$has_ids" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}Ожидание администратора${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Откройте бота в Telegram и отправьте ${CYAN}/start${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Бот автоматически назначит вас администратором ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${DIM}Нажмите Ctrl+C чтобы пропустить${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||||
|
local i=0
|
||||||
|
local waited=0
|
||||||
|
local max_wait=300 # 5 минут максимум
|
||||||
|
|
||||||
|
# Ловим Ctrl+C чтобы выйти из ожидания без убийства скрипта
|
||||||
|
local interrupted=0
|
||||||
|
trap 'interrupted=1' INT
|
||||||
|
|
||||||
|
while [ $waited -lt $max_wait ] && [ $interrupted -eq 0 ]; do
|
||||||
|
printf "\r ${CYAN}${frames[$i]}${NC} Ожидание... напишите /start боту (%d сек) " "$waited" >&2
|
||||||
|
i=$(( (i+1) % ${#frames[@]} ))
|
||||||
|
sleep 1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
|
||||||
|
# Проверяем появился ли ALLOWED_IDS
|
||||||
|
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||||||
|
if [ -n "$has_ids" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
trap - INT
|
||||||
|
printf "\r\033[K" >&2 # очистить строку со спиннером
|
||||||
|
|
||||||
|
if [ -n "$has_ids" ]; then
|
||||||
|
echo ""
|
||||||
|
log_success "Администратор назначен!"
|
||||||
|
echo -e " ${WHITE}ID:${NC} ${GREEN}${has_ids}${NC}"
|
||||||
|
elif [ $interrupted -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
log_warning "Пропущено. Добавить админа позже: меню → Telegram-бот → Настройки"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
log_warning "Таймаут (5 мин). Добавить админа: меню → Telegram-бот → Настройки"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
log_success "Бот установлен и запущен!"
|
log_success "Бот установлен и запущен!"
|
||||||
echo -e " ${DIM}Проверка: systemctl status $BOT_SERVICE${NC}"
|
echo -e " ${DIM}Проверка: systemctl status $BOT_SERVICE${NC}"
|
||||||
@@ -676,9 +952,9 @@ bot_edit_config() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1)${NC} Сменить BOT_TOKEN"
|
echo -e " ${CYAN}1${NC}) Сменить BOT_TOKEN"
|
||||||
echo -e " ${CYAN}2)${NC} Изменить ALLOWED_IDS"
|
echo -e " ${CYAN}2${NC}) Изменить ALLOWED_IDS"
|
||||||
echo -e " ${CYAN}0)${NC} Назад"
|
echo -e " ${CYAN}0${NC}) Назад"
|
||||||
echo -ne " ${WHITE}Выбор:${NC} "
|
echo -ne " ${WHITE}Выбор:${NC} "
|
||||||
read -r ch
|
read -r ch
|
||||||
|
|
||||||
@@ -696,9 +972,10 @@ bot_edit_config() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
echo -ne " ${WHITE}ALLOWED_IDS (через запятую, пусто = все):${NC} "
|
echo -ne " ${WHITE}ALLOWED_IDS (через пробел/запятую, пусто = авто):${NC} "
|
||||||
read -r new_ids
|
read -r new_ids
|
||||||
new_ids=$(echo "$new_ids" | tr -d '[:space:]')
|
# Нормализуем: пробелы и запятые → запятые, убираем лишнее
|
||||||
|
new_ids=$(echo "$new_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||||||
if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then
|
if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then
|
||||||
if [ -n "$new_ids" ]; then
|
if [ -n "$new_ids" ]; then
|
||||||
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
|
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
|
||||||
@@ -733,16 +1010,91 @@ bot_remove() {
|
|||||||
menu_promo() {
|
menu_promo() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ СО СКИДКОЙ ДО -60%${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #1 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} Промокоды: OFF60, antenka20, antenka6, antenka12 ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka20${NC} — 20% + 3% при оплате за 3 месяца ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka6${NC} — 15% + 5% при оплате за 6 месяцев ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #2 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/cUxAhj${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} Донат: ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}☕ Донат / Чаевые${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Проверка: показывать ли промо (раз в сутки) ────────────────────────────
|
||||||
|
should_show_promo() {
|
||||||
|
local stamp_file="$GOTELEGRAM_DIR/.promo_last_shown"
|
||||||
|
if [ ! -f "$stamp_file" ]; then
|
||||||
|
return 0 # никогда не показывали
|
||||||
|
fi
|
||||||
|
local last_shown now diff
|
||||||
|
last_shown=$(cat "$stamp_file" 2>/dev/null || echo "0")
|
||||||
|
last_shown="${last_shown//[^0-9]/}"
|
||||||
|
last_shown="${last_shown:-0}"
|
||||||
|
now=$(date +%s)
|
||||||
|
diff=$(( now - last_shown ))
|
||||||
|
# 86400 = 24 часа
|
||||||
|
[ "$diff" -ge 86400 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_promo_shown() {
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Промо с QR и задержкой (при установке + раз в сутки) ───────────────────
|
||||||
|
show_promo_with_qr() {
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #1 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka20${NC} — 20% + 3% при оплате за 3 месяца ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka6${NC} — 15% + 5% при оплате за 6 месяцев ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #2 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/cUxAhj${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# QR-коды
|
||||||
|
if command -v qrencode &>/dev/null; then
|
||||||
|
echo -e " ${DIM}── QR: Хостинг #1 ──${NC}"
|
||||||
|
qrencode -t UTF8 -m 1 "https://vk.cc/ct29NQ" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
|
echo " $qr_line"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}── QR: Хостинг #2 ──${NC}"
|
||||||
|
qrencode -t UTF8 -m 1 "https://vk.cc/cUxAhj" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
|
echo " $qr_line"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}── QR: Чаевые / Донат ──${NC}"
|
||||||
|
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
|
echo " $qr_line"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
mark_promo_shown
|
||||||
|
|
||||||
|
# 5-секундная задержка с обратным отсчётом
|
||||||
|
for i in 5 4 3 2 1; do
|
||||||
|
echo -ne "\r ${DIM}Меню через ${i} сек...${NC} "
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo -ne "\r \r"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Точка входа ──────────────────────────────────────────────────────────────
|
# ── Точка входа ──────────────────────────────────────────────────────────────
|
||||||
main() {
|
main() {
|
||||||
check_root
|
check_root
|
||||||
@@ -753,31 +1105,113 @@ main() {
|
|||||||
check_os
|
check_os
|
||||||
check_disk_space 500
|
check_disk_space 500
|
||||||
|
|
||||||
while true; do
|
# Промо раз в сутки
|
||||||
show_main_menu
|
if should_show_promo; then
|
||||||
read -r choice
|
show_promo_with_qr
|
||||||
case "$choice" in
|
fi
|
||||||
1) menu_install ;;
|
|
||||||
2) menu_status ;;
|
|
||||||
3) menu_link ;;
|
|
||||||
4) menu_share ;;
|
|
||||||
5) menu_restart ;;
|
|
||||||
6) menu_logs ;;
|
|
||||||
7) menu_change_mode ;;
|
|
||||||
8) interactive_backup ;;
|
|
||||||
9) interactive_restore ;;
|
|
||||||
10) update_telemt ;;
|
|
||||||
11) menu_website ;;
|
|
||||||
12) menu_bot ;;
|
|
||||||
13) menu_remove ;;
|
|
||||||
14) menu_promo ;;
|
|
||||||
0|q|exit) echo ""; log_info "До встречи! 👋"; exit 0 ;;
|
|
||||||
*) log_error "Неверный выбор" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
while true; do
|
||||||
echo -ne " ${DIM}Нажмите Enter для возврата в меню...${NC}"
|
clear
|
||||||
read -r
|
show_main_menu
|
||||||
|
# Auto-refresh: 30 sec timeout
|
||||||
|
if read -t 30 -r choice; then
|
||||||
|
case "$choice" in
|
||||||
|
1) submenu_proxy ;;
|
||||||
|
2) submenu_stats ;;
|
||||||
|
3) submenu_manage ;;
|
||||||
|
4) menu_bot ;;
|
||||||
|
5) submenu_about ;;
|
||||||
|
0|q|exit) echo ""; log_info "До встречи! 👋"; exit 0 ;;
|
||||||
|
*) log_error "Неверный выбор" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Пауза после подменю (кроме статистики — у неё свой цикл)
|
||||||
|
if [ "$choice" != "2" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -ne " ${DIM}Нажмите Enter для возврата в меню...${NC}"
|
||||||
|
read -r
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# If read timed out, loop refreshes the dashboard
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Статистика (авто-обновление 1 сек, без мерцания) ───────────────────────
|
||||||
|
submenu_stats() {
|
||||||
|
# Инициализируем статистику при первом входе
|
||||||
|
if type stats_init &>/dev/null; then
|
||||||
|
stats_init 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
local line2; line2=$(printf '─%.0s' {1..54})
|
||||||
|
local first_draw=1
|
||||||
|
|
||||||
|
# Скрываем курсор для плавного обновления
|
||||||
|
tput civis 2>/dev/null
|
||||||
|
|
||||||
|
# Восстанавливаем курсор при выходе из функции
|
||||||
|
trap 'tput cnorm 2>/dev/null; trap - RETURN' RETURN
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if [ "$first_draw" -eq 1 ]; then
|
||||||
|
clear
|
||||||
|
first_draw=0
|
||||||
|
else
|
||||||
|
# Перемещаем курсор в начало экрана вместо clear — нет мерцания
|
||||||
|
tput cup 0 0 2>/dev/null || printf '\033[H'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Рисуем весь экран поверх старого содержимого
|
||||||
|
echo -e "\033[J" # очищаем от курсора до конца (убирает хвосты)
|
||||||
|
echo -e " ${BOLD}${WHITE}📊 Статистика трафика${NC}"
|
||||||
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
|
|
||||||
|
if type show_traffic_stats &>/dev/null; then
|
||||||
|
show_traffic_stats
|
||||||
|
else
|
||||||
|
echo -e " ${DIM}Модуль статистики не загружен.${NC}"
|
||||||
|
echo -e " ${DIM}Файл lib/stats.sh не найден.${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
|
local stats_on="вкл"
|
||||||
|
if type toggle_stats &>/dev/null; then
|
||||||
|
local cfg_val
|
||||||
|
cfg_val=$(config_get stats_enabled 2>/dev/null || echo "true")
|
||||||
|
[ "$cfg_val" = "false" ] && stats_on="выкл"
|
||||||
|
fi
|
||||||
|
echo -e " ${CYAN}1${NC}) Вкл/Выкл подсчёт (сейчас: ${stats_on})"
|
||||||
|
echo -e " ${CYAN}2${NC}) Установить/обновить сборщик статистики"
|
||||||
|
echo -e " ${CYAN}0${NC}) ${DIM}← Назад${NC}"
|
||||||
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
|
echo -e " ${DIM}Обновление каждые 3 сек${NC}"
|
||||||
|
|
||||||
|
# Показываем курсор для ввода, потом снова скрываем
|
||||||
|
tput cnorm 2>/dev/null
|
||||||
|
echo -ne " ${WHITE}▸ ${NC}"
|
||||||
|
|
||||||
|
if read -t 3 -r ch; then
|
||||||
|
tput civis 2>/dev/null
|
||||||
|
case "$ch" in
|
||||||
|
1)
|
||||||
|
if type toggle_stats &>/dev/null; then
|
||||||
|
toggle_stats
|
||||||
|
echo -ne " ${DIM}Нажмите Enter...${NC}"; read -r
|
||||||
|
first_draw=1 # полная перерисовка после действия
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
if type install_stats_collector &>/dev/null; then
|
||||||
|
install_stats_collector
|
||||||
|
echo -ne " ${DIM}Нажмите Enter...${NC}"; read -r
|
||||||
|
first_draw=1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
0|"") return ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
tput civis 2>/dev/null
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
install_gotelegram_bot.sh
Normal file → Executable file
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
|
#!/bin/bash
|
||||||
# GoTelegram v2.2 — Общие утилиты
|
# GoTelegram v2.3 — Общие утилиты
|
||||||
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
||||||
|
|
||||||
# ── Версия ────────────────────────────────────────────────────────────────────
|
# ── Версия ────────────────────────────────────────────────────────────────────
|
||||||
GOTELEGRAM_VERSION="2.2.1"
|
GOTELEGRAM_VERSION="2.3.1"
|
||||||
GOTELEGRAM_NAME="GoTelegram"
|
GOTELEGRAM_NAME="GoTelegram"
|
||||||
|
|
||||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -270,7 +270,7 @@ save_gotelegram_config() {
|
|||||||
{
|
{
|
||||||
"version": "$GOTELEGRAM_VERSION",
|
"version": "$GOTELEGRAM_VERSION",
|
||||||
"engine": "${1:-telemt}",
|
"engine": "${1:-telemt}",
|
||||||
"mode": "${2:-quick}",
|
"mode": "${2:-lite}",
|
||||||
"port": ${3:-443},
|
"port": ${3:-443},
|
||||||
"secret": "${4:-}",
|
"secret": "${4:-}",
|
||||||
"mask_host": "${5:-google.com}",
|
"mask_host": "${5:-google.com}",
|
||||||
|
|||||||
406
lib/stats.sh
Executable file
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
|
NoNewPrivileges=true
|
||||||
ProtectSystem=strict
|
ProtectSystem=full
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ReadWritePaths=/etc/telemt /var/log
|
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
120
lib/telemt_config.sh
Normal file → Executable file
120
lib/telemt_config.sh
Normal file → Executable file
@@ -25,52 +25,49 @@ QUICK_DOMAINS=(
|
|||||||
"zoom.us"
|
"zoom.us"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Генерация TOML конфига ───────────────────────────────────────────────────
|
# ── Генерация TOML конфига (telemt v3 формат) ───────────────────────────────
|
||||||
generate_telemt_toml() {
|
generate_telemt_toml() {
|
||||||
local secret="$1"
|
local secret="$1"
|
||||||
local port="${2:-443}"
|
local port="${2:-443}"
|
||||||
local mask_mode="${3:-quick}" # quick | stealth
|
local mask_mode="${3:-lite}" # lite | pro
|
||||||
local mask_host="${4:-google.com}"
|
local mask_domain="${4:-google.com}"
|
||||||
local mask_port="${5:-443}"
|
local mask_port="${5:-443}"
|
||||||
local output="${6:-$TELEMT_CONFIG}"
|
local output="${6:-$TELEMT_CONFIG}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$output")"
|
mkdir -p "$(dirname "$output")"
|
||||||
|
|
||||||
# В stealth-режиме telemt слушает только localhost (трафик идёт через nginx)
|
# DNS override для pro: домен резолвится в 127.0.0.1
|
||||||
# В quick-режиме — на всех интерфейсах (клиенты подключаются напрямую)
|
# чтобы mask-трафик шёл на локальный nginx, а не в интернет
|
||||||
local bind_addr="0.0.0.0"
|
local dns_line=""
|
||||||
[ "$mask_mode" = "stealth" ] && bind_addr="127.0.0.1"
|
if [ "$mask_mode" = "pro" ]; then
|
||||||
|
dns_line="dns_overrides = [\"${mask_domain}:${mask_port}:127.0.0.1\"]"
|
||||||
|
fi
|
||||||
|
|
||||||
cat > "$output" << EOTOML
|
cat > "$output" << EOTOML
|
||||||
# GoTelegram v${GOTELEGRAM_VERSION} — telemt configuration
|
# GoTelegram v${GOTELEGRAM_VERSION} — telemt v3 configuration
|
||||||
# Сгенерировано: $(date -Iseconds)
|
# Сгенерировано: $(date -Iseconds)
|
||||||
# Режим: ${mask_mode}
|
# Режим: ${mask_mode}
|
||||||
|
|
||||||
# ── Основные настройки ───────────────────────────────────────────────────────
|
[server]
|
||||||
[stats]
|
port = ${port}
|
||||||
statsd_address = ""
|
listen_addr_ipv4 = "0.0.0.0"
|
||||||
|
|
||||||
# ── Секреты ──────────────────────────────────────────────────────────────────
|
[censorship]
|
||||||
[[users]]
|
tls_domain = "${mask_domain}"
|
||||||
name = "main"
|
mask = true
|
||||||
secret = "${secret}"
|
mask_port = ${mask_port}
|
||||||
|
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
|
||||||
|
|
||||||
# ── Привязка ─────────────────────────────────────────────────────────────────
|
[access.users]
|
||||||
[listen]
|
main = "${secret}"
|
||||||
# quick: 0.0.0.0 (клиенты напрямую) | stealth: 127.0.0.1 (только через nginx)
|
|
||||||
bind_to = "${bind_addr}:${port}"
|
|
||||||
|
|
||||||
# ── TLS маскировка ───────────────────────────────────────────────────────────
|
|
||||||
[security]
|
|
||||||
# Маскировочный хост — куда перенаправлять неопознанные подключения
|
|
||||||
# quick: внешний сайт | stealth: локальный nginx
|
|
||||||
host = "${mask_host}:${mask_port}"
|
|
||||||
|
|
||||||
|
[network]
|
||||||
|
${dns_line}
|
||||||
EOTOML
|
EOTOML
|
||||||
|
|
||||||
chmod 600 "$output"
|
chmod 600 "$output"
|
||||||
log_success "Конфиг telemt записан: $output"
|
log_success "Конфиг telemt записан: $output"
|
||||||
log_dim "Режим: $mask_mode, маскировка: $mask_host:$mask_port"
|
log_dim "Режим: $mask_mode, домен: $mask_domain, порт mask: $mask_port"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Добавление дополнительного секрета ───────────────────────────────────────
|
# ── Добавление дополнительного секрета ───────────────────────────────────────
|
||||||
@@ -95,7 +92,7 @@ EOSECRET
|
|||||||
log_success "Добавлен секрет: $name"
|
log_success "Добавлен секрет: $name"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Чтение текущего конфига ──────────────────────────────────────────────────
|
# ── Чтение текущего конфига (telemt v3 формат) ──────────────────────────────
|
||||||
get_config_value() {
|
get_config_value() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
local config="${2:-$TELEMT_CONFIG}"
|
local config="${2:-$TELEMT_CONFIG}"
|
||||||
@@ -104,13 +101,19 @@ get_config_value() {
|
|||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
secret)
|
secret)
|
||||||
grep -m1 'secret\s*=' "$config" | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' '
|
# [access.users] main = "..."
|
||||||
|
grep -A5 '\[access.users\]' "$config" | grep -m1 '=' | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' '
|
||||||
;;
|
;;
|
||||||
port)
|
port)
|
||||||
grep 'bind_to\s*=' "$config" | sed 's/.*:\([0-9]*\)".*/\1/'
|
# [server] port = 443
|
||||||
|
grep -A5 '\[server\]' "$config" | grep 'port\s*=' | head -1 | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
|
||||||
;;
|
;;
|
||||||
mask_host)
|
mask_host|tls_domain)
|
||||||
grep -A10 '\[security\]' "$config" | grep 'host\s*=' | sed 's/.*=\s*"\(.*\)".*/\1/'
|
# [censorship] tls_domain = "..."
|
||||||
|
grep -A10 '\[censorship\]' "$config" | grep 'tls_domain\s*=' | sed 's/.*=\s*"\(.*\)"/\1/'
|
||||||
|
;;
|
||||||
|
mask_port)
|
||||||
|
grep -A10 '\[censorship\]' "$config" | grep 'mask_port\s*=' | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "'
|
grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "'
|
||||||
@@ -151,7 +154,7 @@ validate_telemt_config() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$host" ]; then
|
if [ -z "$host" ]; then
|
||||||
log_error "Не задан маскировочный хост (security.host)"
|
log_error "Не задан маскировочный хост (censorship.tls_domain)"
|
||||||
((errors++))
|
((errors++))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -254,11 +257,21 @@ show_proxy_info() {
|
|||||||
port=$(get_config_value port "$config")
|
port=$(get_config_value port "$config")
|
||||||
mask_host=$(get_config_value mask_host "$config")
|
mask_host=$(get_config_value mask_host "$config")
|
||||||
ip=$(get_server_ip)
|
ip=$(get_server_ip)
|
||||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
|
||||||
status=$(telemt_status)
|
status=$(telemt_status)
|
||||||
|
|
||||||
local mode
|
local mode domain
|
||||||
mode=$(config_get mode 2>/dev/null || echo "quick")
|
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Pro-режим: ссылка с доменом и fake-TLS секретом
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local domain_hex faketls_secret
|
||||||
|
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||||
|
faketls_secret="ee${secret}${domain_hex}"
|
||||||
|
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||||
|
else
|
||||||
|
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||||
|
fi
|
||||||
|
|
||||||
local status_icon status_text
|
local status_icon status_text
|
||||||
case "$status" in
|
case "$status" in
|
||||||
@@ -271,7 +284,11 @@ show_proxy_info() {
|
|||||||
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||||
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
|
||||||
|
fi
|
||||||
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
|
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
|
||||||
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
||||||
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
||||||
@@ -286,3 +303,34 @@ show_proxy_info() {
|
|||||||
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
|
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Вывод информации о прокси (Pro-режим) ──────────────────────────────────
|
||||||
|
# В pro-режиме ссылка содержит домен (не IP) и fake-TLS секрет (ee...)
|
||||||
|
show_proxy_info_pro() {
|
||||||
|
local domain="$1"
|
||||||
|
local faketls_secret="$2"
|
||||||
|
|
||||||
|
local link="tg://proxy?server=${domain}&port=443&secret=${faketls_secret}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}✅ Pro-прокси настроен${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
|
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||||
|
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||||
|
echo -e " ${WHITE}Порт:${NC} ${CYAN}443${NC} (внешний, telemt)"
|
||||||
|
echo -e " ${WHITE}Режим:${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||||
|
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC} (внутренний)"
|
||||||
|
echo -e " ${WHITE}Secret:${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
|
echo -e " ${WHITE}Ссылка для Telegram:${NC}"
|
||||||
|
echo -e " ${GREEN}${link}${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}Провайдер видит: HTTPS-трафик к ${domain}:443${NC}"
|
||||||
|
echo -e " ${DIM}Telegram-клиент маскирует соединение под TLS${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# QR если доступен
|
||||||
|
if command -v qrencode &>/dev/null; then
|
||||||
|
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|||||||
0
lib/templates_catalog.sh
Normal file → Executable file
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
|
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||||
|
|
||||||
cat > "$NGINX_SITE_CONF" << 'EONGINX'
|
cat > "$NGINX_SITE_CONF" << 'EONGINX'
|
||||||
# GoTelegram v2.2 — nginx config
|
# GoTelegram v2.3 — nginx config
|
||||||
# Обслуживает сайт-маскировку для telemt stealth mode
|
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
|
||||||
|
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@@ -60,8 +61,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen SSL_PORT_PLACEHOLDER ssl http2;
|
listen 127.0.0.1:SSL_PORT_PLACEHOLDER ssl http2;
|
||||||
listen [::]:SSL_PORT_PLACEHOLDER ssl http2;
|
|
||||||
server_name DOMAIN_PLACEHOLDER;
|
server_name DOMAIN_PLACEHOLDER;
|
||||||
|
|
||||||
# SSL сертификаты
|
# SSL сертификаты
|
||||||
@@ -113,7 +113,7 @@ EONGINX
|
|||||||
local escaped_domain
|
local escaped_domain
|
||||||
escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g')
|
escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g')
|
||||||
sed -i "s|DOMAIN_PLACEHOLDER|${escaped_domain}|g" "$NGINX_SITE_CONF"
|
sed -i "s|DOMAIN_PLACEHOLDER|${escaped_domain}|g" "$NGINX_SITE_CONF"
|
||||||
sed -i "s|SSL_PORT_PLACEHOLDER|443|g" "$NGINX_SITE_CONF"
|
sed -i "s|SSL_PORT_PLACEHOLDER|${proxy_port}|g" "$NGINX_SITE_CONF"
|
||||||
|
|
||||||
# Активируем сайт
|
# Активируем сайт
|
||||||
rm -f /etc/nginx/sites-enabled/default 2>/dev/null
|
rm -f /etc/nginx/sites-enabled/default 2>/dev/null
|
||||||
@@ -258,14 +258,14 @@ deploy_template_to_nginx() {
|
|||||||
log_success "Шаблон развёрнут в $WEBSITE_ROOT"
|
log_success "Шаблон развёрнут в $WEBSITE_ROOT"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Полная установка stealth-режима ──────────────────────────────────────────
|
# ── Полная установка pro-режима ──────────────────────────────────────────────
|
||||||
setup_stealth_mode() {
|
setup_pro_mode() {
|
||||||
local domain="$1"
|
local domain="$1"
|
||||||
local template_dir="$2"
|
local template_dir="$2"
|
||||||
local proxy_port="${3:-443}"
|
local proxy_port="${3:-443}"
|
||||||
local email="${4:-}"
|
local email="${4:-}"
|
||||||
|
|
||||||
log_step "Настройка stealth-режима"
|
log_step "Настройка pro-режима"
|
||||||
|
|
||||||
# 1. Устанавливаем nginx
|
# 1. Устанавливаем nginx
|
||||||
run_with_spinner "Установка nginx" install_nginx || return 1
|
run_with_spinner "Установка nginx" install_nginx || return 1
|
||||||
@@ -298,7 +298,7 @@ setup_stealth_mode() {
|
|||||||
# 8. Показываем благодарности авторам шаблонов
|
# 8. Показываем благодарности авторам шаблонов
|
||||||
show_credits
|
show_credits
|
||||||
|
|
||||||
log_success "Stealth-режим настроен: https://${domain}"
|
log_success "Pro-режим настроен: https://${domain}"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,13 +322,13 @@ restart_nginx() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Удаление stealth-режима ──────────────────────────────────────────────────
|
# ── Удаление pro-режима ──────────────────────────────────────────────────────
|
||||||
remove_stealth_mode() {
|
remove_pro_mode() {
|
||||||
log_info "Удаление stealth-режима..."
|
log_info "Удаление pro-режима..."
|
||||||
rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||||
rm -rf "$WEBSITE_ROOT"
|
rm -rf "$WEBSITE_ROOT"
|
||||||
systemctl restart nginx 2>/dev/null
|
systemctl restart nginx 2>/dev/null
|
||||||
log_success "Stealth-режим удалён (nginx оставлен)"
|
log_success "Pro-режим удалён (nginx оставлен)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Смена шаблона ────────────────────────────────────────────────────────────
|
# ── Смена шаблона ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user