#!/bin/bash # GoTelegram v2.4 — common utilities # Colors, logging, spinner, system helpers, v1 compat, i18n-aware # ── Version ─────────────────────────────────────────────────────────────────── GOTELEGRAM_VERSION="2.4.9" GOTELEGRAM_NAME="GoTelegram" # ── Пути ────────────────────────────────────────────────────────────────────── GOTELEGRAM_DIR="/opt/gotelegram" GOTELEGRAM_CONFIG="$GOTELEGRAM_DIR/config.json" TELEMT_CONFIG="/etc/telemt/config.toml" TELEMT_BIN="/usr/local/bin/telemt" TELEMT_SERVICE="telemt" NGINX_SITE_CONF="/etc/nginx/sites-available/gotelegram" NGINX_SITE_LINK="/etc/nginx/sites-enabled/gotelegram" WEBSITE_ROOT="/var/www/gotelegram-site" BACKUP_DIR="$GOTELEGRAM_DIR/backups" LOG_FILE="/var/log/gotelegram.log" BOT_DIR="/opt/gotelegram-bot" # ── V1 совместимость ───────────────────────────────────────────────────────── V1_CONTAINER_NAME="mtproto-proxy" V1_CONFIG_FILE="/opt/gotelegram-bot/proxy.json" V1_SERVICE_NAME="gotelegram-bot" # ── Цвета ──────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' CYAN='\033[0;36m' YELLOW='\033[1;33m' MAGENTA='\033[0;35m' BLUE='\033[0;34m' WHITE='\033[1;37m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # ── Логирование ────────────────────────────────────────────────────────────── log_info() { echo -e " ${CYAN}ℹ${NC} $*" >&2; } log_success() { echo -e " ${GREEN}✓${NC} $*" >&2; } log_warning() { echo -e " ${YELLOW}⚠${NC} $*" >&2; } log_error() { echo -e " ${RED}✗${NC} $*" >&2; } log_step() { echo -e "\n${BOLD}${WHITE} $*${NC}" >&2; } log_dim() { echo -e " ${DIM}$*${NC}" >&2; } log_to_file() { local ts; ts=$(date '+%Y-%m-%d %H:%M:%S') echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null } # ── Spinner ────────────────────────────────────────────────────────────────── _spin_pid="" spinner_start() { local default_msg default_msg=$(type t &>/dev/null && t wait || echo "Please wait...") local msg="${1:-$default_msg}" ( local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local i=0 while true; do printf "\r ${CYAN}${frames[$i]}${NC} ${msg}" >&2 i=$(( (i+1) % ${#frames[@]} )) sleep 0.1 done ) & _spin_pid=$! } spinner_stop() { [ -n "$_spin_pid" ] && kill "$_spin_pid" 2>/dev/null && wait "$_spin_pid" 2>/dev/null _spin_pid="" printf "\r\033[K" >&2 } # ── Прогресс-бар ───────────────────────────────────────────────────────────── progress_bar() { local current="$1" total="$2" label="${3:-}" local pct=$(( current * 100 / total )) local filled=$(( pct / 2 )) local empty=$(( 50 - filled )) local bar="" for ((i=0; i&2 [ "$current" -eq "$total" ] && echo "" >&2 } # ── Выполнение с индикатором ───────────────────────────────────────────────── run_with_spinner() { local label="$1"; shift local err_file="/tmp/.gotelegram_spinner_err_$$" spinner_start "$label" "$@" >/dev/null 2>"$err_file" local rc=$? spinner_stop if [ $rc -eq 0 ]; then log_success "$label" else local err_label err_label=$(type t &>/dev/null && t error || echo "error") log_error "$label ${RED}(${err_label}, code: $rc)${NC}" if [ -s "$err_file" ]; then log_dim " $(head -3 "$err_file")" fi fi rm -f "$err_file" return $rc } # ── Banner ─────────────────────────────────────────────────────────────────── show_banner() { local line line=$(printf '━%.0s' $(seq 1 60)) echo "" echo -e "${CYAN}${line}${NC}" if type tf &>/dev/null; then echo -e " ${BOLD}${WHITE}🚀 $(tf banner_title "$GOTELEGRAM_VERSION")${NC}" echo -e " ${DIM}$(t banner_subtitle)${NC}" echo -e " ${DIM}$(t banner_features)${NC}" else echo -e " ${BOLD}${WHITE}🚀 GoTelegram v${GOTELEGRAM_VERSION}${NC}" echo -e " ${DIM}MTProxy powered by telemt (Rust + Tokio)${NC}" echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}" fi echo -e "${CYAN}${line}${NC}" echo "" } # ── Credits ────────────────────────────────────────────────────────────────── show_credits() { local line line=$(printf '─%.0s' $(seq 1 60)) echo "" echo -e "${MAGENTA}${line}${NC}" echo -e " ${BOLD}$(type t &>/dev/null && t credits_title || echo 'Credits')${NC}" echo -e "${MAGENTA}${line}${NC}" echo -e " ${WHITE}telemt${NC} — MTProxy engine (Rust)" echo -e " ${DIM}github.com/telemt/telemt${NC}" echo "" echo -e " ${WHITE}HTML5 UP${NC} — responsive HTML/CSS templates" echo -e " ${DIM}html5up.net • CC BY 3.0 • @ajlkn${NC}" echo "" echo -e " ${WHITE}learning-zone${NC} — 150+ HTML5 templates" echo -e " ${DIM}github.com/learning-zone/website-templates${NC}" echo "" echo -e " ${WHITE}Start Bootstrap${NC} — MIT license" echo -e " ${DIM}startbootstrap.com${NC}" echo -e "${MAGENTA}${line}${NC}" echo "" } # ── Системные утилиты ──────────────────────────────────────────────────────── _valid_ip() { # Validate that each octet is 0-255 local ip="$1" local IFS='.' read -ra octets <<< "$ip" [ ${#octets[@]} -ne 4 ] && return 1 for octet in "${octets[@]}"; do [[ "$octet" =~ ^[0-9]+$ ]] || return 1 [ "$octet" -gt 255 ] && return 1 done return 0 } get_server_ip() { local ip raw for url in "https://api.ipify.org" "https://icanhazip.com" "https://ifconfig.me"; do raw=$(curl -s -4 --max-time 5 "$url" 2>/dev/null) ip=$(echo "$raw" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) if [ -n "$ip" ] && _valid_ip "$ip"; then echo "$ip" return 0 fi done echo "0.0.0.0" return 1 } _t_or() { # Helper: translate if i18n available, otherwise return fallback local key="$1" fallback="$2" if type t &>/dev/null; then t "$key" else echo "$fallback" fi } check_root() { if [ "$EUID" -ne 0 ]; then log_error "$(_t_or err_need_root 'Run the script with sudo / as root')" exit 1 fi } check_os() { if [ ! -f /etc/os-release ]; then log_error "$(_t_or err_os_unknown 'Failed to detect OS. Linux is required.')" return 1 fi # Validate os-release before sourcing (reject command injection: ;, backticks, $()) if grep -qE '(;|`|\$\(|\$\{)' /etc/os-release 2>/dev/null; then log_warning "/etc/os-release contains suspicious strings, skipping" return 0 fi . /etc/os-release case "$ID" in ubuntu|debian|centos|rocky|almalinux|fedora|rhel) log_dim "OS: $PRETTY_NAME" return 0 ;; *) log_warning "OS $ID may be incompatible. Supported: Ubuntu, Debian, CentOS, Rocky." return 0 ;; esac } get_arch() { local arch arch=$(uname -m) case "$arch" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; armv7*|armhf) echo "armv7" ;; *) echo "$arch" ;; esac } get_pkg_manager() { if command -v apt-get &>/dev/null; then echo "apt" elif command -v dnf &>/dev/null; then echo "dnf" elif command -v yum &>/dev/null; then echo "yum" else echo "unknown" fi } install_pkg() { local pkg="$1" case "$(get_pkg_manager)" in apt) apt_install "$pkg" ;; dnf) dnf install -y -q "$pkg" ;; yum) yum install -y -q "$pkg" ;; *) log_error "$(_t_or err_bad_pkg_mgr 'Unknown package manager')"; return 1 ;; esac } # ── apt lock wait + install ───────────────────────────────────────────────── # На свежих Ubuntu/Debian unattended-upgrades часто держит dpkg lock на старте # → любой apt-get install падает с "Could not get lock /var/lib/dpkg/lock-frontend". # Эти функции ждут освобождения лока до 300с, потом запускают apt с нативным # таймаутом DPkg::Lock::Timeout. Использовать везде, где раньше был # "apt-get install ...". apt_lock_wait() { local max_wait="${1:-300}" local waited=0 local warned=0 while fuser /var/lib/dpkg/lock-frontend &>/dev/null \ || fuser /var/lib/dpkg/lock &>/dev/null \ || fuser /var/lib/apt/lists/lock &>/dev/null \ || pgrep -f '^/usr/bin/unattended-upgrade' &>/dev/null; do if [ "$warned" = "0" ]; then log_warning "apt/dpkg locked by unattended-upgrades, waiting up to ${max_wait}s..." warned=1 fi sleep 3 waited=$((waited + 3)) if [ "$waited" -ge "$max_wait" ]; then log_error "apt lock not released after ${max_wait}s" log_dim "Manual fix: systemctl stop unattended-upgrades && killall -9 unattended-upgr 2>/dev/null; dpkg --configure -a" return 1 fi done [ "$warned" = "1" ] && log_success "apt lock released (waited ${waited}s)" return 0 } # apt_install [pkg2 ...] — ждёт lock + ставит пакеты + показывает ошибку apt_install() { [ $# -eq 0 ] && return 0 apt_lock_wait || return 1 export DEBIAN_FRONTEND=noninteractive local opts="-o DPkg::Lock::Timeout=120" local err_file; err_file=$(mktemp 2>/dev/null || echo /tmp/apt_err.$$) if ! apt-get $opts install -y -qq "$@" 2>"$err_file"; then log_error "apt-get install failed: $*" [ -s "$err_file" ] && tail -n 5 "$err_file" | sed 's/^/ /' >&2 rm -f "$err_file" return 1 fi rm -f "$err_file" return 0 } # apt_update — тихий update с ожиданием лока apt_update() { apt_lock_wait || return 1 export DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 update -qq 2>/dev/null || true return 0 } # ── Зависимости GoTelegram ────────────────────────────────────────────────── # Полный список внешних команд, которые скрипт использует. Для каждой команды # указан пакет на apt и dnf/yum (имена различаются: например dig = dnsutils на # Debian, bind-utils на RHEL). # # КРИТИЧЕСКИЕ (без них скрипт просто не работает): # jq — парсинг config.json, templates_catalog.json # curl — скачивание telemt и проверки HTTPS # openssl — генерация секретов, шифрование бекапов, SSL проверка # git — клонирование шаблонов через download_template # xxd — hex-encode домена для fake-TLS секрета (ee-prefix) # tar — распаковка telemt архива и бекапы # dig — DNS-проверка домена в Pro-режиме # # ЖЕЛАТЕЛЬНЫЕ (есть fallback, но с ними лучше): # qrencode — QR-коды для прокси-ссылок # bc — красивое форматирование чисел в статистике # # Pro-режим доустанавливает nginx/certbot через install_nginx/install_certbot # (они большие и нужны только если пользователь выбрал Pro). # Маппинг команды -> (apt_pkg, dnf_pkg). apt_pkg_for_cmd apt_pkg_for_cmd() { case "$1" in dig) echo "dnsutils" ;; xxd) echo "xxd" ;; # Ubuntu 22+: отдельный пакет, fallback ниже nslookup) echo "dnsutils" ;; host) echo "dnsutils" ;; ss) echo "iproute2" ;; netstat) echo "net-tools" ;; flock) echo "util-linux" ;; *) echo "$1" ;; # команда == имя пакета esac } dnf_pkg_for_cmd() { case "$1" in dig|nslookup|host) echo "bind-utils" ;; xxd) echo "vim-common" ;; ss) echo "iproute" ;; netstat) echo "net-tools" ;; flock) echo "util-linux" ;; *) echo "$1" ;; esac } ensure_deps() { # Критические зависимости — без них скрипт не работает. # flock используется bot_action_dispatch для сериализации параллельных # вызовов (иначе гонка на config.json при одновременных change-template / # change-lite-domain из бота). local critical=(curl jq openssl git xxd tar dig flock) # Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли local optional=(qrencode bc) local missing_critical=() missing_optional=() cmd for cmd in "${critical[@]}"; do command -v "$cmd" &>/dev/null || missing_critical+=("$cmd") done for cmd in "${optional[@]}"; do command -v "$cmd" &>/dev/null || missing_optional+=("$cmd") done local all_missing=("${missing_critical[@]}" "${missing_optional[@]}") [ ${#all_missing[@]} -eq 0 ] && return 0 # Собираем список пакетов для выбранного менеджера local pkg_mgr pkg pkgs=() pkg_mgr=$(get_pkg_manager) for cmd in "${all_missing[@]}"; do case "$pkg_mgr" in apt) pkg=$(apt_pkg_for_cmd "$cmd") ;; dnf|yum) pkg=$(dnf_pkg_for_cmd "$cmd") ;; *) pkg="$cmd" ;; esac pkgs+=("$pkg") done # Убираем дубликаты (например dig+nslookup оба = dnsutils) local uniq_pkgs=() for pkg in "${pkgs[@]}"; do local found=0 p for p in "${uniq_pkgs[@]}"; do [ "$p" = "$pkg" ] && { found=1; break; } done [ "$found" = "0" ] && uniq_pkgs+=("$pkg") done if type tf &>/dev/null; then log_step "$(tf deps_installing "${all_missing[*]}")" else log_step "Installing dependencies: ${all_missing[*]} (packages: ${uniq_pkgs[*]})" fi case "$pkg_mgr" in apt) apt_update apt_install "${uniq_pkgs[@]}" || true ;; dnf) dnf install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;; yum) yum install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;; *) log_error "Unknown package manager — install manually: ${uniq_pkgs[*]}" return 1 ;; esac # Фолбэки для xxd: на некоторых системах нужен vim-common вместо xxd if ! command -v xxd &>/dev/null && [ "$pkg_mgr" = "apt" ]; then apt_install vim-common || true fi # Повторная проверка критических команд local still_missing=() for cmd in "${critical[@]}"; do command -v "$cmd" &>/dev/null || still_missing+=("$cmd") done if [ ${#still_missing[@]} -gt 0 ]; then log_error "Critical dependencies still missing: ${still_missing[*]}" log_error "Install manually and re-run gotelegram" return 1 fi # Опциональные — только предупреждение local still_missing_opt=() for cmd in "${optional[@]}"; do command -v "$cmd" &>/dev/null || still_missing_opt+=("$cmd") done if [ ${#still_missing_opt[@]} -gt 0 ]; then log_warning "Optional deps missing (features degraded): ${still_missing_opt[*]}" fi log_success "Dependencies ready" return 0 } # Быстрая проверка — только смотрит что критические установлены, ничего не ставит. # Возвращает 0 если всё ок, 1 если что-то отсутствует. Используется на старте # main() чтобы не дёргать apt-get update при каждом запуске меню. check_deps_present() { local cmd for cmd in curl jq openssl git xxd tar dig flock; do command -v "$cmd" &>/dev/null || return 1 done return 0 } check_port() { local port="$1" local line line=$(ss -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1) [ -z "$line" ] && line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1) if [ -n "$line" ]; then echo "$line" return 0 # порт занят fi return 1 # свободен } # ── Preflight: port conflict detection ─────────────────────────────────────── # Проверяет, что нужные для установки порты свободны. Если порт занят — # определяет процесс и сопоставляет с известным списком proxy/VPN софта # (xray, sing-box, v2ray, trojan, hysteria, mtg, shadowsocks, x-ui/3x-ui, # marzban, amneziawg, caddy, apache, haproxy). Пользователь видит явное # предупреждение и может либо прервать установку, либо продолжить на свой # страх и риск (GOTELEGRAM_SKIP_PREFLIGHT=1 — полностью отключить проверку). # # Используемые порты GoTelegram: # 443 — telemt (внешний, MTProxy + fake-TLS) — lite и pro # 80 — nginx redirect + certbot ACME HTTP-01 — только pro # 8443 — nginx internal mask (127.0.0.1:8443) — только pro # get_port_process → "|" если занят, иначе пусто get_port_process() { local port="$1" local line="" pid="" proc="" line=$(ss -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1) if [ -z "$line" ]; then line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1) fi if [ -n "$line" ]; then pid=$(echo "$line" | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2) if [ -z "$pid" ]; then # netstat format: "12345/procname" pid=$(echo "$line" | grep -oE '[0-9]+/[^ ]+' | head -1 | cut -d/ -f1) fi fi if [ -z "$pid" ]; then pid=$(fuser -n tcp "$port" 2>/dev/null | tr -s ' ' | awk '{print $1}' | head -1) pid="${pid:-}" fi if [ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null; then proc=$(ps -p "$pid" -o comm= 2>/dev/null | tr -d ' \n') [ -z "$proc" ] && proc="unknown" echo "${pid}|${proc}" return 0 fi if [ -n "$line" ]; then # Port is occupied but process cannot be identified (kernel socket / no root) echo "0|unknown" return 0 fi return 1 } # match_known_conflict → печатает человекочитаемое имя если это # известный proxy/VPN/web софт. Возвращает 0 если нашли, 1 иначе. match_known_conflict() { local proc="$1" case "$proc" in *xray*|*Xray*) echo "Xray"; return 0 ;; *sing-box*|*sing_box*|*singbox*) echo "sing-box"; return 0 ;; *v2ray*|*V2Ray*) echo "V2Ray"; return 0 ;; *trojan*) echo "Trojan"; return 0 ;; *hysteria*) echo "Hysteria"; return 0 ;; *mtg*) echo "mtg (old MTProxy)"; return 0 ;; *ss-server*|*ss-local*|*shadowsocks*|*ssserver*) echo "Shadowsocks"; return 0 ;; *x-ui*|*3x-ui*|*xui*) echo "x-ui / 3x-ui panel"; return 0 ;; *marzban*) echo "Marzban panel"; return 0 ;; *amneziawg*|*awg-go*|*awg*) echo "AmneziaWG"; return 0 ;; *caddy*) echo "Caddy web server"; return 0 ;; *apache2*|*httpd*) echo "Apache httpd"; return 0 ;; *haproxy*) echo "HAProxy"; return 0 ;; *nginx*) echo "nginx (already running)"; return 0 ;; *tgproxy*|*mtproxy*|*mtproto*) echo "MTProto Proxy (other impl)"; return 0 ;; *wireguard*|*wg-quick*) echo "WireGuard"; return 0 ;; *openvpn*) echo "OpenVPN"; return 0 ;; esac return 1 } # preflight_check [port] # mode = "lite" | "pro" # port = selected port for lite mode (default 443) # Returns: # 0 — OK to proceed (no conflicts, or user confirmed to force) # 1 — user aborted (caller should show promo and return) preflight_check() { local mode="${1:-lite}" local lite_port="${2:-443}" # Escape hatch if [ "${GOTELEGRAM_SKIP_PREFLIGHT:-0}" = "1" ]; then log_dim "preflight: skipped (GOTELEGRAM_SKIP_PREFLIGHT=1)" return 0 fi local required_ports=() if [ "$mode" = "pro" ]; then required_ports=(443 80 8443) else # lite: проверяем только выбранный внешний порт required_ports=("$lite_port") fi local known_conflicts=() unknown_conflicts=() info port pid proc label for port in "${required_ports[@]}"; do info=$(get_port_process "$port") if [ -n "$info" ]; then pid="${info%%|*}" proc="${info##*|}" if label=$(match_known_conflict "$proc"); then known_conflicts+=("${port}|${label}|${pid}|${proc}") else unknown_conflicts+=("${port}|${pid}|${proc}") fi fi done if [ ${#known_conflicts[@]} -eq 0 ] && [ ${#unknown_conflicts[@]} -eq 0 ]; then log_dim "preflight: ports ${required_ports[*]} свободны" return 0 fi # Показываем баннер конфликта echo "" >&2 echo -e " ${BOLD}${YELLOW}⚠ $(_t_or preflight_title 'Предустановочная проверка: обнаружены конфликты портов')${NC}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 local item p label2 pid2 proc2 rest if [ ${#known_conflicts[@]} -gt 0 ]; then echo -e " ${RED}$(_t_or preflight_known 'Известный proxy/VPN/веб-софт занимает нужные порты:')${NC}" >&2 for item in "${known_conflicts[@]}"; do p="${item%%|*}" rest="${item#*|}" label2="${rest%%|*}" rest="${rest#*|}" pid2="${rest%%|*}" proc2="${rest##*|}" echo -e " ${RED}✗${NC} ${BOLD}:${p}${NC} → ${BOLD}${label2}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2 done fi if [ ${#unknown_conflicts[@]} -gt 0 ]; then echo -e " ${YELLOW}$(_t_or preflight_unknown 'Порты заняты неизвестными процессами:')${NC}" >&2 for item in "${unknown_conflicts[@]}"; do p="${item%%|*}" rest="${item#*|}" pid2="${rest%%|*}" proc2="${rest##*|}" echo -e " ${YELLOW}⚠${NC} ${BOLD}:${p}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2 done fi echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 echo -e " ${WHITE}$(_t_or preflight_needed 'GoTelegram нужны порты:')${NC} ${CYAN}${required_ports[*]}${NC}" >&2 echo -e " ${WHITE}$(_t_or preflight_hint_header 'Рекомендации:')${NC}" >&2 echo -e " ${DIM}• $(_t_or preflight_hint1 'Остановите и удалите конфликтующие сервисы (systemctl stop ...)')${NC}" >&2 echo -e " ${DIM}• $(_t_or preflight_hint2 'Либо возьмите чистый VPS без других прокси')${NC}" >&2 echo -e " ${DIM}• $(_t_or preflight_hint3 'Установка поверх, скорее всего, завершится некорректно')${NC}" >&2 echo -e " ${DIM}$(_t_or preflight_skip_hint 'Override: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram')${NC}" >&2 echo "" >&2 if confirm "$(_t_or preflight_proceed 'Продолжить установку всё равно (скорее всего не заработает)?')"; then log_warning "$(_t_or preflight_forced 'Установка продолжена вопреки конфликтам — возможны ошибки')" return 0 fi log_info "$(_t_or preflight_aborted 'Установка отменена из-за конфликтов портов')" return 1 } check_disk_space() { local min_mb="${1:-500}" local avail_mb avail_mb=$(df -m / | awk 'NR==2 {print $4}') if [ "$avail_mb" -lt "$min_mb" ]; then if type tf &>/dev/null; then log_error "$(tf err_low_disk "$avail_mb" "$min_mb")" else log_error "Low disk space: ${avail_mb}MB (need ${min_mb}MB+)" fi return 1 fi return 0 } # ── Конфигурация GoTelegram (JSON) ────────────────────────────────────────── save_gotelegram_config() { mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")" local cur_lang cur_lang=$(type get_language &>/dev/null && get_language || echo en) cat > "$GOTELEGRAM_CONFIG" << EOJSON { "version": "$GOTELEGRAM_VERSION", "engine": "${1:-telemt}", "mode": "${2:-lite}", "port": ${3:-443}, "secret": "${4:-}", "mask_host": "${5:-google.com}", "domain": "${6:-}", "template_id": "${7:-}", "language": "${cur_lang}", "installed_at": "$(date -Iseconds)", "updated_at": "$(date -Iseconds)" } EOJSON chmod 600 "$GOTELEGRAM_CONFIG" } load_gotelegram_config() { if [ -f "$GOTELEGRAM_CONFIG" ]; then cat "$GOTELEGRAM_CONFIG" return 0 fi echo "{}" return 1 } config_get() { local key="$1" if [ ! -f "$GOTELEGRAM_CONFIG" ]; then return 2 # file missing fi local val val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null) if [ $? -ne 0 ]; then return 3 # invalid JSON fi if [ -z "$val" ]; then return 1 # key missing or empty fi echo "$val" return 0 } # ── V1 совместимость ───────────────────────────────────────────────────────── detect_v1_installation() { # Проверяем наличие mtg Docker контейнера (v1) if command -v docker &>/dev/null; then if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${V1_CONTAINER_NAME}$"; then return 0 # v1 обнаружена fi fi # Проверяем наличие конфига v1 if [ -f "$V1_CONFIG_FILE" ]; then return 0 fi return 1 } get_v1_config() { # Извлекаем данные из работающего v1 контейнера if ! command -v docker &>/dev/null; then echo "{}" return 1 fi local running running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep "^${V1_CONTAINER_NAME}$") if [ -z "$running" ]; then # Пробуем из сохранённого конфига if [ -f "$V1_CONFIG_FILE" ]; then cat "$V1_CONFIG_FILE" return 0 fi echo "{}" return 1 fi # Достаём из Docker local cmd_str port secret ip cmd_str=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range .Config.Cmd}}{{.}} {{end}}' 2>/dev/null) secret=$(echo "$cmd_str" | awk '{print $NF}') port=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}}{{end}}' 2>/dev/null) ip=$(get_server_ip) jq -n \ --arg secret "$secret" \ --arg port "${port:-443}" \ --arg ip "$ip" \ '{secret: $secret, port: ($port | tonumber), ip: $ip, engine: "mtg"}' } migrate_v1_to_v2() { log_step "$(_t_or v1_migration_step 'Migrating from v1 (mtg) to v2 (telemt)')" local v1_config v1_config=$(get_v1_config) local old_port old_secret old_port=$(echo "$v1_config" | jq -r '.port // 443') old_secret=$(echo "$v1_config" | jq -r '.secret // empty') if [ -z "$old_secret" ]; then log_warning "Failed to extract secret from v1. A new one will be generated." return 1 fi echo "" echo -e " ${WHITE}$(_t_or v1_found_title 'Found v1 (mtg) installation:')${NC}" if type tf &>/dev/null; then echo -e " $(tf v1_port "$old_port")" echo -e " $(tf v1_secret "${old_secret:0:16}")" else echo -e " Port: ${CYAN}${old_port}${NC}" echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}" fi echo "" echo -e " ${YELLOW}$(_t_or warning 'Warning'):${NC} $(_t_or v1_incompatible 'mtg secret is NOT directly compatible with telemt.')" echo -e " $(_t_or v1_new_link 'Clients will need a new link.')" echo "" echo -ne " $(_t_or v1_stop_migrate 'Stop v1 container and migrate to v2? [Y/n]:') " read -r ans if [[ "$ans" =~ ^[Nn] ]]; then log_info "$(_t_or v1_migration_cancelled 'Migration cancelled. v1 left intact.')" return 1 fi # Stop v1 log_info "$(_t_or v1_stopping 'Stopping v1 container...')" docker stop "$V1_CONTAINER_NAME" 2>/dev/null docker rm "$V1_CONTAINER_NAME" 2>/dev/null # Backup v1 config if [ -f "$V1_CONFIG_FILE" ]; then mkdir -p "$GOTELEGRAM_DIR" cp "$V1_CONFIG_FILE" "$GOTELEGRAM_DIR/v1_backup_proxy.json" 2>/dev/null if type tf &>/dev/null; then log_success "$(tf v1_config_saved "$GOTELEGRAM_DIR/v1_backup_proxy.json")" else log_success "v1 config saved to $GOTELEGRAM_DIR/v1_backup_proxy.json" fi fi if type tf &>/dev/null; then log_success "$(tf v1_port_freed "$old_port")" else log_success "v1 stopped. Port $old_port freed." fi return 0 } # ── Confirm prompt ─────────────────────────────────────────────────────────── confirm() { local default_msg default_msg=$(_t_or install_continue_anyway 'Continue?') local msg="${1:-$default_msg}" echo -ne " ${msg} [Y/n]: " >&2 read -r ans [[ ! "$ans" =~ ^[Nn] ]] } # ── Выбор из списка ────────────────────────────────────────────────────────── select_option() { local title="$1" shift local options=("$@") echo "" >&2 echo -e " ${BOLD}${WHITE}${title}${NC}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2 local i=1 for opt in "${options[@]}"; do echo -e " ${CYAN}${i})${NC} ${opt}" >&2 ((i++)) done echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2 echo -ne " ${WHITE}$(_t_or choose 'Choose'):${NC} " >&2 read -r choice echo "$choice" } # ── Генерация случайного hex ───────────────────────────────────────────────── generate_hex() { local len="${1:-32}" openssl rand -hex "$((len/2))" 2>/dev/null || head -c "$((len/2))" /dev/urandom | xxd -p | tr -d '\n' } # ── Проверка домена ────────────────────────────────────────────────────────── validate_domain() { local domain="$1" if echo "$domain" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; then return 0 fi return 1 } # ── Init: создание директорий ──────────────────────────────────────────────── init_dirs() { mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR" /etc/telemt 2>/dev/null touch "$LOG_FILE" 2>/dev/null }