mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 14:36:05 +00:00
v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
* validates HTTPS URLs, rejects shell metachars
* 100MB size guard, 90s clone timeout
* auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
This commit is contained in:
108
lib/backup.sh
108
lib/backup.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Бекап и восстановление конфигурации
|
||||
# GoTelegram v2.4 — backup and restore (i18n-aware)
|
||||
|
||||
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
||||
create_backup() {
|
||||
@@ -13,7 +13,7 @@ create_backup() {
|
||||
mkdir -p "$tmp_dir" "$output_dir"
|
||||
|
||||
# Собираем файлы
|
||||
log_info "Собираю конфигурацию..."
|
||||
log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')"
|
||||
|
||||
# telemt конфиг
|
||||
if [ -f "$TELEMT_CONFIG" ]; then
|
||||
@@ -25,6 +25,11 @@ create_backup() {
|
||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
|
||||
fi
|
||||
|
||||
# Language marker (i18n)
|
||||
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
|
||||
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/.language"
|
||||
fi
|
||||
|
||||
# nginx конфиг (stealth mode)
|
||||
if [ -f "$NGINX_SITE_CONF" ]; then
|
||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
|
||||
@@ -44,40 +49,46 @@ create_backup() {
|
||||
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
|
||||
mkdir -p "$tmp_dir/site"
|
||||
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/"
|
||||
log_dim "Шаблон сайта включён"
|
||||
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
|
||||
fi
|
||||
|
||||
# Метаданные
|
||||
local ip mode engine
|
||||
local ip mode engine lang port domain
|
||||
ip=$(get_server_ip)
|
||||
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
|
||||
port=$(config_get port 2>/dev/null || echo "443")
|
||||
# Ensure port is numeric; fall back to 443 if garbage
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
|
||||
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||
{
|
||||
"backup_version": "1.0",
|
||||
"backup_version": "1.1",
|
||||
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||
"created_at": "$(date -Iseconds)",
|
||||
"hostname": "$(hostname)",
|
||||
"ip": "$ip",
|
||||
"engine": "$engine",
|
||||
"mode": "$mode",
|
||||
"port": $(config_get port 2>/dev/null || echo "443"),
|
||||
"domain": "$(config_get domain 2>/dev/null)"
|
||||
"language": "$lang",
|
||||
"port": $port,
|
||||
"domain": "$domain"
|
||||
}
|
||||
EOMETA
|
||||
|
||||
# Архивируем
|
||||
local tar_file="/tmp/${backup_name}.tar.gz"
|
||||
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
|
||||
log_error "Ошибка создания архива"
|
||||
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 "Архив не создан"
|
||||
log_error "$(_t_or backup_archive_missing 'Архив не создан')"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
@@ -88,13 +99,13 @@ EOMETA
|
||||
final_file="${output_dir}/${backup_name}.tar.gz.enc"
|
||||
openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Ошибка шифрования"
|
||||
log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')"
|
||||
rm -f "$tar_file"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$tar_file"
|
||||
log_success "Бекап зашифрован (AES-256-CBC)"
|
||||
log_success "$(_t_or backup_encrypted 'Бекап зашифрован (AES-256-CBC)')"
|
||||
else
|
||||
final_file="${output_dir}/${backup_name}.tar.gz"
|
||||
mv "$tar_file" "$final_file"
|
||||
@@ -108,7 +119,11 @@ EOMETA
|
||||
|
||||
local size
|
||||
size=$(du -h "$final_file" | cut -f1)
|
||||
log_success "Бекап создан: $final_file ($size)"
|
||||
if type tf &>/dev/null; then
|
||||
log_success "$(tf backup_created_fmt "$final_file" "$size")"
|
||||
else
|
||||
log_success "Бекап создан: $final_file ($size)"
|
||||
fi
|
||||
echo "$final_file"
|
||||
return 0
|
||||
}
|
||||
@@ -119,7 +134,11 @@ restore_backup() {
|
||||
local password="$2"
|
||||
|
||||
if [ ! -f "$backup_file" ]; then
|
||||
log_error "Файл не найден: $backup_file"
|
||||
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
|
||||
|
||||
@@ -130,14 +149,14 @@ restore_backup() {
|
||||
local tar_file=""
|
||||
if echo "$backup_file" | grep -q '\.enc$'; then
|
||||
if [ -z "$password" ]; then
|
||||
echo -ne " Введите пароль от бекапа: "
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): "
|
||||
read -rs password
|
||||
echo ""
|
||||
fi
|
||||
tar_file="/tmp/gotelegram_restore_$$.tar.gz"
|
||||
openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Неверный пароль или повреждённый файл"
|
||||
log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')"
|
||||
rm -rf "$tmp_dir" "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
@@ -148,7 +167,7 @@ restore_backup() {
|
||||
# Распаковываем
|
||||
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Ошибка распаковки архива"
|
||||
log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
@@ -160,18 +179,20 @@ restore_backup() {
|
||||
|
||||
# Проверяем метаданные
|
||||
if [ -f "$backup_dir/metadata.json" ]; then
|
||||
local bk_version bk_mode bk_ip
|
||||
local bk_version bk_mode bk_ip bk_lang bk_date
|
||||
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json")
|
||||
bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json")
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 Бекап:${NC}"
|
||||
echo -e " Версия: $bk_version | Режим: $bk_mode | IP: $bk_ip"
|
||||
echo -e " Дата: $(jq -r '.created_at' "$backup_dir/metadata.json")"
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}"
|
||||
echo -e " $(_t_or backup_version_label 'Версия'): $bk_version | $(_t_or backup_mode_label 'Режим'): $bk_mode | IP: $bk_ip | $(_t_or backup_lang_label 'Язык'): $bk_lang"
|
||||
echo -e " $(_t_or backup_date_label 'Дата'): $bk_date"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if ! confirm "Восстановить конфигурацию? Текущие настройки будут перезаписаны."; then
|
||||
if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 0
|
||||
fi
|
||||
@@ -185,14 +206,21 @@ restore_backup() {
|
||||
mkdir -p /etc/telemt
|
||||
cp "$backup_dir/config.toml" "$TELEMT_CONFIG"
|
||||
chmod 600 "$TELEMT_CONFIG"
|
||||
log_success "telemt конфиг восстановлен"
|
||||
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем GoTelegram конфиг
|
||||
if [ -f "$backup_dir/gotelegram.json" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
|
||||
log_success "GoTelegram конфиг восстановлен"
|
||||
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем language marker (i18n)
|
||||
if [ -f "$backup_dir/.language" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$backup_dir/.language" "$GOTELEGRAM_DIR/.language"
|
||||
log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем nginx конфиг
|
||||
@@ -200,7 +228,7 @@ restore_backup() {
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF"
|
||||
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||
log_success "nginx конфиг восстановлен"
|
||||
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем SSL
|
||||
@@ -211,7 +239,7 @@ restore_backup() {
|
||||
local cert_dir="/etc/letsencrypt/live/$domain"
|
||||
mkdir -p "$cert_dir"
|
||||
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null
|
||||
log_success "SSL сертификаты восстановлены"
|
||||
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -220,7 +248,7 @@ restore_backup() {
|
||||
mkdir -p "$WEBSITE_ROOT"
|
||||
cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/"
|
||||
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
|
||||
log_success "Шаблон сайта восстановлен"
|
||||
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
|
||||
fi
|
||||
|
||||
# Запускаем сервисы
|
||||
@@ -233,7 +261,7 @@ restore_backup() {
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
|
||||
log_success "Восстановление завершено!"
|
||||
log_success "$(_t_or backup_restore_done 'Восстановление завершено!')"
|
||||
show_proxy_info
|
||||
return 0
|
||||
}
|
||||
@@ -241,12 +269,12 @@ restore_backup() {
|
||||
# ── Список бекапов ───────────────────────────────────────────────────────────
|
||||
list_backups() {
|
||||
if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]; then
|
||||
log_info "Бекапов нет"
|
||||
log_info "$(_t_or backup_none 'Бекапов нет')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 Доступные бекапы:${NC}"
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
|
||||
local i=1
|
||||
@@ -276,31 +304,35 @@ cleanup_old_backups() {
|
||||
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
|
||||
log_dim "Удалено $to_delete старых бекапов (оставлено $keep)"
|
||||
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}💾 Создание бекапа${NC}"
|
||||
echo -ne " Зашифровать бекап паролем? [Y/n]: "
|
||||
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 " Введите пароль: "
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль'): "
|
||||
read -rs password
|
||||
echo ""
|
||||
echo -ne " Повторите пароль: "
|
||||
echo -ne " $(_t_or backup_repeat_pass 'Повторите пароль'): "
|
||||
read -rs password2
|
||||
echo ""
|
||||
if [ "$password" != "$password2" ]; then
|
||||
log_error "Пароли не совпадают"
|
||||
log_error "$(_t_or backup_pass_mismatch 'Пароли не совпадают')"
|
||||
return 1
|
||||
fi
|
||||
if [ ${#password} -lt 6 ]; then
|
||||
log_error "Пароль слишком короткий (минимум 6 символов)"
|
||||
log_error "$(_t_or backup_pass_short 'Пароль слишком короткий (минимум 6 символов)')"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
@@ -313,7 +345,7 @@ interactive_backup() {
|
||||
interactive_restore() {
|
||||
list_backups || return 1
|
||||
|
||||
echo -ne " Номер бекапа (или путь к файлу): "
|
||||
echo -ne " $(_t_or backup_pick_prompt 'Номер бекапа (или путь к файлу)'): "
|
||||
read -r choice
|
||||
|
||||
local backup_file=""
|
||||
@@ -333,7 +365,7 @@ interactive_restore() {
|
||||
fi
|
||||
|
||||
if [ -z "$backup_file" ]; then
|
||||
log_error "Бекап не найден"
|
||||
log_error "$(_t_or backup_not_found 'Бекап не найден')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
160
lib/common.sh
160
lib/common.sh
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.3 — Общие утилиты
|
||||
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
||||
# GoTelegram v2.4 — common utilities
|
||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||
|
||||
# ── Версия ────────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.3.1"
|
||||
# ── Version ───────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.4.0"
|
||||
GOTELEGRAM_NAME="GoTelegram"
|
||||
|
||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||
@@ -49,10 +49,12 @@ log_to_file() {
|
||||
echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Спиннер ──────────────────────────────────────────────────────────────────
|
||||
# ── Spinner ──────────────────────────────────────────────────────────────────
|
||||
_spin_pid=""
|
||||
spinner_start() {
|
||||
local msg="${1:-Подождите...}"
|
||||
local default_msg
|
||||
default_msg=$(type t &>/dev/null && t wait || echo "Please wait...")
|
||||
local msg="${1:-$default_msg}"
|
||||
(
|
||||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||
local i=0
|
||||
@@ -95,7 +97,9 @@ run_with_spinner() {
|
||||
if [ $rc -eq 0 ]; then
|
||||
log_success "$label"
|
||||
else
|
||||
log_error "$label ${RED}(ошибка, код: $rc)${NC}"
|
||||
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
|
||||
@@ -104,35 +108,45 @@ run_with_spinner() {
|
||||
return $rc
|
||||
}
|
||||
|
||||
# ── Баннер ───────────────────────────────────────────────────────────────────
|
||||
# ── Banner ───────────────────────────────────────────────────────────────────
|
||||
show_banner() {
|
||||
local line
|
||||
line=$(printf '━%.0s' $(seq 1 60))
|
||||
echo ""
|
||||
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}${WHITE}🚀 GoTelegram v${GOTELEGRAM_VERSION}${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${DIM}MTProxy на ядре telemt (Rust + Tokio)${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
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}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${BOLD}Благодарности / Credits${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}╟──────────────────────────────────────────────────────────╢${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}telemt${NC} — MTProxy engine (Rust) ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}github.com/telemt/telemt${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}HTML5 UP${NC} — адаптивные HTML/CSS шаблоны ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}html5up.net • CC BY 3.0 • @ajlkn${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}learning-zone${NC} — 150+ HTML5 шаблонов ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}github.com/learning-zone/website-templates${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}Start Bootstrap${NC} — MIT лицензия ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}startbootstrap.com${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
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 ""
|
||||
}
|
||||
|
||||
@@ -164,31 +178,41 @@ get_server_ip() {
|
||||
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 "Запустите скрипт с sudo / от root"
|
||||
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 "Не удалось определить ОС. Требуется Linux."
|
||||
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 содержит подозрительные строки, пропускаем"
|
||||
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 "ОС: $PRETTY_NAME"
|
||||
log_dim "OS: $PRETTY_NAME"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
log_warning "ОС $ID может быть несовместима. Поддерживаются: Ubuntu, Debian, CentOS, Rocky."
|
||||
log_warning "OS $ID may be incompatible. Supported: Ubuntu, Debian, CentOS, Rocky."
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
@@ -219,7 +243,7 @@ install_pkg() {
|
||||
apt) apt-get install -y -qq "$pkg" ;;
|
||||
dnf) dnf install -y -q "$pkg" ;;
|
||||
yum) yum install -y -q "$pkg" ;;
|
||||
*) log_error "Неизвестный пакетный менеджер"; return 1 ;;
|
||||
*) log_error "$(_t_or err_bad_pkg_mgr 'Unknown package manager')"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -231,7 +255,11 @@ ensure_deps() {
|
||||
fi
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
log_step "Установка зависимостей: ${missing[*]}"
|
||||
if type tf &>/dev/null; then
|
||||
log_step "$(tf deps_installing "${missing[*]}")"
|
||||
else
|
||||
log_step "Installing dependencies: ${missing[*]}"
|
||||
fi
|
||||
case "$(get_pkg_manager)" in
|
||||
apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;;
|
||||
dnf) dnf install -y -q "${missing[@]}" ;;
|
||||
@@ -257,7 +285,11 @@ check_disk_space() {
|
||||
local avail_mb
|
||||
avail_mb=$(df -m / | awk 'NR==2 {print $4}')
|
||||
if [ "$avail_mb" -lt "$min_mb" ]; then
|
||||
log_error "Мало места на диске: ${avail_mb}MB (нужно ${min_mb}MB+)"
|
||||
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
|
||||
@@ -266,6 +298,8 @@ check_disk_space() {
|
||||
# ── Конфигурация 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",
|
||||
@@ -276,6 +310,7 @@ save_gotelegram_config() {
|
||||
"mask_host": "${5:-google.com}",
|
||||
"domain": "${6:-}",
|
||||
"template_id": "${7:-}",
|
||||
"language": "${cur_lang}",
|
||||
"installed_at": "$(date -Iseconds)",
|
||||
"updated_at": "$(date -Iseconds)"
|
||||
}
|
||||
@@ -295,13 +330,11 @@ load_gotelegram_config() {
|
||||
config_get() {
|
||||
local key="$1"
|
||||
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
log_dim "Конфиг не найден: $GOTELEGRAM_CONFIG" >&2
|
||||
return 2 # file missing
|
||||
fi
|
||||
local val
|
||||
val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
log_dim "Ошибка чтения JSON: $GOTELEGRAM_CONFIG" >&2
|
||||
return 3 # invalid JSON
|
||||
fi
|
||||
if [ -z "$val" ]; then
|
||||
@@ -361,7 +394,7 @@ get_v1_config() {
|
||||
}
|
||||
|
||||
migrate_v1_to_v2() {
|
||||
log_step "Миграция с v1 (mtg) на v2 (telemt)"
|
||||
log_step "$(_t_or v1_migration_step 'Migrating from v1 (mtg) to v2 (telemt)')"
|
||||
|
||||
local v1_config
|
||||
v1_config=$(get_v1_config)
|
||||
@@ -371,44 +404,59 @@ migrate_v1_to_v2() {
|
||||
old_secret=$(echo "$v1_config" | jq -r '.secret // empty')
|
||||
|
||||
if [ -z "$old_secret" ]; then
|
||||
log_warning "Не удалось извлечь secret из v1. Будет создан новый."
|
||||
log_warning "Failed to extract secret from v1. A new one will be generated."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${WHITE}Найдена установка v1 (mtg):${NC}"
|
||||
echo -e " Порт: ${CYAN}${old_port}${NC}"
|
||||
echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}"
|
||||
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}Внимание:${NC} секрет mtg НЕ совместим с telemt напрямую."
|
||||
echo -e " Клиентам потребуется новая ссылка."
|
||||
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 " Остановить v1 контейнер и перейти на v2? [Y/n]: "
|
||||
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 "Миграция отменена. v1 оставлен без изменений."
|
||||
log_info "$(_t_or v1_migration_cancelled 'Migration cancelled. v1 left intact.')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Останавливаем v1
|
||||
log_info "Остановка v1 контейнера..."
|
||||
# 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
|
||||
|
||||
# Бекапим v1 конфиг
|
||||
# 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
|
||||
log_success "Конфиг v1 сохранён в $GOTELEGRAM_DIR/v1_backup_proxy.json"
|
||||
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
|
||||
|
||||
log_success "v1 остановлен. Порт $old_port освобождён."
|
||||
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 msg="${1:-Продолжить?}"
|
||||
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] ]]
|
||||
@@ -429,7 +477,7 @@ select_option() {
|
||||
((i++))
|
||||
done
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||
echo -ne " ${WHITE}Выбор:${NC} " >&2
|
||||
echo -ne " ${WHITE}$(_t_or choose 'Choose'):${NC} " >&2
|
||||
read -r choice
|
||||
echo "$choice"
|
||||
}
|
||||
|
||||
140
lib/i18n.sh
Executable file
140
lib/i18n.sh
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — i18n engine
|
||||
# Internationalization support: EN (English) / RU (Русский)
|
||||
#
|
||||
# Usage:
|
||||
# source lib/i18n.sh
|
||||
# load_language "ru" # or "en"
|
||||
# echo "$(t menu_install)" # translated string
|
||||
# printf "$(t greeting)\n" "$name" # with format args
|
||||
|
||||
# ── Global i18n state ──
|
||||
declare -gA I18N
|
||||
LANG_CODE="${LANG_CODE:-en}"
|
||||
LANG_FILE=""
|
||||
|
||||
# ── Load a language ──
|
||||
# Sources lib/lang/${lang}.sh into the I18N associative array.
|
||||
# Falls back to English if requested language file is missing.
|
||||
load_language() {
|
||||
local lang="${1:-en}"
|
||||
# Sanitize: only allow [a-z]{2} codes
|
||||
if ! [[ "$lang" =~ ^[a-z]{2}$ ]]; then
|
||||
lang="en"
|
||||
fi
|
||||
|
||||
local lang_dir
|
||||
lang_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/lang"
|
||||
local lang_file="${lang_dir}/${lang}.sh"
|
||||
|
||||
if [ ! -f "$lang_file" ]; then
|
||||
lang_file="${lang_dir}/en.sh"
|
||||
lang="en"
|
||||
fi
|
||||
|
||||
if [ -f "$lang_file" ]; then
|
||||
# Clear previous keys then source the new language
|
||||
I18N=()
|
||||
# shellcheck disable=SC1090
|
||||
source "$lang_file"
|
||||
LANG_CODE="$lang"
|
||||
LANG_FILE="$lang_file"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Translate: fetch value by key ──
|
||||
# t <key> → echoes translation (or key if missing)
|
||||
t() {
|
||||
local key="$1"
|
||||
local val="${I18N[$key]:-}"
|
||||
if [ -z "$val" ]; then
|
||||
# Fallback to key name so missing translations are visible
|
||||
echo "$key"
|
||||
else
|
||||
echo "$val"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Translate + printf-style formatting ──
|
||||
# tf <key> <arg1> <arg2> ...
|
||||
tf() {
|
||||
local key="$1"
|
||||
shift
|
||||
local fmt="${I18N[$key]:-$key}"
|
||||
# shellcheck disable=SC2059
|
||||
printf "$fmt" "$@"
|
||||
}
|
||||
|
||||
# ── Get current language code ──
|
||||
get_language() {
|
||||
echo "$LANG_CODE"
|
||||
}
|
||||
|
||||
# ── Detect saved language from config.json, default en ──
|
||||
detect_language() {
|
||||
local cfg="${GOTELEGRAM_CONFIG:-/opt/gotelegram/config.json}"
|
||||
local lang=""
|
||||
if [ -f "$cfg" ] && command -v jq >/dev/null 2>&1; then
|
||||
lang=$(jq -r '.language // empty' "$cfg" 2>/dev/null)
|
||||
fi
|
||||
# Also check marker file (language set before config.json exists)
|
||||
if [ -z "$lang" ]; then
|
||||
local marker="${GOTELEGRAM_DIR:-/opt/gotelegram}/.language"
|
||||
if [ -f "$marker" ]; then
|
||||
lang=$(head -c 2 "$marker" 2>/dev/null | tr -d '[:space:]')
|
||||
fi
|
||||
fi
|
||||
# Sanitize
|
||||
if ! [[ "$lang" =~ ^(en|ru)$ ]]; then
|
||||
lang="en"
|
||||
fi
|
||||
echo "$lang"
|
||||
}
|
||||
|
||||
# ── Persist selected language ──
|
||||
# Saves to config.json if present, otherwise to marker file
|
||||
save_language() {
|
||||
local lang="$1"
|
||||
if ! [[ "$lang" =~ ^(en|ru)$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "${GOTELEGRAM_DIR:-/opt/gotelegram}" 2>/dev/null
|
||||
# Always write marker for early-access (before config.json exists)
|
||||
echo "$lang" > "${GOTELEGRAM_DIR:-/opt/gotelegram}/.language" 2>/dev/null
|
||||
|
||||
local cfg="${GOTELEGRAM_CONFIG:-/opt/gotelegram/config.json}"
|
||||
if [ -f "$cfg" ] && command -v jq >/dev/null 2>&1; then
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
if jq --arg lang "$lang" '. + {language: $lang}' "$cfg" > "$tmp" 2>/dev/null; then
|
||||
mv "$tmp" "$cfg"
|
||||
chmod 600 "$cfg"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── First-run interactive language picker ──
|
||||
# Shows a minimal, language-agnostic picker (keeps it culture-neutral).
|
||||
# Returns the chosen code via echo.
|
||||
pick_language_interactive() {
|
||||
echo "" >&2
|
||||
echo " ┌──────────────────────────────────────────┐" >&2
|
||||
echo " │ Select language / Выберите язык │" >&2
|
||||
echo " ├──────────────────────────────────────────┤" >&2
|
||||
echo " │ 1) English │" >&2
|
||||
echo " │ 2) Русский │" >&2
|
||||
echo " └──────────────────────────────────────────┘" >&2
|
||||
echo -n " > " >&2
|
||||
local ch
|
||||
read -r ch
|
||||
case "$ch" in
|
||||
1|en|EN|english|English) echo "en" ;;
|
||||
2|ru|RU|russian|Russian|русский) echo "ru" ;;
|
||||
*) echo "en" ;;
|
||||
esac
|
||||
}
|
||||
375
lib/lang/en.sh
Executable file
375
lib/lang/en.sh
Executable file
@@ -0,0 +1,375 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — English translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
I18N[yes]="Yes"
|
||||
I18N[no]="No"
|
||||
I18N[ok]="OK"
|
||||
I18N[cancel]="Cancel"
|
||||
I18N[back]="« Back"
|
||||
I18N[exit]="Exit"
|
||||
I18N[skip]="Skip"
|
||||
I18N[choose]="Choose"
|
||||
I18N[press_enter]="Press Enter..."
|
||||
I18N[press_enter_to_return]="Press Enter to return to menu..."
|
||||
I18N[invalid_choice]="Invalid choice"
|
||||
I18N[running]="running"
|
||||
I18N[stopped]="stopped"
|
||||
I18N[not_installed]="not installed"
|
||||
I18N[unknown]="unknown"
|
||||
I18N[error]="Error"
|
||||
I18N[warning]="Warning"
|
||||
I18N[info]="Info"
|
||||
I18N[success]="Done"
|
||||
I18N[wait]="Please wait..."
|
||||
|
||||
# ── Banner ──────────────────────────────────────────────────────────────
|
||||
I18N[banner_title]="GoTelegram v%s"
|
||||
I18N[banner_subtitle]="MTProxy powered by telemt (Rust + Tokio)"
|
||||
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||
I18N[credits_title]="Credits / Thanks"
|
||||
|
||||
# ── Main menu (dashboard) ───────────────────────────────────────────────
|
||||
I18N[dashboard_title]="Control panel"
|
||||
I18N[svc_proxy]="Proxy"
|
||||
I18N[svc_nginx]="nginx"
|
||||
I18N[svc_site]="Site"
|
||||
I18N[svc_ssl]="SSL"
|
||||
I18N[svc_bot]="Bot"
|
||||
I18N[ssl_until]="until %s"
|
||||
I18N[net_ip]="IP:"
|
||||
I18N[net_port]="Port:"
|
||||
I18N[net_mode]="Mode:"
|
||||
I18N[net_domain]="Domain:"
|
||||
I18N[connection_link]="Telegram connection link:"
|
||||
I18N[proxy_not_configured]="Proxy is not configured. Select option 1."
|
||||
I18N[menu_proxy]="Proxy ▸"
|
||||
I18N[menu_stats]="Statistics ▸"
|
||||
I18N[menu_manage]="Management ▸"
|
||||
I18N[menu_telegram_bot]="Telegram bot ▸"
|
||||
I18N[menu_about]="About ▸"
|
||||
I18N[auto_refresh_30s]="Refresh in 30 sec"
|
||||
|
||||
# ── Submenu: Proxy ──────────────────────────────────────────────────────
|
||||
I18N[submenu_proxy_title]="🚀 PROXY"
|
||||
I18N[proxy_install_update]="Install / Update"
|
||||
I18N[proxy_status_detail]="Detailed status"
|
||||
I18N[proxy_copy_link]="Copy link"
|
||||
I18N[proxy_share]="Share key"
|
||||
I18N[proxy_restart]="Restart"
|
||||
I18N[proxy_logs]="Logs"
|
||||
I18N[proxy_change_mode]="Change mode / template"
|
||||
|
||||
# ── Submenu: Manage ─────────────────────────────────────────────────────
|
||||
I18N[submenu_manage_title]="⚙️ MANAGEMENT"
|
||||
I18N[manage_backup]="Backup"
|
||||
I18N[manage_restore]="Restore"
|
||||
I18N[manage_update_telemt]="Update telemt"
|
||||
I18N[manage_site_ssl]="Site / SSL"
|
||||
I18N[manage_remove]="Remove"
|
||||
I18N[manage_language]="Language / Язык"
|
||||
|
||||
# ── Submenu: About ──────────────────────────────────────────────────────
|
||||
I18N[submenu_about_title]="ℹ️ ABOUT"
|
||||
I18N[about_version_info]="Version info"
|
||||
I18N[about_promo]="Promo / Donate"
|
||||
I18N[version_title]="🔍 Information"
|
||||
I18N[version_label]="GoTelegram:"
|
||||
I18N[version_engine]="Engine:"
|
||||
I18N[version_tech]="Technology:"
|
||||
I18N[version_license]="License:"
|
||||
|
||||
# ── Install flow ────────────────────────────────────────────────────────
|
||||
I18N[install_select_mode]="🎭 Select masquerade mode:"
|
||||
I18N[install_lite_title]="⚡ Lite — masquerade as popular website"
|
||||
I18N[install_lite_desc1]="Fast, no domain needed. telemt disguises traffic"
|
||||
I18N[install_lite_desc2]="as the chosen site (google.com etc.)"
|
||||
I18N[install_pro_title]="🛡 Pro — your own site + full masquerade"
|
||||
I18N[install_pro_desc1]="nginx + SSL + HTML template + telemt."
|
||||
I18N[install_pro_desc2]="DPI sees a real website with a real certificate."
|
||||
I18N[install_pro_desc3]="Requires: a domain pointing to this server."
|
||||
I18N[install_mode_choice]="Choice (1/2):"
|
||||
I18N[install_bad_choice]="Invalid choice: %s"
|
||||
I18N[install_lite_step]="Installing Lite mode"
|
||||
I18N[install_pro_step]="Installing Pro mode"
|
||||
I18N[install_enter_domain]="Enter your domain (e.g. example.com):"
|
||||
I18N[install_bad_domain]="Invalid domain: %s"
|
||||
I18N[install_dns_mismatch]="Domain %s points to %s, not to %s"
|
||||
I18N[install_continue_anyway]="Continue anyway?"
|
||||
I18N[install_enter_email]="Email for SSL (Enter = no email):"
|
||||
I18N[install_config_title]="📋 Configuration:"
|
||||
I18N[install_cfg_ip]="IP:"
|
||||
I18N[install_cfg_port]="Port:"
|
||||
I18N[install_cfg_mask]="Masquerade:"
|
||||
I18N[install_cfg_mode]="Mode:"
|
||||
I18N[install_cfg_domain]="Domain:"
|
||||
I18N[install_confirm_proxy]="Install proxy?"
|
||||
I18N[install_confirm_proxy_site]="Install proxy + website?"
|
||||
I18N[install_done]="GoTelegram v%s installed! (%s mode)"
|
||||
I18N[install_arch_desc1]="telemt accepts all traffic on 443 (HTTPS masquerade)"
|
||||
I18N[install_arch_desc2]="nginx serves the site on internal port %s"
|
||||
I18N[install_arch_desc3]="ISP only sees HTTPS traffic to %s:443"
|
||||
|
||||
# ── Change mode/template ────────────────────────────────────────────────
|
||||
I18N[change_current_mode]="Current mode:"
|
||||
I18N[change_template]="Change site template (pro only)"
|
||||
I18N[change_mode_switch]="Switch mode (lite ↔ pro)"
|
||||
I18N[change_only_pro]="Template change is available in pro mode only"
|
||||
I18N[change_requires_reinstall]="Mode switch requires reinstall."
|
||||
I18N[change_reinstall_confirm]="Reinstall proxy?"
|
||||
|
||||
# ── Logs ────────────────────────────────────────────────────────────────
|
||||
I18N[logs_telemt_title]="📋 telemt logs (last %s lines):"
|
||||
|
||||
# ── Link / Share ────────────────────────────────────────────────────────
|
||||
I18N[link_title]="🔗 Connection link:"
|
||||
I18N[share_title]="📤 Forward this message:"
|
||||
I18N[share_line1]="🔐 MTProxy for Telegram (GoTelegram v%s)"
|
||||
I18N[share_server]="🌍 Server: %s"
|
||||
I18N[share_port]="🔌 Port: %s"
|
||||
I18N[share_connect_cta]="👉 Connect with one tap:"
|
||||
I18N[share_footer]="Just tap the link or configure manually."
|
||||
|
||||
# ── Website ─────────────────────────────────────────────────────────────
|
||||
I18N[website_title]="🌐 Website management"
|
||||
I18N[website_domain]="Domain:"
|
||||
I18N[website_ssl_until]="SSL until:"
|
||||
I18N[website_only_pro]="Website management is available in pro mode only"
|
||||
I18N[website_renew_ssl]="Renew SSL certificate"
|
||||
I18N[website_restart_nginx]="Restart nginx"
|
||||
I18N[website_change_template]="Change template"
|
||||
|
||||
# ── Remove ──────────────────────────────────────────────────────────────
|
||||
I18N[remove_title]="🗑 Remove GoTelegram"
|
||||
I18N[remove_proxy_only]="Remove proxy only (telemt)"
|
||||
I18N[remove_bot_only]="Remove Telegram bot only"
|
||||
I18N[remove_all]="Remove everything (proxy + bot + settings)"
|
||||
I18N[remove_warn_proxy]="This will remove the proxy and all its settings."
|
||||
I18N[remove_confirm_proxy]="Remove proxy?"
|
||||
I18N[remove_backup_before]="Create a backup before removal?"
|
||||
I18N[remove_warn_all]="This will remove EVERYTHING: proxy, bot, site, settings."
|
||||
I18N[remove_confirm_all]="Are you absolutely sure?"
|
||||
I18N[remove_proxy_done]="Proxy removed"
|
||||
I18N[remove_all_done]="GoTelegram fully removed (proxy + bot)"
|
||||
|
||||
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||
I18N[bot_title]="🤖 Telegram bot"
|
||||
I18N[bot_status_running]="● Running"
|
||||
I18N[bot_status_stopped]="○ Stopped"
|
||||
I18N[bot_status_not_installed]="✗ Not installed"
|
||||
I18N[bot_menu_status]="📊 Bot status"
|
||||
I18N[bot_menu_logs]="📋 Bot logs"
|
||||
I18N[bot_menu_restart]="🔄 Restart bot"
|
||||
I18N[bot_menu_stop]="⏹ Stop bot"
|
||||
I18N[bot_menu_start]="▶️ Start bot"
|
||||
I18N[bot_menu_settings]="⚙️ Settings (.env)"
|
||||
I18N[bot_menu_remove]="🗑 Remove bot"
|
||||
I18N[bot_menu_install]="🔧 Install bot"
|
||||
I18N[bot_intro1]="The bot lets you manage the proxy from Telegram:"
|
||||
I18N[bot_intro2]="status, restart, change mode, backup, QR code."
|
||||
I18N[bot_install_step]="Installing Telegram bot"
|
||||
I18N[bot_install_python]="Installing Python3..."
|
||||
I18N[bot_files_not_found]="Bot files not found in %s"
|
||||
I18N[bot_create_venv]="Creating virtual environment..."
|
||||
I18N[bot_install_deps]="Installing dependencies..."
|
||||
I18N[bot_enter_token]="Enter BOT_TOKEN from @BotFather:"
|
||||
I18N[bot_token_empty]="Token cannot be empty"
|
||||
I18N[bot_token]="Token:"
|
||||
I18N[bot_add_admin_how]="How to add the administrator?"
|
||||
I18N[bot_admin_auto]="Auto — bot will capture the ID on first /start"
|
||||
I18N[bot_admin_manual]="Manual — enter the ID now"
|
||||
I18N[bot_admin_ids_prompt]="Admin IDs (space or comma separated):"
|
||||
I18N[bot_env_created]=".env created"
|
||||
I18N[bot_env_exists]=".env already exists, settings preserved"
|
||||
I18N[bot_wait_admin_title]="Waiting for administrator"
|
||||
I18N[bot_wait_admin_msg1]="Open the bot in Telegram and send"
|
||||
I18N[bot_wait_admin_msg2]="The bot will automatically make you an admin"
|
||||
I18N[bot_wait_admin_skip]="Press Ctrl+C to skip"
|
||||
I18N[bot_wait_spinner]="Waiting... send /start to the bot (%d sec)"
|
||||
I18N[bot_admin_assigned]="Administrator assigned!"
|
||||
I18N[bot_wait_skipped]="Skipped. Add admin later via: menu → Telegram bot → Settings"
|
||||
I18N[bot_wait_timeout]="Timeout (5 min). Add admin via: menu → Telegram bot → Settings"
|
||||
I18N[bot_installed]="Bot installed and running!"
|
||||
I18N[bot_status_title]="📊 Telegram bot status"
|
||||
I18N[bot_token_configured]="configured"
|
||||
I18N[bot_access_open]="all users"
|
||||
I18N[bot_logs_title]="📋 Bot logs (last 30 lines):"
|
||||
I18N[bot_settings_title]="⚙️ Bot settings"
|
||||
I18N[bot_current_env]="Current .env:"
|
||||
I18N[bot_change_token]="Change BOT_TOKEN"
|
||||
I18N[bot_change_allowed]="Change ALLOWED_IDS"
|
||||
I18N[bot_new_token]="New BOT_TOKEN:"
|
||||
I18N[bot_token_empty_err]="Empty token"
|
||||
I18N[bot_token_updated]="Token updated, bot restarted"
|
||||
I18N[bot_allowed_prompt]="ALLOWED_IDS (space or comma separated, empty = auto):"
|
||||
I18N[bot_access_updated]="Access updated, bot restarted"
|
||||
I18N[bot_remove_warn]="This will remove the Telegram bot and all its settings."
|
||||
I18N[bot_remove_confirm]="Remove bot?"
|
||||
I18N[bot_removed]="Bot fully removed"
|
||||
I18N[bot_restarted]="Bot restarted"
|
||||
I18N[bot_stopped]="Bot stopped"
|
||||
I18N[bot_started]="Bot started"
|
||||
I18N[bot_status_colon]="Status:"
|
||||
I18N[bot_access_colon]="Access:"
|
||||
I18N[bot_access_ids_fmt]="ID: %s"
|
||||
|
||||
# ── Promo / Donate ──────────────────────────────────────────────────────
|
||||
I18N[promo_host1_title]="💰 HOSTING #1 — UP TO 60% OFF"
|
||||
I18N[promo_host2_title]="💰 HOSTING #2 — UP TO 60% OFF"
|
||||
I18N[promo_tips_title]="☕ Donate / Tips"
|
||||
I18N[promo_link_label]="Link:"
|
||||
I18N[promo_off60]="60%% discount on the first month"
|
||||
I18N[promo_ant20]="20%% + 3%% when paid for 3 months"
|
||||
I18N[promo_ant6]="15%% + 5%% when paid for 6 months"
|
||||
I18N[promo_qr_host1]="── QR: Hosting #1 ──"
|
||||
I18N[promo_qr_host2]="── QR: Hosting #2 ──"
|
||||
I18N[promo_qr_tips]="── QR: Donate / Tips ──"
|
||||
I18N[promo_menu_in]="Menu in %d sec..."
|
||||
|
||||
# ── Stats ───────────────────────────────────────────────────────────────
|
||||
I18N[stats_title]="📊 Traffic statistics"
|
||||
I18N[stats_module_missing]="Statistics module not loaded."
|
||||
I18N[stats_file_missing]="File lib/stats.sh not found."
|
||||
I18N[stats_toggle]="Toggle counter (now: %s)"
|
||||
I18N[stats_install_collector]="Install/update stats collector"
|
||||
I18N[stats_auto_refresh]="Refresh every 3 sec"
|
||||
I18N[stats_on]="on"
|
||||
I18N[stats_off]="off"
|
||||
|
||||
# ── Templates catalog ───────────────────────────────────────────────────
|
||||
I18N[templates_categories]="📂 Site template categories:"
|
||||
I18N[templates_custom_git]="📎 Custom template from git URL"
|
||||
I18N[templates_random]="🎲 Random template"
|
||||
I18N[templates_count_fmt]="(%d templates)"
|
||||
I18N[templates_list]="📋 %s — available templates:"
|
||||
I18N[templates_preview_title]="🔍 Template preview:"
|
||||
I18N[templates_name]="Name:"
|
||||
I18N[templates_source]="Source:"
|
||||
I18N[templates_description]="Description:"
|
||||
I18N[templates_preview]="👁 Preview:"
|
||||
I18N[templates_preview_hint]="Open the link in a browser to preview the template"
|
||||
I18N[templates_repo]="📦 Repo:"
|
||||
I18N[templates_thanks]="💜 Thanks to the authors of %s for the open source code!"
|
||||
I18N[templates_install_this]="Install this template?"
|
||||
I18N[templates_cat_empty]="No templates in this category"
|
||||
I18N[templates_downloading]="Downloading template \"%s\"..."
|
||||
I18N[templates_downloaded]="Template \"%s\" downloaded"
|
||||
I18N[templates_downloaded_subfolder]="Template \"%s\" downloaded (from subfolder)"
|
||||
I18N[templates_no_index]="Template does not contain index.html"
|
||||
I18N[templates_path]="Path: %s"
|
||||
I18N[templates_catalog_not_found]="Templates catalog not found: %s"
|
||||
|
||||
# ── Custom git template ─────────────────────────────────────────────────
|
||||
I18N[custom_git_title]="📎 CUSTOM TEMPLATE FROM GIT URL"
|
||||
I18N[custom_git_help_1]="You can use ANY public static HTML repository as a template."
|
||||
I18N[custom_git_help_2]="The repository must be public and contain a ready-made"
|
||||
I18N[custom_git_help_3]="index.html (build via npm is NOT performed)."
|
||||
I18N[custom_git_formats]="Supported URL formats:"
|
||||
I18N[custom_git_fmt_github]=" • https://github.com/user/repo"
|
||||
I18N[custom_git_fmt_gitlab]=" • https://gitlab.com/user/repo"
|
||||
I18N[custom_git_fmt_gitext]=" • https://example.com/user/repo.git"
|
||||
I18N[custom_git_fmt_branch]=" • https://github.com/user/repo@branch (branch after @)"
|
||||
I18N[custom_git_auto_detect]="Repository structure (auto-detection):"
|
||||
I18N[custom_git_auto_1]=" 1. index.html in repo root"
|
||||
I18N[custom_git_auto_2]=" 2. dist/index.html (StartBootstrap, Vite, webpack)"
|
||||
I18N[custom_git_auto_3]=" 3. public/ or build/ or _site/ or site/ or docs/"
|
||||
I18N[custom_git_auto_4]=" 4. Fallback: search index.html across whole repo"
|
||||
I18N[custom_git_requirements]="Requirements:"
|
||||
I18N[custom_git_req_1]=" • HTTPS only (ssh:// and git:// are blocked)"
|
||||
I18N[custom_git_req_2]=" • Public repositories only"
|
||||
I18N[custom_git_req_3]=" • Repo size up to 100 MB"
|
||||
I18N[custom_git_req_4]=" • Static HTML (no PHP/Python/Node server code)"
|
||||
I18N[custom_git_examples]="Tested example repos:"
|
||||
I18N[custom_git_ex_1]=" • https://github.com/html5up-collective/strata"
|
||||
I18N[custom_git_ex_2]=" • https://github.com/StartBootstrap/startbootstrap-landing-page"
|
||||
I18N[custom_git_enter_url]="Paste git URL (or Enter to cancel):"
|
||||
I18N[custom_git_empty]="No URL provided, cancelled"
|
||||
I18N[custom_git_bad_url]="Invalid URL. Only https:// addresses are accepted"
|
||||
I18N[custom_git_cloning]="Cloning repository..."
|
||||
I18N[custom_git_clone_failed]="Failed to clone repository: %s"
|
||||
I18N[custom_git_too_big]="Repository is too large: %s (limit 100MB)"
|
||||
I18N[custom_git_scanning]="Scanning for index.html..."
|
||||
I18N[custom_git_found_at]="✓ Found index.html in: %s"
|
||||
I18N[custom_git_no_index]="index.html not found in the repository"
|
||||
I18N[custom_git_installed]="Custom template installed from %s"
|
||||
I18N[custom_git_saved]="Template URL saved in config (menu → Site → Update from git)"
|
||||
|
||||
# ── First-run language picker ───────────────────────────────────────────
|
||||
I18N[lang_picker_title]="Select language / Выберите язык"
|
||||
I18N[lang_english]="English"
|
||||
I18N[lang_russian]="Русский"
|
||||
I18N[lang_saved]="Language saved: %s"
|
||||
I18N[lang_change_prompt]="Select a new language:"
|
||||
|
||||
# ── Backup ──────────────────────────────────────────────────────────────
|
||||
I18N[backup_title]="💾 Backup"
|
||||
I18N[backup_creating]="Creating backup..."
|
||||
I18N[backup_created]="Backup created: %s"
|
||||
I18N[backup_failed]="Backup creation failed"
|
||||
I18N[backup_restore_title]="↩️ Restore from backup"
|
||||
I18N[backup_no_files]="No backup files"
|
||||
I18N[backup_select]="Select a backup to restore:"
|
||||
I18N[backup_restoring]="Restoring..."
|
||||
I18N[backup_restored]="Backup restored"
|
||||
I18N[backup_collecting]="Collecting configuration..."
|
||||
I18N[backup_site_included]="Website template included"
|
||||
I18N[backup_archive_err]="Archive creation failed"
|
||||
I18N[backup_archive_missing]="Archive not created"
|
||||
I18N[backup_encrypt_err]="Encryption failed"
|
||||
I18N[backup_encrypted]="Backup encrypted (AES-256-CBC)"
|
||||
I18N[backup_created_fmt]="Backup created: %s (%s)"
|
||||
I18N[backup_file_not_found_fmt]="File not found: %s"
|
||||
I18N[backup_enter_pass]="Enter backup password"
|
||||
I18N[backup_bad_pass]="Wrong password or corrupted file"
|
||||
I18N[backup_extract_err]="Archive extraction failed"
|
||||
I18N[backup_label]="Backup"
|
||||
I18N[backup_version_label]="Version"
|
||||
I18N[backup_mode_label]="Mode"
|
||||
I18N[backup_lang_label]="Language"
|
||||
I18N[backup_date_label]="Date"
|
||||
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
||||
I18N[backup_restored_telemt]="telemt config restored"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram config restored"
|
||||
I18N[backup_restored_lang]="Interface language restored"
|
||||
I18N[backup_restored_nginx]="nginx config restored"
|
||||
I18N[backup_restored_ssl]="SSL certificates restored"
|
||||
I18N[backup_restored_site]="Website template restored"
|
||||
I18N[backup_restore_done]="Restore completed!"
|
||||
I18N[backup_none]="No backups"
|
||||
I18N[backup_list_title]="Available backups"
|
||||
I18N[backup_cleanup_fmt]="Removed %s old backups (kept %s)"
|
||||
I18N[backup_create_title]="Create backup"
|
||||
I18N[backup_encrypt_prompt]="Encrypt backup with a password?"
|
||||
I18N[backup_repeat_pass]="Repeat password"
|
||||
I18N[backup_pass_mismatch]="Passwords do not match"
|
||||
I18N[backup_pass_short]="Password too short (minimum 6 characters)"
|
||||
I18N[backup_pick_prompt]="Backup number (or path to file)"
|
||||
I18N[backup_not_found]="Backup not found"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Run the script with sudo / as root"
|
||||
I18N[err_os_unknown]="Failed to detect OS. Linux is required."
|
||||
I18N[err_low_disk]="Low disk space: %sMB (need %sMB+)"
|
||||
I18N[err_bad_pkg_mgr]="Unknown package manager"
|
||||
I18N[err_unexpected]="Unexpected error"
|
||||
I18N[bye]="See you later! 👋"
|
||||
I18N[auto_refresh]="Refresh in 30 sec"
|
||||
|
||||
# ── Deps ────────────────────────────────────────────────────────────────
|
||||
I18N[deps_installing]="Installing dependencies: %s"
|
||||
|
||||
# ── Migration ───────────────────────────────────────────────────────────
|
||||
I18N[v1_detected]="⚠️ GoTelegram v1 (mtg) installation detected"
|
||||
I18N[v1_container]="Container: %s"
|
||||
I18N[v1_migration_step]="Migrating from v1 (mtg) to v2 (telemt)"
|
||||
I18N[v1_found_title]="Found v1 (mtg) installation:"
|
||||
I18N[v1_port]="Port: %s"
|
||||
I18N[v1_secret]="Secret: %s..."
|
||||
I18N[v1_incompatible]="mtg secret is NOT directly compatible with telemt."
|
||||
I18N[v1_new_link]="Clients will need a new link."
|
||||
I18N[v1_stop_migrate]="Stop v1 container and migrate to v2? [Y/n]:"
|
||||
I18N[v1_migration_cancelled]="Migration cancelled. v1 left intact."
|
||||
I18N[v1_stopping]="Stopping v1 container..."
|
||||
I18N[v1_config_saved]="v1 config saved to %s"
|
||||
I18N[v1_port_freed]="v1 stopped. Port %s freed."
|
||||
375
lib/lang/ru.sh
Executable file
375
lib/lang/ru.sh
Executable file
@@ -0,0 +1,375 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — Russian translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
I18N[yes]="Да"
|
||||
I18N[no]="Нет"
|
||||
I18N[ok]="OK"
|
||||
I18N[cancel]="Отмена"
|
||||
I18N[back]="« Назад"
|
||||
I18N[exit]="Выход"
|
||||
I18N[skip]="Пропустить"
|
||||
I18N[choose]="Выбор"
|
||||
I18N[press_enter]="Нажмите Enter..."
|
||||
I18N[press_enter_to_return]="Нажмите Enter для возврата в меню..."
|
||||
I18N[invalid_choice]="Неверный выбор"
|
||||
I18N[running]="работает"
|
||||
I18N[stopped]="остановлен"
|
||||
I18N[not_installed]="не установлен"
|
||||
I18N[unknown]="неизвестно"
|
||||
I18N[error]="Ошибка"
|
||||
I18N[warning]="Внимание"
|
||||
I18N[info]="Инфо"
|
||||
I18N[success]="Готово"
|
||||
I18N[wait]="Подождите..."
|
||||
|
||||
# ── Banner ──────────────────────────────────────────────────────────────
|
||||
I18N[banner_title]="GoTelegram v%s"
|
||||
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
|
||||
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||
I18N[credits_title]="Благодарности / Credits"
|
||||
|
||||
# ── Main menu (dashboard) ───────────────────────────────────────────────
|
||||
I18N[dashboard_title]="Панель управления"
|
||||
I18N[svc_proxy]="Прокси"
|
||||
I18N[svc_nginx]="nginx"
|
||||
I18N[svc_site]="Сайт"
|
||||
I18N[svc_ssl]="SSL"
|
||||
I18N[svc_bot]="Бот"
|
||||
I18N[ssl_until]="до %s"
|
||||
I18N[net_ip]="IP:"
|
||||
I18N[net_port]="Порт:"
|
||||
I18N[net_mode]="Режим:"
|
||||
I18N[net_domain]="Домен:"
|
||||
I18N[connection_link]="Ссылка для Telegram:"
|
||||
I18N[proxy_not_configured]="Прокси не настроен. Выберите пункт 1."
|
||||
I18N[menu_proxy]="Прокси ▸"
|
||||
I18N[menu_stats]="Статистика ▸"
|
||||
I18N[menu_manage]="Управление ▸"
|
||||
I18N[menu_telegram_bot]="Telegram-бот ▸"
|
||||
I18N[menu_about]="О программе ▸"
|
||||
I18N[auto_refresh_30s]="Обновление через 30 сек"
|
||||
|
||||
# ── Submenu: Proxy ──────────────────────────────────────────────────────
|
||||
I18N[submenu_proxy_title]="🚀 ПРОКСИ"
|
||||
I18N[proxy_install_update]="Установить / Обновить"
|
||||
I18N[proxy_status_detail]="Статус подробно"
|
||||
I18N[proxy_copy_link]="Скопировать ссылку"
|
||||
I18N[proxy_share]="Поделиться ключом"
|
||||
I18N[proxy_restart]="Перезапуск"
|
||||
I18N[proxy_logs]="Логи"
|
||||
I18N[proxy_change_mode]="Сменить режим / шаблон"
|
||||
|
||||
# ── Submenu: Manage ─────────────────────────────────────────────────────
|
||||
I18N[submenu_manage_title]="⚙️ УПРАВЛЕНИЕ"
|
||||
I18N[manage_backup]="Бекап"
|
||||
I18N[manage_restore]="Восстановить"
|
||||
I18N[manage_update_telemt]="Обновить telemt"
|
||||
I18N[manage_site_ssl]="Сайт / SSL"
|
||||
I18N[manage_remove]="Удалить"
|
||||
I18N[manage_language]="Язык / Language"
|
||||
|
||||
# ── Submenu: About ──────────────────────────────────────────────────────
|
||||
I18N[submenu_about_title]="ℹ️ О ПРОГРАММЕ"
|
||||
I18N[about_version_info]="Информация о версии"
|
||||
I18N[about_promo]="Промо / Донат"
|
||||
I18N[version_title]="🔍 Информация"
|
||||
I18N[version_label]="GoTelegram:"
|
||||
I18N[version_engine]="Ядро:"
|
||||
I18N[version_tech]="Технология:"
|
||||
I18N[version_license]="Лицензия:"
|
||||
|
||||
# ── Install flow ────────────────────────────────────────────────────────
|
||||
I18N[install_select_mode]="🎭 Выберите режим маскировки:"
|
||||
I18N[install_lite_title]="⚡ Lite — маскировка под популярный сайт"
|
||||
I18N[install_lite_desc1]="Быстро, без домена. telemt маскирует трафик"
|
||||
I18N[install_lite_desc2]="под выбранный сайт (google.com и т.д.)"
|
||||
I18N[install_pro_title]="🛡 Pro — свой сайт + полная маскировка"
|
||||
I18N[install_pro_desc1]="nginx + SSL + HTML-шаблон + telemt."
|
||||
I18N[install_pro_desc2]="DPI видит реальный сайт с реальным сертификатом."
|
||||
I18N[install_pro_desc3]="Требует: домен, направленный на этот сервер."
|
||||
I18N[install_mode_choice]="Выбор (1/2):"
|
||||
I18N[install_bad_choice]="Неверный выбор: %s"
|
||||
I18N[install_lite_step]="Установка Lite-режима"
|
||||
I18N[install_pro_step]="Установка Pro-режима"
|
||||
I18N[install_enter_domain]="Введите ваш домен (например, example.com):"
|
||||
I18N[install_bad_domain]="Некорректный домен: %s"
|
||||
I18N[install_dns_mismatch]="Домен %s указывает на %s, а не на %s"
|
||||
I18N[install_continue_anyway]="Продолжить всё равно?"
|
||||
I18N[install_enter_email]="Email для SSL (Enter = без email):"
|
||||
I18N[install_config_title]="📋 Конфигурация:"
|
||||
I18N[install_cfg_ip]="IP:"
|
||||
I18N[install_cfg_port]="Порт:"
|
||||
I18N[install_cfg_mask]="Маскировка:"
|
||||
I18N[install_cfg_mode]="Режим:"
|
||||
I18N[install_cfg_domain]="Домен:"
|
||||
I18N[install_confirm_proxy]="Установить прокси?"
|
||||
I18N[install_confirm_proxy_site]="Установить прокси + сайт?"
|
||||
I18N[install_done]="GoTelegram v%s установлен! (%s-режим)"
|
||||
I18N[install_arch_desc1]="telemt принимает весь трафик на 443 (маскировка под HTTPS)"
|
||||
I18N[install_arch_desc2]="nginx обслуживает сайт на внутреннем порту %s"
|
||||
I18N[install_arch_desc3]="Провайдер видит только HTTPS-трафик к %s:443"
|
||||
|
||||
# ── Change mode/template ────────────────────────────────────────────────
|
||||
I18N[change_current_mode]="Текущий режим:"
|
||||
I18N[change_template]="Сменить шаблон сайта (только pro)"
|
||||
I18N[change_mode_switch]="Переключить режим (lite ↔ pro)"
|
||||
I18N[change_only_pro]="Смена шаблона доступна только в pro-режиме"
|
||||
I18N[change_requires_reinstall]="Переключение режима требует переустановки."
|
||||
I18N[change_reinstall_confirm]="Переустановить прокси?"
|
||||
|
||||
# ── Logs ────────────────────────────────────────────────────────────────
|
||||
I18N[logs_telemt_title]="📋 Логи telemt (последние %s строк):"
|
||||
|
||||
# ── Link / Share ────────────────────────────────────────────────────────
|
||||
I18N[link_title]="🔗 Ссылка для подключения:"
|
||||
I18N[share_title]="📤 Перешлите это сообщение:"
|
||||
I18N[share_line1]="🔐 MTProxy для Telegram (GoTelegram v%s)"
|
||||
I18N[share_server]="🌍 Сервер: %s"
|
||||
I18N[share_port]="🔌 Порт: %s"
|
||||
I18N[share_connect_cta]="👉 Подключиться одним нажатием:"
|
||||
I18N[share_footer]="Просто нажмите на ссылку или настройте вручную."
|
||||
|
||||
# ── Website ─────────────────────────────────────────────────────────────
|
||||
I18N[website_title]="🌐 Управление сайтом"
|
||||
I18N[website_domain]="Домен:"
|
||||
I18N[website_ssl_until]="SSL до:"
|
||||
I18N[website_only_pro]="Управление сайтом доступно только в pro-режиме"
|
||||
I18N[website_renew_ssl]="Обновить SSL сертификат"
|
||||
I18N[website_restart_nginx]="Перезапустить nginx"
|
||||
I18N[website_change_template]="Сменить шаблон"
|
||||
|
||||
# ── Remove ──────────────────────────────────────────────────────────────
|
||||
I18N[remove_title]="🗑 Удаление GoTelegram"
|
||||
I18N[remove_proxy_only]="Удалить только прокси (telemt)"
|
||||
I18N[remove_bot_only]="Удалить только Telegram-бота"
|
||||
I18N[remove_all]="Удалить всё (прокси + бот + настройки)"
|
||||
I18N[remove_warn_proxy]="Это удалит прокси и все его настройки."
|
||||
I18N[remove_confirm_proxy]="Удалить прокси?"
|
||||
I18N[remove_backup_before]="Сделать бекап перед удалением?"
|
||||
I18N[remove_warn_all]="Это удалит ВСЁ: прокси, бот, сайт, настройки."
|
||||
I18N[remove_confirm_all]="Вы точно уверены?"
|
||||
I18N[remove_proxy_done]="Прокси удалён"
|
||||
I18N[remove_all_done]="GoTelegram полностью удалён (прокси + бот)"
|
||||
|
||||
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||
I18N[bot_title]="🤖 Telegram-бот"
|
||||
I18N[bot_status_running]="● Работает"
|
||||
I18N[bot_status_stopped]="○ Остановлен"
|
||||
I18N[bot_status_not_installed]="✗ Не установлен"
|
||||
I18N[bot_menu_status]="📊 Статус бота"
|
||||
I18N[bot_menu_logs]="📋 Логи бота"
|
||||
I18N[bot_menu_restart]="🔄 Перезапустить бота"
|
||||
I18N[bot_menu_stop]="⏹ Остановить бота"
|
||||
I18N[bot_menu_start]="▶️ Запустить бота"
|
||||
I18N[bot_menu_settings]="⚙️ Настройки (.env)"
|
||||
I18N[bot_menu_remove]="🗑 Удалить бота"
|
||||
I18N[bot_menu_install]="🔧 Установить бота"
|
||||
I18N[bot_intro1]="Бот позволяет управлять прокси прямо из Telegram:"
|
||||
I18N[bot_intro2]="статус, перезапуск, смена режима, бекап, QR-код."
|
||||
I18N[bot_install_step]="Установка Telegram-бота"
|
||||
I18N[bot_install_python]="Установка Python3..."
|
||||
I18N[bot_files_not_found]="Файлы бота не найдены в %s"
|
||||
I18N[bot_create_venv]="Создание виртуального окружения..."
|
||||
I18N[bot_install_deps]="Установка зависимостей..."
|
||||
I18N[bot_enter_token]="Введите BOT_TOKEN от @BotFather:"
|
||||
I18N[bot_token_empty]="Токен не может быть пустым"
|
||||
I18N[bot_token]="Token:"
|
||||
I18N[bot_add_admin_how]="Как добавить администратора?"
|
||||
I18N[bot_admin_auto]="Автоматически — бот определит ID при первом /start"
|
||||
I18N[bot_admin_manual]="Вручную — ввести ID сейчас"
|
||||
I18N[bot_admin_ids_prompt]="ID администраторов (через пробел/запятую):"
|
||||
I18N[bot_env_created]=".env создан"
|
||||
I18N[bot_env_exists]=".env уже существует, настройки сохранены"
|
||||
I18N[bot_wait_admin_title]="Ожидание администратора"
|
||||
I18N[bot_wait_admin_msg1]="Откройте бота в Telegram и отправьте"
|
||||
I18N[bot_wait_admin_msg2]="Бот автоматически назначит вас администратором"
|
||||
I18N[bot_wait_admin_skip]="Нажмите Ctrl+C чтобы пропустить"
|
||||
I18N[bot_wait_spinner]="Ожидание... напишите /start боту (%d сек)"
|
||||
I18N[bot_admin_assigned]="Администратор назначен!"
|
||||
I18N[bot_wait_skipped]="Пропущено. Добавить админа позже: меню → Telegram-бот → Настройки"
|
||||
I18N[bot_wait_timeout]="Таймаут (5 мин). Добавить админа: меню → Telegram-бот → Настройки"
|
||||
I18N[bot_installed]="Бот установлен и запущен!"
|
||||
I18N[bot_status_title]="📊 Статус Telegram-бота"
|
||||
I18N[bot_token_configured]="настроен"
|
||||
I18N[bot_access_open]="все пользователи"
|
||||
I18N[bot_logs_title]="📋 Логи бота (последние 30 строк):"
|
||||
I18N[bot_settings_title]="⚙️ Настройки бота"
|
||||
I18N[bot_current_env]="Текущий .env:"
|
||||
I18N[bot_change_token]="Сменить BOT_TOKEN"
|
||||
I18N[bot_change_allowed]="Изменить ALLOWED_IDS"
|
||||
I18N[bot_new_token]="Новый BOT_TOKEN:"
|
||||
I18N[bot_token_empty_err]="Пустой токен"
|
||||
I18N[bot_token_updated]="Токен обновлён, бот перезапущен"
|
||||
I18N[bot_allowed_prompt]="ALLOWED_IDS (через пробел/запятую, пусто = авто):"
|
||||
I18N[bot_access_updated]="Доступ обновлён, бот перезапущен"
|
||||
I18N[bot_remove_warn]="Это удалит Telegram-бота и все его настройки."
|
||||
I18N[bot_remove_confirm]="Удалить бота?"
|
||||
I18N[bot_removed]="Бот полностью удалён"
|
||||
I18N[bot_restarted]="Бот перезапущен"
|
||||
I18N[bot_stopped]="Бот остановлен"
|
||||
I18N[bot_started]="Бот запущен"
|
||||
I18N[bot_status_colon]="Статус:"
|
||||
I18N[bot_access_colon]="Доступ:"
|
||||
I18N[bot_access_ids_fmt]="ID: %s"
|
||||
|
||||
# ── Promo / Donate ──────────────────────────────────────────────────────
|
||||
I18N[promo_host1_title]="💰 ХОСТИНГ #1 — СКИДКА ДО 60%"
|
||||
I18N[promo_host2_title]="💰 ХОСТИНГ #2 — СКИДКА ДО 60%"
|
||||
I18N[promo_tips_title]="☕ Донат / Чаевые"
|
||||
I18N[promo_link_label]="Ссылка:"
|
||||
I18N[promo_off60]="60%% скидки на первый месяц"
|
||||
I18N[promo_ant20]="20%% + 3%% при оплате за 3 месяца"
|
||||
I18N[promo_ant6]="15%% + 5%% при оплате за 6 месяцев"
|
||||
I18N[promo_qr_host1]="── QR: Хостинг #1 ──"
|
||||
I18N[promo_qr_host2]="── QR: Хостинг #2 ──"
|
||||
I18N[promo_qr_tips]="── QR: Чаевые / Донат ──"
|
||||
I18N[promo_menu_in]="Меню через %d сек..."
|
||||
|
||||
# ── Stats ───────────────────────────────────────────────────────────────
|
||||
I18N[stats_title]="📊 Статистика трафика"
|
||||
I18N[stats_module_missing]="Модуль статистики не загружен."
|
||||
I18N[stats_file_missing]="Файл lib/stats.sh не найден."
|
||||
I18N[stats_toggle]="Вкл/Выкл подсчёт (сейчас: %s)"
|
||||
I18N[stats_install_collector]="Установить/обновить сборщик статистики"
|
||||
I18N[stats_auto_refresh]="Обновление каждые 3 сек"
|
||||
I18N[stats_on]="вкл"
|
||||
I18N[stats_off]="выкл"
|
||||
|
||||
# ── Templates catalog ───────────────────────────────────────────────────
|
||||
I18N[templates_categories]="📂 Категории шаблонов сайтов:"
|
||||
I18N[templates_custom_git]="📎 Свой шаблон по git URL"
|
||||
I18N[templates_random]="🎲 Случайный шаблон"
|
||||
I18N[templates_count_fmt]="(%d шаблонов)"
|
||||
I18N[templates_list]="📋 %s — доступные шаблоны:"
|
||||
I18N[templates_preview_title]="🔍 Превью шаблона:"
|
||||
I18N[templates_name]="Название:"
|
||||
I18N[templates_source]="Источник:"
|
||||
I18N[templates_description]="Описание:"
|
||||
I18N[templates_preview]="👁 Превью:"
|
||||
I18N[templates_preview_hint]="Откройте ссылку в браузере для просмотра шаблона"
|
||||
I18N[templates_repo]="📦 Репо:"
|
||||
I18N[templates_thanks]="💜 Спасибо авторам %s за открытый код!"
|
||||
I18N[templates_install_this]="Установить этот шаблон?"
|
||||
I18N[templates_cat_empty]="В этой категории нет шаблонов"
|
||||
I18N[templates_downloading]="Скачивание шаблона \"%s\"..."
|
||||
I18N[templates_downloaded]="Шаблон \"%s\" скачан"
|
||||
I18N[templates_downloaded_subfolder]="Шаблон \"%s\" скачан (из подпапки)"
|
||||
I18N[templates_no_index]="Шаблон не содержит index.html"
|
||||
I18N[templates_path]="Путь: %s"
|
||||
I18N[templates_catalog_not_found]="Каталог шаблонов не найден: %s"
|
||||
|
||||
# ── Custom git template ─────────────────────────────────────────────────
|
||||
I18N[custom_git_title]="📎 СВОЙ ШАБЛОН ПО GIT URL"
|
||||
I18N[custom_git_help_1]="Вы можете использовать ЛЮБОЙ репозиторий со статическим HTML-сайтом"
|
||||
I18N[custom_git_help_2]="в качестве шаблона. Репозиторий должен быть публичным и содержать"
|
||||
I18N[custom_git_help_3]="готовый index.html (сборка через npm НЕ выполняется)."
|
||||
I18N[custom_git_formats]="Поддерживаемые форматы URL:"
|
||||
I18N[custom_git_fmt_github]=" • https://github.com/user/repo"
|
||||
I18N[custom_git_fmt_gitlab]=" • https://gitlab.com/user/repo"
|
||||
I18N[custom_git_fmt_gitext]=" • https://example.com/user/repo.git"
|
||||
I18N[custom_git_fmt_branch]=" • https://github.com/user/repo@branch (ветка после @)"
|
||||
I18N[custom_git_auto_detect]="Структура репозитория (авто-определение):"
|
||||
I18N[custom_git_auto_1]=" 1. index.html в корне репозитория"
|
||||
I18N[custom_git_auto_2]=" 2. dist/index.html (StartBootstrap, Vite, webpack)"
|
||||
I18N[custom_git_auto_3]=" 3. public/ или build/ или _site/ или site/ или docs/"
|
||||
I18N[custom_git_auto_4]=" 4. Fallback: поиск index.html по всему репозиторию"
|
||||
I18N[custom_git_requirements]="Требования:"
|
||||
I18N[custom_git_req_1]=" • Только HTTPS (ssh:// и git:// блокируются)"
|
||||
I18N[custom_git_req_2]=" • Только публичные репозитории"
|
||||
I18N[custom_git_req_3]=" • Размер репо не более 100 МБ"
|
||||
I18N[custom_git_req_4]=" • Статический HTML (без серверного кода PHP/Python/Node)"
|
||||
I18N[custom_git_examples]="Примеры проверенных репо:"
|
||||
I18N[custom_git_ex_1]=" • https://github.com/html5up-collective/strata"
|
||||
I18N[custom_git_ex_2]=" • https://github.com/StartBootstrap/startbootstrap-landing-page"
|
||||
I18N[custom_git_enter_url]="Вставьте git URL (или Enter для отмены):"
|
||||
I18N[custom_git_empty]="URL не указан, отмена"
|
||||
I18N[custom_git_bad_url]="Недопустимый URL. Принимаются только https:// адреса"
|
||||
I18N[custom_git_cloning]="Клонирование репозитория..."
|
||||
I18N[custom_git_clone_failed]="Не удалось клонировать репозиторий: %s"
|
||||
I18N[custom_git_too_big]="Репозиторий слишком большой: %s (лимит 100MB)"
|
||||
I18N[custom_git_scanning]="Поиск index.html в структуре..."
|
||||
I18N[custom_git_found_at]="✓ Найден index.html в: %s"
|
||||
I18N[custom_git_no_index]="index.html не найден в репозитории"
|
||||
I18N[custom_git_installed]="Свой шаблон установлен из %s"
|
||||
I18N[custom_git_saved]="URL шаблона сохранён в конфиге (меню → Сайт → Обновить из git)"
|
||||
|
||||
# ── First-run language picker ───────────────────────────────────────────
|
||||
I18N[lang_picker_title]="Выберите язык / Select language"
|
||||
I18N[lang_english]="English"
|
||||
I18N[lang_russian]="Русский"
|
||||
I18N[lang_saved]="Язык сохранён: %s"
|
||||
I18N[lang_change_prompt]="Выберите новый язык:"
|
||||
|
||||
# ── Backup ──────────────────────────────────────────────────────────────
|
||||
I18N[backup_title]="💾 Бекап"
|
||||
I18N[backup_creating]="Создание бекапа..."
|
||||
I18N[backup_created]="Бекап создан: %s"
|
||||
I18N[backup_failed]="Ошибка создания бекапа"
|
||||
I18N[backup_restore_title]="↩️ Восстановление из бекапа"
|
||||
I18N[backup_no_files]="Нет файлов бекапа"
|
||||
I18N[backup_select]="Выберите бекап для восстановления:"
|
||||
I18N[backup_restoring]="Восстановление..."
|
||||
I18N[backup_restored]="Бекап восстановлен"
|
||||
I18N[backup_collecting]="Собираю конфигурацию..."
|
||||
I18N[backup_site_included]="Шаблон сайта включён"
|
||||
I18N[backup_archive_err]="Ошибка создания архива"
|
||||
I18N[backup_archive_missing]="Архив не создан"
|
||||
I18N[backup_encrypt_err]="Ошибка шифрования"
|
||||
I18N[backup_encrypted]="Бекап зашифрован (AES-256-CBC)"
|
||||
I18N[backup_created_fmt]="Бекап создан: %s (%s)"
|
||||
I18N[backup_file_not_found_fmt]="Файл не найден: %s"
|
||||
I18N[backup_enter_pass]="Введите пароль от бекапа"
|
||||
I18N[backup_bad_pass]="Неверный пароль или повреждённый файл"
|
||||
I18N[backup_extract_err]="Ошибка распаковки архива"
|
||||
I18N[backup_label]="Бекап"
|
||||
I18N[backup_version_label]="Версия"
|
||||
I18N[backup_mode_label]="Режим"
|
||||
I18N[backup_lang_label]="Язык"
|
||||
I18N[backup_date_label]="Дата"
|
||||
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
||||
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
|
||||
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
||||
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||
I18N[backup_restored_site]="Шаблон сайта восстановлен"
|
||||
I18N[backup_restore_done]="Восстановление завершено!"
|
||||
I18N[backup_none]="Бекапов нет"
|
||||
I18N[backup_list_title]="Доступные бекапы"
|
||||
I18N[backup_cleanup_fmt]="Удалено %s старых бекапов (оставлено %s)"
|
||||
I18N[backup_create_title]="Создание бекапа"
|
||||
I18N[backup_encrypt_prompt]="Зашифровать бекап паролем?"
|
||||
I18N[backup_repeat_pass]="Повторите пароль"
|
||||
I18N[backup_pass_mismatch]="Пароли не совпадают"
|
||||
I18N[backup_pass_short]="Пароль слишком короткий (минимум 6 символов)"
|
||||
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||
I18N[backup_not_found]="Бекап не найден"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Запустите скрипт с sudo / от root"
|
||||
I18N[err_os_unknown]="Не удалось определить ОС. Требуется Linux."
|
||||
I18N[err_low_disk]="Мало места на диске: %sMB (нужно %sMB+)"
|
||||
I18N[err_bad_pkg_mgr]="Неизвестный пакетный менеджер"
|
||||
I18N[err_unexpected]="Неожиданная ошибка"
|
||||
I18N[bye]="До встречи! 👋"
|
||||
I18N[auto_refresh]="Обновление через 30 сек"
|
||||
|
||||
# ── Deps ────────────────────────────────────────────────────────────────
|
||||
I18N[deps_installing]="Установка зависимостей: %s"
|
||||
|
||||
# ── Migration ───────────────────────────────────────────────────────────
|
||||
I18N[v1_detected]="⚠️ Обнаружена установка GoTelegram v1 (mtg)"
|
||||
I18N[v1_container]="Контейнер: %s"
|
||||
I18N[v1_migration_step]="Миграция с v1 (mtg) на v2 (telemt)"
|
||||
I18N[v1_found_title]="Найдена установка v1 (mtg):"
|
||||
I18N[v1_port]="Порт: %s"
|
||||
I18N[v1_secret]="Secret: %s..."
|
||||
I18N[v1_incompatible]="секрет mtg НЕ совместим с telemt напрямую."
|
||||
I18N[v1_new_link]="Клиентам потребуется новая ссылка."
|
||||
I18N[v1_stop_migrate]="Остановить v1 контейнер и перейти на v2? [Y/n]:"
|
||||
I18N[v1_migration_cancelled]="Миграция отменена. v1 оставлен без изменений."
|
||||
I18N[v1_stopping]="Остановка v1 контейнера..."
|
||||
I18N[v1_config_saved]="Конфиг v1 сохранён в %s"
|
||||
I18N[v1_port_freed]="v1 остановлен. Порт %s освобождён."
|
||||
406
lib/stats.sh
406
lib/stats.sh
@@ -1,406 +0,0 @@
|
||||
#!/bin/bash
|
||||
# stats.sh — Traffic statistics module for GoTelegram
|
||||
# Tracks proxy (telemt port 443) and site (nginx port 8443) traffic
|
||||
# Uses iptables counters + real-time snapshots + historical CSV
|
||||
|
||||
# Color codes (from common.sh)
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
STATS_DIR="/run/gotelegram"
|
||||
HISTORY_FILE="/opt/gotelegram/stats_history.csv"
|
||||
SNAPSHOTS_DIR="$STATS_DIR/snapshots"
|
||||
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
|
||||
CONFIG_FILE="/opt/gotelegram/config.json"
|
||||
|
||||
# Initialize stats infrastructure
|
||||
stats_init() {
|
||||
# Create runtime directory
|
||||
mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
||||
chmod 755 "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
||||
|
||||
# Create iptables chain if not exists
|
||||
if ! iptables -L GOTELEGRAM_STATS -n >/dev/null 2>&1; then
|
||||
iptables -N GOTELEGRAM_STATS 2>/dev/null
|
||||
fi
|
||||
|
||||
# Add chain to INPUT if not already present
|
||||
if ! iptables -C INPUT -j GOTELEGRAM_STATS 2>/dev/null; then
|
||||
iptables -I INPUT -j GOTELEGRAM_STATS 2>/dev/null
|
||||
fi
|
||||
|
||||
# Add rule for proxy traffic (port 443, TCP)
|
||||
if ! iptables -C GOTELEGRAM_STATS -p tcp --dport 443 2>/dev/null; then
|
||||
iptables -A GOTELEGRAM_STATS -p tcp --dport 443 2>/dev/null
|
||||
fi
|
||||
|
||||
# Add rule for site traffic (loopback, port 8443, TCP)
|
||||
if ! iptables -C GOTELEGRAM_STATS -i lo -p tcp --dport 8443 2>/dev/null; then
|
||||
iptables -A GOTELEGRAM_STATS -i lo -p tcp --dport 8443 2>/dev/null
|
||||
fi
|
||||
|
||||
# Initialize CSV header if file doesn't exist
|
||||
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Write initial snapshot
|
||||
stats_collect
|
||||
}
|
||||
|
||||
# Collect current traffic statistics from iptables
|
||||
stats_collect() {
|
||||
local proxy_bytes=0 proxy_pkts=0 site_bytes=0 site_pkts=0
|
||||
local ts=$(date +%s)
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
# Parse iptables output: format is "pkts bytes target"
|
||||
# We need to extract bytes (2nd column) for each rule
|
||||
local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null)
|
||||
|
||||
# Extract counters for port 443 (proxy)
|
||||
proxy_bytes=$(echo "$iptables_output" | grep "dpt:443" | grep -v "lo" | awk '{print $2}')
|
||||
proxy_pkts=$(echo "$iptables_output" | grep "dpt:443" | grep -v "lo" | awk '{print $1}')
|
||||
|
||||
# Extract counters for port 8443 on loopback (site)
|
||||
site_bytes=$(echo "$iptables_output" | grep "dpt:8443" | awk '{print $2}')
|
||||
site_pkts=$(echo "$iptables_output" | grep "dpt:8443" | awk '{print $1}')
|
||||
|
||||
# Default to 0 if not found
|
||||
proxy_bytes=${proxy_bytes:-0}
|
||||
proxy_pkts=${proxy_pkts:-0}
|
||||
site_bytes=${site_bytes:-0}
|
||||
site_pkts=${site_pkts:-0}
|
||||
|
||||
# Write current snapshot as JSON
|
||||
if command -v jq &>/dev/null; then
|
||||
echo "{\"ts\":$ts,\"proxy_bytes\":$proxy_bytes,\"proxy_pkts\":$proxy_pkts,\"site_bytes\":$site_bytes,\"site_pkts\":$site_pkts}" > "$CURRENT_SNAPSHOT" 2>/dev/null
|
||||
else
|
||||
cat > "$CURRENT_SNAPSHOT" 2>/dev/null <<EOF
|
||||
{"ts":$ts,"proxy_bytes":$proxy_bytes,"proxy_pkts":$proxy_pkts,"site_bytes":$site_bytes,"site_pkts":$site_pkts}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Save snapshot for rate calculation (one per minute)
|
||||
local minute_key
|
||||
minute_key=$(date +%Y%m%d%H%M 2>/dev/null)
|
||||
local snapshot_file="$SNAPSHOTS_DIR/snap_${minute_key}.json"
|
||||
cp "$CURRENT_SNAPSHOT" "$snapshot_file" 2>/dev/null
|
||||
|
||||
# Append to history CSV (once per minute, check if last entry is fresh)
|
||||
if [[ -f "$HISTORY_FILE" ]]; then
|
||||
local last_ts
|
||||
last_ts=$(grep -E '^[0-9]' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f1)
|
||||
last_ts="${last_ts:-0}"
|
||||
local current_minute=$((ts - (ts % 60)))
|
||||
|
||||
if [[ "$last_ts" -eq 0 ]] || [[ $((current_minute - last_ts)) -ge 60 ]]; then
|
||||
echo "$current_minute,$proxy_bytes,$site_bytes" >> "$HISTORY_FILE" 2>/dev/null
|
||||
|
||||
# Cleanup old entries (keep only 365 days)
|
||||
stats_cleanup_history
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$temp_file" 2>/dev/null
|
||||
}
|
||||
|
||||
# Read current snapshot as JSON
|
||||
stats_read_current() {
|
||||
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
|
||||
cat "$CURRENT_SNAPSHOT"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract value from JSON (fallback if jq not available)
|
||||
json_get() {
|
||||
local json="$1"
|
||||
local key="$2"
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
echo "$json" | jq -r ".${key}" 2>/dev/null || echo "0"
|
||||
else
|
||||
echo "$json" | grep -o "\"$key\":[^,}]*" | cut -d: -f2 | tr -d ' "' || echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Convert bytes to human-readable format
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
|
||||
if (( bytes < 1024 )); then
|
||||
printf "%.0f B" "$bytes"
|
||||
elif (( bytes < 1024 * 1024 )); then
|
||||
printf "%.1f KB" "$(echo "scale=1; $bytes / 1024" | bc 2>/dev/null || echo "$((bytes / 1024))")"
|
||||
elif (( bytes < 1024 * 1024 * 1024 )); then
|
||||
printf "%.1f MB" "$(echo "scale=1; $bytes / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024))")"
|
||||
elif (( bytes < 1024 * 1024 * 1024 * 1024 )); then
|
||||
printf "%.1f GB" "$(echo "scale=1; $bytes / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024 / 1024))")"
|
||||
else
|
||||
printf "%.1f TB" "$(echo "scale=1; $bytes / 1024 / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024 / 1024 / 1024))")"
|
||||
fi
|
||||
}
|
||||
|
||||
# Convert bytes/sec to human-readable rate
|
||||
format_rate() {
|
||||
local bytes_per_sec=$1
|
||||
|
||||
if (( bytes_per_sec < 1024 )); then
|
||||
printf "%.0f B/s" "$bytes_per_sec"
|
||||
elif (( bytes_per_sec < 1024 * 1024 )); then
|
||||
printf "%.1f KB/s" "$(echo "scale=1; $bytes_per_sec / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024))")"
|
||||
elif (( bytes_per_sec < 1024 * 1024 * 1024 )); then
|
||||
printf "%.1f MB/s" "$(echo "scale=1; $bytes_per_sec / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024 / 1024))")"
|
||||
else
|
||||
printf "%.1f GB/s" "$(echo "scale=1; $bytes_per_sec / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024 / 1024 / 1024))")"
|
||||
fi
|
||||
}
|
||||
|
||||
# Safely convert value to integer (returns 0 for empty/non-numeric)
|
||||
_to_int() {
|
||||
local val="${1:-0}"
|
||||
# Strip non-numeric chars, default to 0
|
||||
val="${val//[^0-9]/}"
|
||||
echo "${val:-0}"
|
||||
}
|
||||
|
||||
# Calculate diff safely (never negative, never crashes on empty)
|
||||
_safe_diff() {
|
||||
local a=$(_to_int "$1")
|
||||
local b=$(_to_int "$2")
|
||||
local d=$((a - b))
|
||||
(( d < 0 )) && d=0
|
||||
echo "$d"
|
||||
}
|
||||
|
||||
# Calculate traffic rates and totals from history
|
||||
stats_calculate_rates() {
|
||||
local traffic_type="$1" # "proxy" or "site"
|
||||
local col_idx=2 # proxy_bytes is column 2
|
||||
[[ "$traffic_type" == "site" ]] && col_idx=3
|
||||
|
||||
local now
|
||||
now=$(date +%s)
|
||||
|
||||
# Get latest data line (skip header with grep -E '^[0-9]')
|
||||
local bytes_now
|
||||
bytes_now=$(_to_int "$(grep -E '^[0-9]' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f"$col_idx")")
|
||||
|
||||
local periods="60 300 3600 86400 604800 2592000 31536000"
|
||||
local results=""
|
||||
|
||||
for secs in $periods; do
|
||||
local target_ts=$((now - secs))
|
||||
# Find closest entry at or after target timestamp (skip header)
|
||||
local old_val
|
||||
old_val=$(_to_int "$(awk -F, -v ts="$target_ts" '$1 ~ /^[0-9]/ && $1 <= ts' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f"$col_idx")")
|
||||
|
||||
local diff
|
||||
diff=$(_safe_diff "$bytes_now" "$old_val")
|
||||
local rate=$(( secs > 0 ? diff / secs : 0 ))
|
||||
|
||||
local bytes_fmt rate_fmt
|
||||
bytes_fmt=$(format_bytes "$diff")
|
||||
rate_fmt=$(format_rate "$rate")
|
||||
|
||||
if [ -z "$results" ]; then
|
||||
results="${bytes_fmt}|${rate_fmt}"
|
||||
else
|
||||
results="${results}|${bytes_fmt}|${rate_fmt}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$results"
|
||||
}
|
||||
|
||||
# Main display function for traffic statistics
|
||||
show_traffic_stats() {
|
||||
# Ensure stats are collected
|
||||
stats_collect
|
||||
|
||||
# Get current counters
|
||||
local current_json=$(stats_read_current)
|
||||
local proxy_pkts=$(json_get "$current_json" "proxy_pkts")
|
||||
local site_pkts=$(json_get "$current_json" "site_pkts")
|
||||
|
||||
# Calculate rates for proxy
|
||||
local proxy_rates=$(stats_calculate_rates "proxy")
|
||||
IFS='|' read -r p1m p1mr p5m p5mr p60m p60mr p1d p1dr p7d p7dr p30d p30dr p365d p365dr <<< "$proxy_rates"
|
||||
|
||||
# Calculate rates for site
|
||||
local site_rates=$(stats_calculate_rates "site")
|
||||
IFS='|' read -r s1m s1mr s5m s5mr s60m s60mr s1d s1dr s7d s7dr s30d s30dr s365d s365dr <<< "$site_rates"
|
||||
|
||||
# Display proxy stats
|
||||
{
|
||||
echo ""
|
||||
echo -e "${BLUE} Proxy (telemt, порт 443):${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$p1m" "$p1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$p5m" "$p5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$p60m" "$p60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$p1d" "$p1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$p7d" "$p7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$p30d" "$p30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$p365d" "$p365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " Пакетов: %d\n\n" "$proxy_pkts"
|
||||
|
||||
echo -e "${BLUE} Сайт (nginx, порт 8443):${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$s1m" "$s1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$s5m" "$s5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$s60m" "$s60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$s1d" "$s1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$s7d" "$s7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$s30d" "$s30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$s365d" "$s365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " Пакетов: %d\n" "$site_pkts"
|
||||
echo ""
|
||||
} >&2
|
||||
}
|
||||
|
||||
# Clean up history older than 365 days
|
||||
stats_cleanup_history() {
|
||||
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local now=$(date +%s)
|
||||
local ts_365d=$((now - 31536000))
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
# Keep header + entries from last 365 days
|
||||
{
|
||||
head -1 "$HISTORY_FILE"
|
||||
awk -F, -v ts="$ts_365d" '$1 >= ts' "$HISTORY_FILE" | tail -n +2
|
||||
} > "$temp_file" 2>/dev/null
|
||||
|
||||
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# Toggle stats collection on/off
|
||||
toggle_stats() {
|
||||
local current_state="false"
|
||||
|
||||
# Read current state from config
|
||||
if [[ -f "$CONFIG_FILE" ]] && command -v jq &>/dev/null; then
|
||||
current_state=$(jq -r '.stats_enabled // false' "$CONFIG_FILE" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Toggle
|
||||
if [[ "$current_state" == "true" ]]; then
|
||||
# Disable stats
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
if command -v jq &>/dev/null; then
|
||||
jq '.stats_enabled = false' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" 2>/dev/null
|
||||
mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove iptables rules
|
||||
iptables -D INPUT -j GOTELEGRAM_STATS 2>/dev/null
|
||||
iptables -F GOTELEGRAM_STATS 2>/dev/null
|
||||
iptables -X GOTELEGRAM_STATS 2>/dev/null
|
||||
|
||||
# Clean up directories
|
||||
rm -rf "$STATS_DIR" 2>/dev/null
|
||||
|
||||
echo "Сбор статистики ОТКЛЮЧЕН" >&2
|
||||
else
|
||||
# Enable stats
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
if command -v jq &>/dev/null; then
|
||||
jq '.stats_enabled = true' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" 2>/dev/null
|
||||
mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize stats collection
|
||||
stats_init
|
||||
|
||||
echo "Сбор статистики ВКЛЮЧЕН" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Install systemd service for stats collection
|
||||
install_stats_collector() {
|
||||
local service_file="/etc/systemd/system/gotelegram-stats.service"
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Требуется root для установки сервиса" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get script directory (resolve symlinks)
|
||||
local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||
local lib_dir=$(dirname "$script_dir")
|
||||
|
||||
# Create systemd service file
|
||||
cat > "$service_file" <<'EOF'
|
||||
[Unit]
|
||||
Description=GoTelegram Traffic Stats Collector
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/bin/bash -c 'source /opt/gotelegram/lib/common.sh; source /opt/gotelegram/lib/stats.sh; stats_init; while true; do stats_collect; sleep 1; done'
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
chmod 644 "$service_file"
|
||||
systemctl daemon-reload
|
||||
systemctl enable gotelegram-stats.service
|
||||
systemctl start gotelegram-stats.service
|
||||
|
||||
echo "Сервис gotelegram-stats установлен и запущен" >&2
|
||||
}
|
||||
|
||||
# Remove stats collector service
|
||||
remove_stats_collector() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Требуется root для удаления сервиса" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
systemctl stop gotelegram-stats.service 2>/dev/null
|
||||
systemctl disable gotelegram-stats.service 2>/dev/null
|
||||
rm -f /etc/systemd/system/gotelegram-stats.service
|
||||
systemctl daemon-reload
|
||||
|
||||
# Remove iptables rules
|
||||
iptables -D INPUT -j GOTELEGRAM_STATS 2>/dev/null
|
||||
iptables -F GOTELEGRAM_STATS 2>/dev/null
|
||||
iptables -X GOTELEGRAM_STATS 2>/dev/null
|
||||
|
||||
# Clean up directories and files
|
||||
rm -rf "$STATS_DIR" 2>/dev/null
|
||||
rm -f "$HISTORY_FILE" 2>/dev/null
|
||||
|
||||
echo "Сервис статистики удалён" >&2
|
||||
}
|
||||
|
||||
# Export functions for external use
|
||||
export -f stats_init stats_collect stats_read_current stats_calculate_rates
|
||||
export -f show_traffic_stats format_bytes format_rate toggle_stats
|
||||
export -f stats_cleanup_history install_stats_collector remove_stats_collector
|
||||
export -f json_get
|
||||
@@ -1,20 +1,29 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Каталог шаблонов сайтов
|
||||
# Выбор из ~200 шаблонов, превью-ссылки, скачивание через git sparse-checkout
|
||||
# GoTelegram v2.4 — website templates catalog
|
||||
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
|
||||
# + custom git URL templates (user-supplied public repos)
|
||||
|
||||
CATALOG_FILE="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")/templates_catalog.json"
|
||||
TEMPLATES_CACHE="/tmp/gotelegram_templates"
|
||||
|
||||
# ── Загрузка каталога ────────────────────────────────────────────────────────
|
||||
# Custom git template limits
|
||||
CUSTOM_GIT_MAX_SIZE_MB=100
|
||||
CUSTOM_GIT_CLONE_TIMEOUT=90
|
||||
|
||||
# ── Catalog loading ────────────────────────────────────────────────────
|
||||
load_catalog() {
|
||||
if [ ! -f "$CATALOG_FILE" ]; then
|
||||
log_error "Каталог шаблонов не найден: $CATALOG_FILE"
|
||||
if type tf &>/dev/null; then
|
||||
log_error "$(tf templates_catalog_not_found "$CATALOG_FILE")"
|
||||
else
|
||||
log_error "Templates catalog not found: $CATALOG_FILE"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Категории ────────────────────────────────────────────────────────────────
|
||||
# ── Categories ─────────────────────────────────────────────────────────
|
||||
get_categories() {
|
||||
jq -r '.categories[] | "\(.id)|\(.name)|\(.icon)|\(.templates | length)"' "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
@@ -24,13 +33,13 @@ get_category_name() {
|
||||
jq -r ".categories[] | select(.id == \"$cat_id\") | .name" "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Шаблоны по категории ────────────────────────────────────────────────────
|
||||
# ── Templates in a category ────────────────────────────────────────────
|
||||
get_templates_by_category() {
|
||||
local cat_id="$1"
|
||||
jq -r ".categories[] | select(.id == \"$cat_id\") | .templates[] | \"\(.id)|\(.name)|\(.source)|\(.preview_url)\"" "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Информация о шаблоне ────────────────────────────────────────────────────
|
||||
# ── Template info ──────────────────────────────────────────────────────
|
||||
get_template_info() {
|
||||
local tpl_id="$1"
|
||||
jq ".categories[].templates[] | select(.id == \"$tpl_id\")" "$CATALOG_FILE" 2>/dev/null
|
||||
@@ -42,16 +51,19 @@ get_template_field() {
|
||||
jq -r ".categories[].templates[] | select(.id == \"$tpl_id\") | .$field" "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Интерактивный выбор категории ────────────────────────────────────────────
|
||||
# ── Interactive category picker (returns category id or special __custom_git__/__random__) ──
|
||||
select_category() {
|
||||
load_catalog || return 1
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📂 Категории шаблонов сайтов:${NC}" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t templates_categories)${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
|
||||
# First item: custom git URL template
|
||||
printf " ${CYAN}%2d)${NC} ${GREEN}%s${NC}\n" 1 "$(t templates_custom_git)" >&2
|
||||
|
||||
local cats=()
|
||||
local i=1
|
||||
local i=2
|
||||
while IFS='|' read -r id name icon count; do
|
||||
[ "$count" -eq 0 ] && continue
|
||||
local emoji
|
||||
@@ -67,40 +79,52 @@ select_category() {
|
||||
chart-bar) emoji="🔧" ;;
|
||||
*) emoji="📄" ;;
|
||||
esac
|
||||
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}(%d шаблонов)${NC}\n" "$i" "$name" "$count" >&2
|
||||
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}$(tf templates_count_fmt "$count")${NC}\n" "$i" "$name" >&2
|
||||
cats+=("$id")
|
||||
((i++))
|
||||
done < <(get_categories)
|
||||
|
||||
printf " ${CYAN}%2d)${NC} 🎲 Случайный шаблон\n" "$i" >&2
|
||||
printf " ${CYAN}%2d)${NC} %s\n" "$i" "$(t templates_random)" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
echo -ne " ${WHITE}Выбор:${NC} " >&2
|
||||
echo -ne " ${WHITE}$(t choose):${NC} " >&2
|
||||
read -r choice
|
||||
|
||||
# Случайный
|
||||
if [ "$choice" -eq "$i" ] 2>/dev/null; then
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
|
||||
log_error "$(t invalid_choice)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Custom git URL
|
||||
if [ "$choice" -eq 1 ]; then
|
||||
echo "__custom_git__"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Random
|
||||
if [ "$choice" -eq "$i" ]; then
|
||||
local random_cat="${cats[$((RANDOM % ${#cats[@]}))]}"
|
||||
echo "$random_cat"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
|
||||
echo "${cats[$((choice-1))]}"
|
||||
# Regular category (offset by 1 because item 1 is custom git)
|
||||
if [ "$choice" -ge 2 ] && [ "$choice" -lt "$i" ]; then
|
||||
echo "${cats[$((choice-2))]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Неверный выбор"
|
||||
log_error "$(t invalid_choice)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Интерактивный выбор шаблона ──────────────────────────────────────────────
|
||||
# ── Interactive template picker ────────────────────────────────────────
|
||||
select_template() {
|
||||
local cat_id="$1"
|
||||
local cat_name
|
||||
cat_name=$(get_category_name "$cat_id")
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📋 $cat_name — доступные шаблоны:${NC}" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(tf templates_list "$cat_name")${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
|
||||
local tpls=()
|
||||
@@ -112,29 +136,29 @@ select_template() {
|
||||
done < <(get_templates_by_category "$cat_id")
|
||||
|
||||
if [ ${#tpls[@]} -eq 0 ]; then
|
||||
log_info "В этой категории нет шаблонов"
|
||||
log_info "$(t templates_cat_empty)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -ne " ${WHITE}Выбор (1-$((i-1))):${NC} " >&2
|
||||
echo -ne " ${WHITE}$(t choose) (1-$((i-1))):${NC} " >&2
|
||||
read -r choice
|
||||
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
|
||||
local selected_id="${tpls[$((choice-1))]}"
|
||||
|
||||
# Показываем превью
|
||||
show_template_preview "$selected_id"
|
||||
# Show preview
|
||||
show_template_preview "$selected_id" || return 1
|
||||
|
||||
echo "$selected_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Неверный выбор"
|
||||
log_error "$(t invalid_choice)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Показ превью шаблона ────────────────────────────────────────────────────
|
||||
# ── Template preview ───────────────────────────────────────────────────
|
||||
show_template_preview() {
|
||||
local tpl_id="$1"
|
||||
local info
|
||||
@@ -148,36 +172,36 @@ show_template_preview() {
|
||||
description=$(echo "$info" | jq -r '.description // "—"')
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}🔍 Превью шаблона:${NC}" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t templates_preview_title)${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
echo -e " ${WHITE}Название:${NC} $name" >&2
|
||||
echo -e " ${WHITE}Источник:${NC} $source" >&2
|
||||
echo -e " ${WHITE}Описание:${NC} $description" >&2
|
||||
echo -e " ${WHITE}$(t templates_name)${NC} $name" >&2
|
||||
echo -e " ${WHITE}$(t templates_source)${NC} $source" >&2
|
||||
echo -e " ${WHITE}$(t templates_description)${NC} $description" >&2
|
||||
|
||||
if [ -n "$preview_url" ]; then
|
||||
echo "" >&2
|
||||
echo -e " ${GREEN}👁 Превью:${NC} ${CYAN}${preview_url}${NC}" >&2
|
||||
echo -e " ${DIM}Откройте ссылку в браузере для просмотра шаблона${NC}" >&2
|
||||
echo -e " ${GREEN}$(t templates_preview)${NC} ${CYAN}${preview_url}${NC}" >&2
|
||||
echo -e " ${DIM}$(t templates_preview_hint)${NC}" >&2
|
||||
fi
|
||||
|
||||
if [ -n "$repo_url" ]; then
|
||||
echo -e " ${DIM}📦 Репо: ${repo_url}${NC}" >&2
|
||||
echo -e " ${DIM}$(t templates_repo) ${repo_url}${NC}" >&2
|
||||
fi
|
||||
|
||||
# Благодарность автору
|
||||
# Thanks
|
||||
echo "" >&2
|
||||
echo -e " ${MAGENTA}💜 Спасибо авторам ${source} за открытый код!${NC}" >&2
|
||||
echo -e " ${MAGENTA}$(tf templates_thanks "$source")${NC}" >&2
|
||||
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if ! confirm "Установить этот шаблон?"; then
|
||||
if ! confirm "$(t templates_install_this)"; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Скачивание шаблона ───────────────────────────────────────────────────────
|
||||
# ── Template download (from catalog) ───────────────────────────────────
|
||||
download_template() {
|
||||
local tpl_id="$1"
|
||||
local output_dir="${2:-$TEMPLATES_CACHE}"
|
||||
@@ -194,9 +218,9 @@ download_template() {
|
||||
rm -rf "$clone_dir"
|
||||
mkdir -p "$clone_dir"
|
||||
|
||||
log_info "Скачивание шаблона \"$name\"..."
|
||||
log_info "$(tf templates_downloading "$name")"
|
||||
|
||||
# Для HTML5 UP — отдельный репо с папками
|
||||
# HTML5 UP — one repo with folders
|
||||
if [ "$source" = "html5up" ]; then
|
||||
local tmp_clone="/tmp/html5up_clone_$$"
|
||||
rm -rf "$tmp_clone"
|
||||
@@ -204,7 +228,7 @@ download_template() {
|
||||
# Sparse checkout
|
||||
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
# Fallback: полный clone
|
||||
# Fallback: full clone
|
||||
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
|
||||
fi
|
||||
|
||||
@@ -217,7 +241,7 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$tmp_clone"
|
||||
|
||||
# Для learning-zone — один большой репо
|
||||
# learning-zone — one big repo
|
||||
elif [ "$source" = "learning-zone" ]; then
|
||||
local tmp_clone="/tmp/lz_clone_$$"
|
||||
rm -rf "$tmp_clone"
|
||||
@@ -236,14 +260,14 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$tmp_clone"
|
||||
|
||||
# Для StartBootstrap — каждый шаблон в своём репо
|
||||
# StartBootstrap — each template in its own repo
|
||||
elif [ "$source" = "startbootstrap" ]; then
|
||||
local sb_tmp="/tmp/sb_clone_$$"
|
||||
rm -rf "$sb_tmp"
|
||||
git clone --depth 1 "$repo_url" "$sb_tmp" 2>/dev/null
|
||||
if [ -d "$sb_tmp" ]; then
|
||||
rm -rf "$sb_tmp/.git"
|
||||
# StartBootstrap хранит production-файлы в dist/
|
||||
# StartBootstrap stores production files in dist/
|
||||
if [ -f "$sb_tmp/dist/index.html" ]; then
|
||||
cp -r "$sb_tmp/dist/"* "$clone_dir/"
|
||||
elif [ -f "$sb_tmp/index.html" ]; then
|
||||
@@ -260,7 +284,7 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$sb_tmp"
|
||||
|
||||
# Для ThemeWagon / ColorlibHQ — каждый шаблон в отдельном репо
|
||||
# ThemeWagon / ColorlibHQ — each template in its own repo
|
||||
elif [ "$source" = "themewagon" ] || [ "$source" = "colorlib" ]; then
|
||||
local tw_tmp="/tmp/tw_clone_$$"
|
||||
rm -rf "$tw_tmp"
|
||||
@@ -283,7 +307,7 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$tw_tmp"
|
||||
|
||||
# Для dawidolko — один большой репо с папками (как learning-zone)
|
||||
# dawidolko — one big repo with folders (similar to learning-zone)
|
||||
elif [ "$source" = "dawidolko" ]; then
|
||||
local tmp_clone="/tmp/dw_clone_$$"
|
||||
rm -rf "$tmp_clone"
|
||||
@@ -301,13 +325,13 @@ download_template() {
|
||||
rm -rf "$tmp_clone"
|
||||
fi
|
||||
|
||||
# Проверяем результат
|
||||
# Check result
|
||||
if [ -f "$clone_dir/index.html" ]; then
|
||||
log_success "Шаблон \"$name\" скачан"
|
||||
log_success "$(tf templates_downloaded "$name")"
|
||||
echo "$clone_dir"
|
||||
return 0
|
||||
else
|
||||
# fallback: ищем index.html в подпапках (нестандартная структура)
|
||||
# fallback: find index.html in subfolders (non-standard structure)
|
||||
local fallback_index
|
||||
fallback_index=$(find "$clone_dir" -name "index.html" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$fallback_index" ]; then
|
||||
@@ -315,33 +339,253 @@ download_template() {
|
||||
fallback_dir=$(dirname "$fallback_index")
|
||||
if [ "$fallback_dir" != "$clone_dir" ]; then
|
||||
cp -r "$fallback_dir/"* "$clone_dir/"
|
||||
log_success "Шаблон \"$name\" скачан (из подпапки)"
|
||||
log_success "$(tf templates_downloaded_subfolder "$name")"
|
||||
echo "$clone_dir"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
log_error "Шаблон не содержит index.html"
|
||||
log_dim "Путь: $clone_dir"
|
||||
log_error "$(t templates_no_index)"
|
||||
log_dim "$(tf templates_path "$clone_dir")"
|
||||
ls -la "$clone_dir" 2>/dev/null >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Полный интерактивный процесс выбора ──────────────────────────────────────
|
||||
# ── Custom git URL helpers ─────────────────────────────────────────────
|
||||
|
||||
# Validate a user-supplied git URL
|
||||
# Accepts: https://host/path[.git][@branch]
|
||||
# Rejects: ssh://, git://, file://, absolute file paths
|
||||
_validate_custom_git_url() {
|
||||
local url="$1"
|
||||
# Must begin with https://
|
||||
[[ "$url" =~ ^https:// ]] || return 1
|
||||
# Reject shell metacharacters that could be exploited
|
||||
[[ "$url" =~ [[:space:]\;\`\$\(\)\<\>\|\\\&] ]] && return 1
|
||||
# Reasonable length limit
|
||||
[ "${#url}" -gt 512 ] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# Parse URL → sets CUSTOM_GIT_CLEAN and CUSTOM_GIT_BRANCH globals
|
||||
_parse_custom_git_url() {
|
||||
local url="$1"
|
||||
CUSTOM_GIT_CLEAN=""
|
||||
CUSTOM_GIT_BRANCH=""
|
||||
# Handle trailing @branch
|
||||
if [[ "$url" =~ ^(https://[^@]+)@([A-Za-z0-9._/-]+)$ ]]; then
|
||||
CUSTOM_GIT_CLEAN="${BASH_REMATCH[1]}"
|
||||
CUSTOM_GIT_BRANCH="${BASH_REMATCH[2]}"
|
||||
else
|
||||
CUSTOM_GIT_CLEAN="$url"
|
||||
fi
|
||||
# Strip trailing slash
|
||||
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN%/}"
|
||||
# Append .git if missing (works better with git clone on some hosts)
|
||||
if [[ ! "$CUSTOM_GIT_CLEAN" =~ \.git$ ]]; then
|
||||
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN}.git"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check repo size (in MB) by inspecting cloned directory
|
||||
_clone_dir_size_mb() {
|
||||
local dir="$1"
|
||||
du -sm "$dir" 2>/dev/null | awk '{print $1}'
|
||||
}
|
||||
|
||||
# ── Show detailed help for custom git template ─────────────────────────
|
||||
show_custom_git_help() {
|
||||
local line
|
||||
line=$(printf '─%.0s' $(seq 1 60))
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${GREEN}$(t custom_git_title)${NC}" >&2
|
||||
echo -e " ${DIM}${line}${NC}" >&2
|
||||
echo -e " $(t custom_git_help_1)" >&2
|
||||
echo -e " $(t custom_git_help_2)" >&2
|
||||
echo -e " $(t custom_git_help_3)" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_formats)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_github)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_gitlab)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_gitext)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_branch)${NC}" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_auto_detect)${NC}" >&2
|
||||
echo -e " $(t custom_git_auto_1)" >&2
|
||||
echo -e " $(t custom_git_auto_2)" >&2
|
||||
echo -e " $(t custom_git_auto_3)" >&2
|
||||
echo -e " $(t custom_git_auto_4)" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_requirements)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_1)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_2)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_3)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_4)${NC}" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_examples)${NC}" >&2
|
||||
echo -e " ${DIM}$(t custom_git_ex_1)${NC}" >&2
|
||||
echo -e " ${DIM}$(t custom_git_ex_2)${NC}" >&2
|
||||
echo -e " ${DIM}${line}${NC}" >&2
|
||||
echo "" >&2
|
||||
}
|
||||
|
||||
# ── Download a custom git template ─────────────────────────────────────
|
||||
# Prompts user for a URL (unless passed), clones, detects index.html,
|
||||
# copies result into $output_dir/custom_<hash>, echoes the final path.
|
||||
download_custom_git_template() {
|
||||
local url="${1:-}"
|
||||
local output_dir="${2:-$TEMPLATES_CACHE}"
|
||||
|
||||
show_custom_git_help
|
||||
|
||||
if [ -z "$url" ]; then
|
||||
echo -ne " ${WHITE}$(t custom_git_enter_url)${NC} " >&2
|
||||
read -r url
|
||||
url=$(echo "$url" | tr -d '\r\n[:space:]')
|
||||
fi
|
||||
|
||||
if [ -z "$url" ]; then
|
||||
log_error "$(t custom_git_empty)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _validate_custom_git_url "$url"; then
|
||||
log_error "$(t custom_git_bad_url)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_parse_custom_git_url "$url"
|
||||
local clean_url="$CUSTOM_GIT_CLEAN"
|
||||
local branch="$CUSTOM_GIT_BRANCH"
|
||||
|
||||
# Stable-ish directory name from a hash of the original URL
|
||||
local hash
|
||||
hash=$(echo -n "$url" | md5sum 2>/dev/null | awk '{print $1}' | head -c 10)
|
||||
[ -z "$hash" ] && hash=$(date +%s)
|
||||
local tpl_id="custom_${hash}"
|
||||
local clone_dir="$output_dir/${tpl_id}"
|
||||
local tmp_clone="/tmp/custom_git_clone_$$"
|
||||
|
||||
rm -rf "$clone_dir" "$tmp_clone"
|
||||
mkdir -p "$clone_dir"
|
||||
|
||||
log_info "$(t custom_git_cloning)"
|
||||
|
||||
# Clone with timeout so a hung server can't freeze the installer
|
||||
local clone_status=0
|
||||
local git_args=("clone" "--depth" "1")
|
||||
[ -n "$branch" ] && git_args+=("--branch" "$branch")
|
||||
git_args+=("$clean_url" "$tmp_clone")
|
||||
|
||||
if command -v timeout &>/dev/null; then
|
||||
timeout "$CUSTOM_GIT_CLONE_TIMEOUT" git "${git_args[@]}" 2>/tmp/custom_git_err_$$
|
||||
clone_status=$?
|
||||
else
|
||||
git "${git_args[@]}" 2>/tmp/custom_git_err_$$
|
||||
clone_status=$?
|
||||
fi
|
||||
|
||||
if [ $clone_status -ne 0 ] || [ ! -d "$tmp_clone" ]; then
|
||||
local err_msg
|
||||
err_msg=$(head -3 "/tmp/custom_git_err_$$" 2>/dev/null | tr '\n' ' ')
|
||||
rm -f "/tmp/custom_git_err_$$"
|
||||
rm -rf "$tmp_clone" "$clone_dir"
|
||||
log_error "$(tf custom_git_clone_failed "${err_msg:-$clone_status}")"
|
||||
return 1
|
||||
fi
|
||||
rm -f "/tmp/custom_git_err_$$"
|
||||
|
||||
# Drop .git before measuring size (we only care about payload)
|
||||
rm -rf "$tmp_clone/.git"
|
||||
|
||||
# Size guard
|
||||
local size_mb
|
||||
size_mb=$(_clone_dir_size_mb "$tmp_clone")
|
||||
if [ -n "$size_mb" ] && [ "$size_mb" -gt "$CUSTOM_GIT_MAX_SIZE_MB" ]; then
|
||||
rm -rf "$tmp_clone" "$clone_dir"
|
||||
log_error "$(tf custom_git_too_big "${size_mb}MB")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "$(t custom_git_scanning)"
|
||||
|
||||
# Priority list of common static-site output folders
|
||||
local candidates=("" "dist" "public" "build" "_site" "site" "docs" "out" "www")
|
||||
local found_dir=""
|
||||
for sub in "${candidates[@]}"; do
|
||||
local try_dir="$tmp_clone"
|
||||
[ -n "$sub" ] && try_dir="$tmp_clone/$sub"
|
||||
if [ -f "$try_dir/index.html" ]; then
|
||||
found_dir="$try_dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Fallback: search for any index.html in the repo (shallow depth first)
|
||||
if [ -z "$found_dir" ]; then
|
||||
local fallback_index
|
||||
fallback_index=$(find "$tmp_clone" -maxdepth 4 -name "index.html" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$fallback_index" ]; then
|
||||
found_dir=$(dirname "$fallback_index")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$found_dir" ] || [ ! -f "$found_dir/index.html" ]; then
|
||||
rm -rf "$tmp_clone" "$clone_dir"
|
||||
log_error "$(t custom_git_no_index)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Show what we found (human-friendly relative path)
|
||||
local rel_path="${found_dir#$tmp_clone}"
|
||||
rel_path="${rel_path#/}"
|
||||
[ -z "$rel_path" ] && rel_path="(root)"
|
||||
log_dim "$(tf custom_git_found_at "$rel_path")"
|
||||
|
||||
# Copy the detected directory as the new template
|
||||
cp -r "$found_dir"/* "$clone_dir/" 2>/dev/null
|
||||
cp -r "$found_dir"/.[!.]* "$clone_dir/" 2>/dev/null
|
||||
|
||||
rm -rf "$tmp_clone"
|
||||
|
||||
if [ ! -f "$clone_dir/index.html" ]; then
|
||||
rm -rf "$clone_dir"
|
||||
log_error "$(t custom_git_no_index)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Remember the URL so users can see what template they used
|
||||
echo "$url" > "$clone_dir/.custom_git_source" 2>/dev/null
|
||||
|
||||
log_success "$(tf custom_git_installed "$url")"
|
||||
echo "$clone_dir"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Full interactive template selection ───────────────────────────────
|
||||
interactive_template_selection() {
|
||||
load_catalog || return 1
|
||||
|
||||
# Выбор категории
|
||||
# Category selection
|
||||
local cat_id
|
||||
cat_id=$(select_category)
|
||||
[ $? -ne 0 ] && return 1
|
||||
|
||||
# Выбор шаблона
|
||||
# Custom git URL path
|
||||
if [ "$cat_id" = "__custom_git__" ]; then
|
||||
local template_dir
|
||||
template_dir=$(download_custom_git_template)
|
||||
[ $? -ne 0 ] && return 1
|
||||
echo "$template_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Template selection
|
||||
local tpl_id
|
||||
tpl_id=$(select_template "$cat_id")
|
||||
[ $? -ne 0 ] && return 1
|
||||
|
||||
# Скачивание
|
||||
# Download
|
||||
local template_dir
|
||||
template_dir=$(download_template "$tpl_id")
|
||||
[ $? -ne 0 ] && return 1
|
||||
|
||||
Reference in New Issue
Block a user