#!/bin/bash # GoTelegram v2.4.9 — Unified Backup Format (UBF) v2.0 # # UBF v2.0 layout (inside the tarball): # gotelegram_backup_YYYYMMDD_HHMMSS_/ # ├── metadata.json # backup_id, versions, fingerprint, ... # ├── secrets.json # raw_secret, faketls_secret, proxy_link, bot_token # ├── telemt/config.toml # ├── gotelegram/config.json # ├── gotelegram/.language # ├── nginx/site.conf # ├── letsencrypt/ # │ ├── live//{fullchain,privkey,chain,cert}.pem # │ └── renewal/.conf # ├── site/ (nginx document root) # └── bot/.env # # Backup ID format: GT-YYMMDD- # Archive name: gotelegram_backup_YYYYMMDD_HHMMSS_.tar.gz[.enc] # # Encryption: AES-256-CBC + PBKDF2 (optional, password-based) # Integrity: SHA-256 sidecar file (.sha256) # # Restore path automatically detects v1.1 (legacy) vs v2.0 layouts by reading # metadata.json.backup_version. When restoring a v1.1 archive the script # immediately writes a fresh v2.0 backup alongside the old one, so subsequent # reinstalls can benefit from the new format. # ── Utility: generate a backup ID from a raw secret ───────────────────────── # Format: GT-YYMMDD-. Deterministic per-day per-key; easy to read. generate_backup_id() { local raw_secret="$1" local date_part date_part=$(date +%y%m%d) local last6="000000" if [ -n "$raw_secret" ] && [ ${#raw_secret} -ge 6 ]; then last6="${raw_secret: -6}" last6=$(echo "$last6" | tr 'A-F' 'a-f') fi echo "GT-${date_part}-${last6}" } # ── Utility: SHA-256 fingerprint of a raw secret ──────────────────────────── secret_fingerprint() { local raw_secret="$1" [ -z "$raw_secret" ] && { echo ""; return; } printf '%s' "$raw_secret" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}' } # ── Utility: hex-encode an ASCII string (for fake-TLS secret) ─────────────── _hex_encode() { printf '%s' "$1" | xxd -p | tr -d '\n' } # ── Создание бекапа (UBF v2.0) ────────────────────────────────────────────── create_backup() { local password="${1:-}" local output_dir="${2:-$BACKUP_DIR}" # Pull current config (so backup_id can include the real secret) local raw_secret domain mode engine port lang tpl_id mask_host raw_secret=$(config_get secret 2>/dev/null || echo "") domain=$(config_get domain 2>/dev/null || echo "") mode=$(config_get mode 2>/dev/null || echo "unknown") engine=$(config_get engine 2>/dev/null || echo "telemt") port=$(config_get port 2>/dev/null || echo "443") lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en") tpl_id=$(config_get template_id 2>/dev/null || echo "") mask_host=$(config_get mask_host 2>/dev/null || echo "") # Sanitise port [[ "$port" =~ ^[0-9]+$ ]] || port=443 # Backup id / short id (last 6 of secret, or random if unknown) local backup_id short_id backup_id=$(generate_backup_id "$raw_secret") short_id="${backup_id##*-}" local timestamp timestamp=$(date +%Y%m%d_%H%M%S) local backup_name="gotelegram_backup_${timestamp}_${short_id}" local tmp_dir="/tmp/${backup_name}" mkdir -p "$tmp_dir" "$output_dir" log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')" # ── telemt ── if [ -f "$TELEMT_CONFIG" ]; then mkdir -p "$tmp_dir/telemt" cp "$TELEMT_CONFIG" "$tmp_dir/telemt/config.toml" fi # ── gotelegram ── mkdir -p "$tmp_dir/gotelegram" if [ -f "$GOTELEGRAM_CONFIG" ]; then cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram/config.json" fi if [ -f "$GOTELEGRAM_DIR/.language" ]; then cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/gotelegram/.language" fi # ── nginx ── if [ -f "$NGINX_SITE_CONF" ]; then mkdir -p "$tmp_dir/nginx" cp "$NGINX_SITE_CONF" "$tmp_dir/nginx/site.conf" fi # ── Let's Encrypt (full tree: live//*.pem + renewal/.conf) ── if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then mkdir -p "$tmp_dir/letsencrypt/live/$domain" # Follow symlinks — letsencrypt's live/ tree is symlinks into archive/ cp -L "/etc/letsencrypt/live/$domain/"*.pem "$tmp_dir/letsencrypt/live/$domain/" 2>/dev/null if [ -f "/etc/letsencrypt/renewal/${domain}.conf" ]; then mkdir -p "$tmp_dir/letsencrypt/renewal" cp "/etc/letsencrypt/renewal/${domain}.conf" "$tmp_dir/letsencrypt/renewal/" fi log_dim "$(_t_or backup_ssl_included 'SSL-сертификаты включены (+ chain + renewal)')" fi # ── Website template ── if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then mkdir -p "$tmp_dir/site" cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" 2>/dev/null log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')" fi # ── Telegram bot ── if [ -f "$BOT_DIR/.env" ]; then mkdir -p "$tmp_dir/bot" cp "$BOT_DIR/.env" "$tmp_dir/bot/.env" chmod 600 "$tmp_dir/bot/.env" 2>/dev/null log_dim "$(_t_or backup_bot_included 'Конфиг Telegram-бота включён')" fi # ── secrets.json ── local faketls_secret="" proxy_link="" bot_token="" if [ -n "$raw_secret" ] && [ "$mode" = "pro" ] && [ -n "$domain" ]; then faketls_secret="ee${raw_secret}$(_hex_encode "$domain")" fi if type generate_proxy_link &>/dev/null; then if [ "$mode" = "pro" ] && [ -n "$domain" ]; then proxy_link=$(generate_proxy_link "$domain" "$port" "$raw_secret" "$domain" 2>/dev/null || echo "") elif [ -n "$raw_secret" ]; then local ip ip=$(get_server_ip) proxy_link=$(generate_proxy_link "$ip" "$port" "$raw_secret" "$mask_host" 2>/dev/null || echo "") fi fi if [ -f "$BOT_DIR/.env" ]; then bot_token=$(grep -E '^BOT_TOKEN=' "$BOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-) bot_token="${bot_token%\"}" bot_token="${bot_token#\"}" fi cat > "$tmp_dir/secrets.json" << EOSEC { "version": "1", "raw_secret": "${raw_secret}", "faketls_secret": "${faketls_secret}", "proxy_link": "${proxy_link}", "bot_token": "${bot_token}", "exported_at": "$(date -Iseconds)" } EOSEC chmod 600 "$tmp_dir/secrets.json" # ── metadata.json v2.0 ── local ip fingerprint ip=$(get_server_ip) fingerprint=$(secret_fingerprint "$raw_secret") cat > "$tmp_dir/metadata.json" << EOMETA { "backup_version": "2.0", "backup_id": "${backup_id}", "gotelegram_version": "$GOTELEGRAM_VERSION", "created_at": "$(date -Iseconds)", "hostname": "$(hostname)", "ip": "$ip", "engine": "$engine", "mode": "$mode", "language": "$lang", "port": $port, "domain": "$domain", "template_id": "$tpl_id", "mask_host": "$mask_host", "secret_fingerprint_sha256": "$fingerprint", "has_secrets": true, "has_letsencrypt": $([ -d "$tmp_dir/letsencrypt" ] && echo true || echo false), "has_site": $([ -d "$tmp_dir/site" ] && echo true || echo false), "has_bot": $([ -d "$tmp_dir/bot" ] && echo true || echo false) } EOMETA # ── Archive ── local tar_file="/tmp/${backup_name}.tar.gz" if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then log_error "$(_t_or backup_archive_err 'Ошибка создания архива')" rm -rf "$tmp_dir"; rm -f "$tar_file" return 1 fi if [ ! -f "$tar_file" ]; then log_error "$(_t_or backup_archive_missing 'Архив не создан')" rm -rf "$tmp_dir" return 1 fi # ── Encrypt (optional) ── local final_file="" if [ -n "$password" ]; then final_file="${output_dir}/${backup_name}.tar.gz.enc" if ! openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null; then log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')" rm -f "$tar_file"; rm -rf "$tmp_dir" return 1 fi rm -f "$tar_file" log_success "$(_t_or backup_encrypted 'Бекап зашифрован (AES-256-CBC)')" else final_file="${output_dir}/${backup_name}.tar.gz" mv "$tar_file" "$final_file" fi # SHA-256 sidecar sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null # Cleanup rm -rf "$tmp_dir" local size size=$(du -h "$final_file" | cut -f1) echo "" >&2 echo -e " ${BOLD}${GREEN}✓ $(_t_or backup_created 'Бекап создан')${NC}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${backup_id}${NC}" >&2 echo -e " ${WHITE}$(_t_or backup_file_label 'Файл'):${NC} ${final_file}" >&2 echo -e " ${WHITE}$(_t_or backup_size_label 'Размер'):${NC} ${size}" >&2 if [ -n "$raw_secret" ]; then echo -e " ${WHITE}$(_t_or backup_key_label 'Ключ в бекапе (fingerprint)'):${NC} ${DIM}${fingerprint:0:32}...${NC}" >&2 fi echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 echo "" >&2 # Only the final file path on stdout (callers capture it) echo "$final_file" return 0 } # ── Восстановление из бекапа (auto-detect v1.1 vs v2.0) ───────────────────── restore_backup() { local backup_file="$1" local password="${2:-}" if [ ! -f "$backup_file" ]; then if type tf &>/dev/null; then log_error "$(tf backup_file_not_found_fmt "$backup_file")" else log_error "Файл не найден: $backup_file" fi return 1 fi local tmp_dir="/tmp/gotelegram_restore_$$" mkdir -p "$tmp_dir" # ── Decrypt if needed ── local tar_file="" if echo "$backup_file" | grep -q '\.enc$'; then if [ -z "$password" ]; then echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): " >&2 read -rs password echo "" >&2 fi tar_file="/tmp/gotelegram_restore_$$.tar.gz" if ! openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null; then log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')" rm -rf "$tmp_dir" "$tar_file" return 1 fi else tar_file="$backup_file" fi # ── Extract ── if ! tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null; then log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')" rm -rf "$tmp_dir" [ "$tar_file" != "$backup_file" ] && rm -f "$tar_file" return 1 fi # Find the single top-level dir inside the archive local backup_dir backup_dir=$(find "$tmp_dir" -maxdepth 1 -mindepth 1 -type d -name "gotelegram_backup_*" | head -1) [ -z "$backup_dir" ] && backup_dir="$tmp_dir" # ── Parse metadata.json ── local bk_version="1.1" bk_id="" bk_mode="" bk_domain="" bk_ip="" bk_lang="" bk_date="" if [ -f "$backup_dir/metadata.json" ]; then bk_version=$(jq -r '.backup_version // "1.1"' "$backup_dir/metadata.json") bk_id=$(jq -r '.backup_id // empty' "$backup_dir/metadata.json") bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json") bk_domain=$(jq -r '.domain // empty' "$backup_dir/metadata.json") bk_ip=$(jq -r '.ip // "-"' "$backup_dir/metadata.json") bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json") bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json") fi echo "" >&2 echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 if [ -n "$bk_id" ]; then echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${bk_id}${NC}" >&2 fi echo -e " ${WHITE}$(_t_or backup_format_label 'Формат'):${NC} UBF ${bk_version}" >&2 echo -e " ${WHITE}$(_t_or backup_mode_label 'Режим'):${NC} ${bk_mode}${bk_domain:+ | $bk_domain}" >&2 echo -e " ${WHITE}$(_t_or backup_lang_label 'Язык'):${NC} ${bk_lang} | IP: ${bk_ip}" >&2 echo -e " ${WHITE}$(_t_or backup_date_label 'Дата'):${NC} ${bk_date}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 echo "" >&2 if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then rm -rf "$tmp_dir" [ "$tar_file" != "$backup_file" ] && rm -f "$tar_file" return 0 fi stop_telemt 2>/dev/null systemctl stop nginx 2>/dev/null # ── Detect layout ── # v2.0 paths: telemt/config.toml, gotelegram/config.json, nginx/site.conf, letsencrypt/live// # v1.1 paths: config.toml, gotelegram.json, nginx.conf, certs/ local src_telemt src_gt src_lang src_nginx src_le_live src_le_renewal src_site src_bot if [ "$bk_version" = "2.0" ] || [ -d "$backup_dir/telemt" ]; then src_telemt="$backup_dir/telemt/config.toml" src_gt="$backup_dir/gotelegram/config.json" src_lang="$backup_dir/gotelegram/.language" src_nginx="$backup_dir/nginx/site.conf" src_le_live=$(find "$backup_dir/letsencrypt/live" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | head -1) src_le_renewal="$backup_dir/letsencrypt/renewal" src_site="$backup_dir/site" src_bot="$backup_dir/bot/.env" else src_telemt="$backup_dir/config.toml" src_gt="$backup_dir/gotelegram.json" src_lang="$backup_dir/.language" src_nginx="$backup_dir/nginx.conf" src_le_live="$backup_dir/certs" # v1.1 dumps certs flat src_le_renewal="" src_site="$backup_dir/site" src_bot="" # v1.1 never backed up bot fi # ── telemt config ── if [ -f "$src_telemt" ]; then mkdir -p /etc/telemt cp "$src_telemt" "$TELEMT_CONFIG" chmod 600 "$TELEMT_CONFIG" log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')" fi # ── GoTelegram config ── if [ -f "$src_gt" ]; then mkdir -p "$GOTELEGRAM_DIR" cp "$src_gt" "$GOTELEGRAM_CONFIG" log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')" fi # ── Language ── if [ -f "$src_lang" ]; then mkdir -p "$GOTELEGRAM_DIR" cp "$src_lang" "$GOTELEGRAM_DIR/.language" log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')" fi # ── nginx ── if [ -f "$src_nginx" ]; then mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled cp "$src_nginx" "$NGINX_SITE_CONF" ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK" log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')" fi # ── Let's Encrypt (v2.0: full tree; v1.1: just flat certs/) ── if [ -n "$bk_domain" ] && [ -d "$src_le_live" ]; then local live_dir="/etc/letsencrypt/live/$bk_domain" mkdir -p "$live_dir" cp "$src_le_live/"*.pem "$live_dir/" 2>/dev/null if [ -n "$src_le_renewal" ] && [ -f "$src_le_renewal/${bk_domain}.conf" ]; then mkdir -p /etc/letsencrypt/renewal cp "$src_le_renewal/${bk_domain}.conf" "/etc/letsencrypt/renewal/" fi log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')" fi # ── Site ── if [ -d "$src_site" ] && [ -f "$src_site/index.html" ]; then mkdir -p "$WEBSITE_ROOT" cp -r "$src_site"/* "$WEBSITE_ROOT/" chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')" fi # ── Bot .env (v2.0 only) ── if [ -n "$src_bot" ] && [ -f "$src_bot" ]; then mkdir -p "$BOT_DIR" cp "$src_bot" "$BOT_DIR/.env" chmod 600 "$BOT_DIR/.env" log_success "$(_t_or backup_restored_bot 'Конфиг Telegram-бота восстановлен')" fi # ── Start services ── if type is_telemt_installed &>/dev/null && is_telemt_installed; then start_telemt fi systemctl start nginx 2>/dev/null # ── Cleanup ── rm -rf "$tmp_dir" [ "$tar_file" != "$backup_file" ] && rm -f "$tar_file" log_success "$(_t_or backup_restore_done 'Восстановление завершено!')" # ── Auto-migrate v1.1 → v2.0 ── if [ "$bk_version" != "2.0" ]; then log_info "$(_t_or backup_automigrate 'Конвертирую старый бекап в UBF v2.0...')" create_backup "" >/dev/null 2>&1 && \ log_success "$(_t_or backup_migrated 'Свежий UBF v2.0 бекап сохранён в $BACKUP_DIR')" fi type show_proxy_info &>/dev/null && show_proxy_info return 0 } # ── Список бекапов ─────────────────────────────────────────────────────────── list_backups() { if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]; then log_info "$(_t_or backup_none 'Бекапов нет')" return 1 fi echo "" >&2 echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2 local i=1 for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do [ -f "$f" ] || continue [[ "$f" == *.sha256 ]] && continue local size name date_str id_tail encrypted="" size=$(du -h "$f" | cut -f1) name=$(basename "$f") date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1) id_tail=$(echo "$name" | grep -oE '_[0-9a-f]{6}\.tar' | head -1 | tr -d '_.tar') [[ "$f" == *.enc ]] && encrypted=" 🔒" local id_display="" [ -n "$id_tail" ] && id_display=" ${DIM}[...${id_tail}]${NC}" echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}${id_display}" >&2 ((i++)) done echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2 } # ── Очистка старых бекапов ─────────────────────────────────────────────────── cleanup_old_backups() { local keep="${1:-5}" local count count=$(find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l) if [ "$count" -gt "$keep" ]; then local to_delete=$((count - keep)) find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do rm -f "$f" "${f}.sha256" done if type tf &>/dev/null; then log_dim "$(tf backup_cleanup_fmt "$to_delete" "$keep")" else log_dim "Удалено $to_delete старых бекапов (оставлено $keep)" fi fi } # ── Интерактивный бекап ────────────────────────────────────────────────────── interactive_backup() { echo "" echo -e " ${BOLD}${WHITE}💾 $(_t_or backup_create_title 'Создание бекапа')${NC}" echo -ne " $(_t_or backup_encrypt_prompt 'Зашифровать бекап паролем?') [Y/n]: " read -r use_pass local password="" if [[ ! "$use_pass" =~ ^[Nn] ]]; then echo -ne " $(_t_or backup_enter_pass 'Введите пароль'): " read -rs password echo "" echo -ne " $(_t_or backup_repeat_pass 'Повторите пароль'): " read -rs password2 echo "" if [ "$password" != "$password2" ]; then log_error "$(_t_or backup_pass_mismatch 'Пароли не совпадают')" return 1 fi if [ ${#password} -lt 6 ]; then log_error "$(_t_or backup_pass_short 'Пароль слишком короткий (минимум 6 символов)')" return 1 fi fi create_backup "$password" >/dev/null cleanup_old_backups } # ── Интерактивное восстановление ───────────────────────────────────────────── interactive_restore() { list_backups || return 1 echo -ne " $(_t_or backup_pick_prompt 'Номер бекапа (или путь к файлу)'): " read -r choice local backup_file="" if [[ "$choice" =~ ^[0-9]+$ ]]; then local i=1 for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do [ -f "$f" ] || continue [[ "$f" == *.sha256 ]] && continue if [ "$i" -eq "$choice" ]; then backup_file="$f" break fi ((i++)) done elif [ -f "$choice" ]; then backup_file="$choice" fi if [ -z "$backup_file" ]; then log_error "$(_t_or backup_not_found 'Бекап не найден')" return 1 fi restore_backup "$backup_file" } # ── Manual secret recovery (v2.4.9) ────────────────────────────────────────── # Parser accepts any of the 3 formats and emits key=value lines on stdout: # tg://proxy?server=X&port=Y&secret=Z → raw_secret, server, port, domain (if ee-prefix) # ee<32hex> → raw_secret, domain # <32hex> → raw_secret only # Returns 0 on success, 1 on parse failure. parse_manual_secret() { local input="$1" input=$(echo "$input" | tr -d ' \t\n\r') [ -z "$input" ] && return 1 local raw_secret="" domain="" server="" port="" if echo "$input" | grep -q '^tg://proxy?'; then local qs="${input#tg://proxy?}" local kv k v local -a kvs IFS='&' read -ra kvs <<< "$qs" for kv in "${kvs[@]}"; do k="${kv%%=*}" v="${kv#*=}" case "$k" in server) server="$v" ;; port) port="$v" ;; secret) raw_secret="$v" ;; esac done [ -z "$raw_secret" ] && return 1 # Strip hex-escapes that Telegram sometimes URL-encodes raw_secret=$(echo "$raw_secret" | tr -d '%') fi # After pulling from URL (if any), raw_secret might still be ee-prefixed. # Otherwise, try the raw_secret as the whole input. local candidate="${raw_secret:-$input}" if [[ "$candidate" =~ ^[eE][eE][0-9a-fA-F]{32}[0-9a-fA-F]*$ ]]; then raw_secret="${candidate:2:32}" local hex_domain="${candidate:34}" if [ -n "$hex_domain" ]; then local decoded decoded=$(echo "$hex_domain" | xxd -r -p 2>/dev/null) # Validate decoded looks like a domain if echo "$decoded" | grep -qE '^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then domain="$decoded" fi fi elif [[ "$candidate" =~ ^[0-9a-fA-F]{32}$ ]]; then raw_secret="$candidate" else return 1 fi [[ "$raw_secret" =~ ^[0-9a-fA-F]{32}$ ]] || return 1 raw_secret=$(echo "$raw_secret" | tr 'A-F' 'a-f') echo "raw_secret=$raw_secret" [ -n "$domain" ] && echo "domain=$domain" [ -n "$server" ] && echo "server=$server" [ -n "$port" ] && echo "port=$port" return 0 } # ── Interactive: user types their old key, we parse it ───────────────────── manual_secret_input() { echo "" >&2 echo -e " ${BOLD}${WHITE}🔑 $(_t_or manual_secret_title 'Ввод существующего ключа')${NC}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 echo -e " ${DIM}$(_t_or manual_secret_help1 'Поддерживаются форматы:')${NC}" >&2 echo -e " ${DIM} • tg://proxy?server=...&port=...&secret=...${NC}" >&2 echo -e " ${DIM} • ee<32hex> (fake-TLS)${NC}" >&2 echo -e " ${DIM} • 32hex (только raw secret)${NC}" >&2 echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2 echo -ne " ${WHITE}$(_t_or manual_secret_prompt 'Вставьте ключ'):${NC} " >&2 read -r user_input if [ -z "$user_input" ]; then log_error "$(_t_or manual_secret_empty 'Ключ не введён')" return 1 fi local parsed if ! parsed=$(parse_manual_secret "$user_input"); then log_error "$(_t_or manual_secret_bad 'Не удалось распознать формат ключа')" return 1 fi local p_raw="" p_domain="" p_server="" p_port="" while IFS='=' read -r k v; do case "$k" in raw_secret) p_raw="$v" ;; domain) p_domain="$v" ;; server) p_server="$v" ;; port) p_port="$v" ;; esac done <<< "$parsed" echo "" >&2 echo -e " ${GREEN}✓ $(_t_or manual_secret_parsed 'Ключ распознан')${NC}" >&2 echo -e " ${WHITE}raw_secret:${NC} ${DIM}${p_raw:0:8}...${p_raw: -4}${NC}" >&2 [ -n "$p_domain" ] && echo -e " ${WHITE}domain:${NC} ${CYAN}${p_domain}${NC}" >&2 [ -n "$p_server" ] && echo -e " ${WHITE}server:${NC} ${CYAN}${p_server}${NC}" >&2 [ -n "$p_port" ] && echo -e " ${WHITE}port:${NC} ${CYAN}${p_port}${NC}" >&2 echo "" >&2 # Export for the subsequent install flow to pick up export GOTELEGRAM_EXISTING_SECRET="$p_raw" export GOTELEGRAM_EXISTING_DOMAIN="$p_domain" export GOTELEGRAM_EXISTING_PORT="$p_port" return 0 }