Files
gotelegram_pro/lib/common.sh
2026-04-25 09:44:53 +03:00

734 lines
28 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# goTelegram Pro v2.5.0 — common utilities
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
# ── Version ───────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.5.0"
GOTELEGRAM_NAME="goTelegram Pro"
# ── Пути ──────────────────────────────────────────────────────────────────────
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 Pro 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 Pro не будет молча останавливать или переписывать 3x-ui."
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и goTelegram Pro."
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
# goTelegram Pro + 3x-ui on one TCP/443
goTelegram Pro 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 Pro telemt: 127.0.0.1:7443
- 3x-ui/Xray inbound: 127.0.0.1:9443
- goTelegram Pro nginx mask site: 127.0.0.1:8443
The dispatcher must route Xray SNI domains to Xray and route the goTelegram Pro
SNI domain to telemt. If Xray and goTelegram Pro use the same SNI domain, automatic
sharing is not reliable: the first TLS ClientHello is intentionally identical.
goTelegram Pro 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
}