mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 16:46:03 +00:00
734 lines
28 KiB
Bash
Executable File
734 lines
28 KiB
Bash
Executable File
#!/bin/bash
|
||
# GoTelegram v2.5.0 — common utilities
|
||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||
|
||
# ── Version ───────────────────────────────────────────────────────────────────
|
||
GOTELEGRAM_VERSION="2.5.0"
|
||
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"
|
||
ADMIN_WEB_DIR="/opt/gotelegram-admin"
|
||
ADMIN_WEB_SERVICE="gotelegram-admin"
|
||
ADMIN_WEB_HOST="127.0.0.1"
|
||
ADMIN_WEB_PORT="1984"
|
||
|
||
# ── 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<filled; i++)); do bar+="█"; done
|
||
for ((i=0; i<empty; i++)); do bar+="░"; done
|
||
printf "\r ${GREEN}[${bar}]${NC} ${pct}%% ${label}" >&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 <pkg> [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 <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" ;;
|
||
iptables) echo "iptables" ;;
|
||
*) 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" ;;
|
||
iptables) echo "iptables" ;;
|
||
*) 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 iptables)
|
||
|
||
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 # свободен
|
||
}
|
||
|
||
detect_3xui() {
|
||
if systemctl list-unit-files 2>/dev/null | grep -Eq '^(x-ui|3x-ui)\.service'; then
|
||
return 0
|
||
fi
|
||
[ -d /etc/x-ui ] || [ -d /usr/local/x-ui ] || [ -f /etc/x-ui/x-ui.db ]
|
||
}
|
||
|
||
detect_3xui_443_listener() {
|
||
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiq '(xray|x-ui|3x-ui)'
|
||
}
|
||
|
||
warn_3xui_443_conflict() {
|
||
detect_3xui_443_listener || return 1
|
||
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
|
||
log_warning "GoTelegram не будет молча останавливать или переписывать 3x-ui."
|
||
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и GoTelegram."
|
||
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
|
||
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
|
||
# GoTelegram + 3x-ui on one TCP/443
|
||
|
||
GoTelegram detected that 3x-ui/Xray already owns TCP/443. Two independent
|
||
processes cannot bind the same IP:port at the same time. A safe shared setup
|
||
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
|
||
|
||
- dispatcher: 0.0.0.0:443
|
||
- GoTelegram telemt: 127.0.0.1:7443
|
||
- 3x-ui/Xray inbound: 127.0.0.1:9443
|
||
- GoTelegram nginx mask site: 127.0.0.1:8443
|
||
|
||
The dispatcher must route Xray SNI domains to Xray and route the GoTelegram
|
||
SNI domain to telemt. If Xray and GoTelegram use the same SNI domain, automatic
|
||
sharing is not reliable: the first TLS ClientHello is intentionally identical.
|
||
|
||
GoTelegram intentionally does not rewrite the 3x-ui SQLite database or generated
|
||
Xray config without explicit operator confirmation, because 3x-ui can overwrite
|
||
manual JSON edits on the next panel change.
|
||
EOF
|
||
return 0
|
||
}
|
||
|
||
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
|
||
}
|