v2.2.1: unified menu with bot management, grouped sections, telemt download fix, QR cleanup, version bump

This commit is contained in:
anten-ka
2026-04-06 21:40:34 +03:00
parent fe9e5fa019
commit fee25f191e
9 changed files with 2306 additions and 1907 deletions

View File

@@ -1,70 +1,147 @@
# GoTelegram MTProxy Bot # GoTelegram v2.2 Bot
Telegram-бот для управления MTProxy на сервере — те же функции, что и у CLI `gotelegram`, но через бота. Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers.
## Команды ## Features
| Команда | Описание | - **Complete CLI Feature Parity** - All menu items from CLI version
|--------|----------| - Install (Quick/Stealth modes)
| `/start`, `/help` | Справка | - Status monitoring
| `/install` | Установить или обновить прокси (выбор домена и порта) | - Proxy link generation
| `/status` | Статус и данные подключения (IP, порт, secret, ссылка) | - Share with QR codes
| `/link` | Только ссылка `tg://proxy` | - Service restart
| `/restart` | Перезапустить контейнер | - Logs viewing
| `/logs` | Последние логи контейнера | - Mode/template changes
| `/remove` | Удалить прокси | - Backup/restore
| `/promo` | Промо хостинга | - telemt updates
- Website/SSL management
## Установка на сервер - Remove installation
- Promotional links
### Публичный репозиторий (одной командой)
- **Template Browsing** - Browse categories → templates → preview → install
```bash - **V1 Migration** - Detects old mtg Docker container and offers migration
curl -sL https://raw.githubusercontent.com/anten-ka/gotelegram_pro/main/install_gotelegram_bot.sh -o /tmp/install_gotelegram_bot.sh && sudo bash /tmp/install_gotelegram_bot.sh - **Access Control** - ALLOWED_IDS from .env
``` - **Async/Await** - Full async support via python-telegram-bot v21+
- **Inline Keyboards** - Modern UI with callback-based navigation
При установке скрипт запросит **BOT_TOKEN** (получить у [@BotFather](https://t.me/BotFather)). - **Shell Integration** - Executes system commands via asyncio subprocess
- **Error Handling** - Production-ready error handling
### Закрытый репозиторий (установка по ключу)
## Installation
Для **приватного** репо используется клонирование по **SSH-ключу** или по **токену (PAT)**. Подробно: **[INSTALL_PRIVATE.md](../INSTALL_PRIVATE.md)** в корне репозитория.
### Prerequisites
Кратко:
- **По SSH:** скопируйте `bootstrap_install.sh` на сервер, затем - Python 3.8+
`GIT_REPO_SSH=git@github.com:USER/REPO.git sudo bash bootstrap_install.sh` - Linux system with systemd
- **По токену:** - telemt installed and running
`GITHUB_TOKEN=ghp_xxx GIT_REPO_HTTPS=https://github.com/USER/REPO.git sudo -E bash bootstrap_install.sh` - Telegram Bot Token from @BotFather
- Или клонируйте репо вручную и запустите:
`sudo ./install_gotelegram_bot.sh` ### Setup
### Локально (файлы уже рядом со скриптом) 1. Install dependencies:
```bash
```bash pip install -r requirements.txt
sudo ./install_gotelegram_bot.sh ```
```
2. Create .env file:
## Конфигурация ```bash
cp config.example.env .env
Файл: `/opt/gotelegram-bot/.env` # Edit .env and set your BOT_TOKEN
nano .env
- **BOT_TOKEN** — токен от @BotFather (обязательно). ```
- **ALLOWED_IDS** — опционально. Список ID пользователей через запятую; если не задан, бот доступен всем.
3. (Optional) Restrict access to specific users:
После изменения `.env` перезапуск сервиса: ```bash
# Edit .env and uncomment ALLOWED_IDS
```bash # ALLOWED_IDS=123456789,987654321
sudo systemctl restart gotelegram-bot ```
```
### Running the Bot
## Требования на сервере
```bash
- Linux (systemd), Docker, Python 3. python3 bot.py
- Перед использованием бота на сервере должен быть установлен Docker (бот сам поднимает контейнер `nineseconds/mtg:2` по команде `/install`). ```
## Управление сервисом For systemd service:
```bash ```bash
sudo systemctl status gotelegram-bot [Unit]
sudo systemctl restart gotelegram-bot Description=GoTelegram Bot
journalctl -u gotelegram-bot -f After=network.target
```
[Service]
Type=simple
User=gotelegram
WorkingDirectory=/opt/gotelegram/bot
ExecStart=/usr/bin/python3 /opt/gotelegram/bot/bot.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
## Configuration
### .env Variables
- `BOT_TOKEN` - Telegram bot token (required)
- `ALLOWED_IDS` - Comma-separated user IDs (optional, all users allowed if empty)
### System Paths
- `GOTELEGRAM_CONFIG` - `/opt/gotelegram/config.json`
- `TELEMT_CONFIG` - `/etc/telemt/config.toml`
- `TELEMT_SERVICE` - `telemt` (systemd service name)
- `WEBSITE_ROOT` - `/var/www/gotelegram-site`
- `BACKUP_DIR` - `/opt/gotelegram/backups`
- `TEMPLATES_CATALOG` - `/opt/gotelegram/templates_catalog.json`
## Architecture
### Single File Design
All functionality in one `bot.py` for simplicity and ease of deployment.
### Command Handlers
- `/start` - Main menu
- `/help` - Help text
- `/status` - Quick status
- `/logs` - Recent logs
### Callback Handlers
Organized by feature:
- Installation (quick/stealth modes)
- Status monitoring
- Backup/restore
- SSL management
- Updates
- Removal
### Shell Integration
Async subprocess wrapper:
```python
code, stdout, stderr = await sh("command", "arg1", "arg2")
```
## Callback Data Convention
- `menu_*` - Menu items
- `install_mode_*` - Install options
- `quick_dom_*` - Domain selection
- `stealth_cat_*` - Template categories
- `stealth_tpl_*` - Template selection
- `stealth_confirm_*` - Confirm installation
- `backup_*` - Backup operations
- `ssl_*` - SSL operations
- `restore_backup_*` - Restore operations
## Credits
- **telemt** - MTProxy engine foundation
- **HTML5UP** - Beautiful web templates
- **Learning Zone** - Educational resources
- **Start Bootstrap** - Bootstrap framework
- **Community** - Your feedback and support
## License
GoTelegram v2.2 - Open source community project

View File

@@ -6,4 +6,4 @@ BOT_TOKEN=your_bot_token_from_@BotFather
# Comma-separated list of allowed Telegram user IDs # Comma-separated list of allowed Telegram user IDs
# Leave empty to allow all users # Leave empty to allow all users
# ALLOWED_IDS=123456789,987654321 # ALLOWED_IDS=123456789,987654321

View File

@@ -1,3 +1,3 @@
python-telegram-bot>=21.0 python-telegram-bot>=21.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
toml>=0.10.2 toml>=0.10.2

1242
install.sh

File diff suppressed because it is too large Load Diff

View File

@@ -1,132 +1,132 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.2 — Установка Telegram-бота # GoTelegram v2.2.1 — Установка Telegram-бота
# Создаёт venv, ставит зависимости, настраивает systemd # Создаёт venv, ставит зависимости, настраивает systemd
set -e set -e
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
NC='\033[0m' NC='\033[0m'
BOT_DIR="/opt/gotelegram-bot" BOT_DIR="/opt/gotelegram-bot"
SERVICE_NAME="gotelegram-bot" SERVICE_NAME="gotelegram-bot"
GOTELEGRAM_DIR="/opt/gotelegram" GOTELEGRAM_DIR="/opt/gotelegram"
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Запустите с sudo.${NC}" echo -e "${RED}Запустите с sudo.${NC}"
exit 1 exit 1
fi fi
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}" echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
echo -e "${CYAN}${NC} ${GREEN}GoTelegram v2.2 — Установка бота${NC} ${CYAN}${NC}" echo -e "${CYAN}${NC} ${GREEN}GoTelegram v2.2.1 — Установка бота${NC} ${CYAN}${NC}"
echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}" echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}"
echo "" echo ""
# ── Python ─────────────────────────────────────────────────────────────────── # ── Python ───────────────────────────────────────────────────────────────────
if ! command -v python3 &>/dev/null; then if ! command -v python3 &>/dev/null; then
echo -e "${YELLOW}[*] Установка python3...${NC}" echo -e "${YELLOW}[*] Установка python3...${NC}"
if command -v apt-get &>/dev/null; then if command -v apt-get &>/dev/null; then
apt-get update -qq && apt-get install -y -qq python3 python3-pip python3-venv apt-get update -qq && apt-get install -y -qq python3 python3-pip python3-venv
elif command -v dnf &>/dev/null; then elif command -v dnf &>/dev/null; then
dnf install -y -q python3 python3-pip dnf install -y -q python3 python3-pip
elif command -v yum &>/dev/null; then elif command -v yum &>/dev/null; then
yum install -y -q python3 python3-pip yum install -y -q python3 python3-pip
fi fi
fi fi
# ── Каталог бота ───────────────────────────────────────────────────────────── # ── Каталог бота ─────────────────────────────────────────────────────────────
mkdir -p "$BOT_DIR" mkdir -p "$BOT_DIR"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ]; then if [ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ]; then
echo -e "${GREEN}[*] Копирование файлов бота...${NC}" echo -e "${GREEN}[*] Копирование файлов бота...${NC}"
cp "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/" cp "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/"
cp "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/" cp "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/"
[ -f "$SCRIPT_DIR/gotelegram-bot/config.example.env" ] && cp "$SCRIPT_DIR/gotelegram-bot/config.example.env" "$BOT_DIR/" [ -f "$SCRIPT_DIR/gotelegram-bot/config.example.env" ] && cp "$SCRIPT_DIR/gotelegram-bot/config.example.env" "$BOT_DIR/"
else else
echo -e "${RED}Файлы бота не найдены в $SCRIPT_DIR/gotelegram-bot/${NC}" echo -e "${RED}Файлы бота не найдены в $SCRIPT_DIR/gotelegram-bot/${NC}"
exit 1 exit 1
fi fi
# Копируем каталог шаблонов # Копируем каталог шаблонов
if [ -f "$SCRIPT_DIR/templates_catalog.json" ]; then if [ -f "$SCRIPT_DIR/templates_catalog.json" ]; then
mkdir -p "$GOTELEGRAM_DIR" mkdir -p "$GOTELEGRAM_DIR"
cp "$SCRIPT_DIR/templates_catalog.json" "$GOTELEGRAM_DIR/" cp "$SCRIPT_DIR/templates_catalog.json" "$GOTELEGRAM_DIR/"
echo -e "${GREEN}[*] Каталог шаблонов скопирован${NC}" echo -e "${GREEN}[*] Каталог шаблонов скопирован${NC}"
fi fi
# ── Virtual environment ────────────────────────────────────────────────────── # ── Virtual environment ──────────────────────────────────────────────────────
if [ ! -d "$BOT_DIR/venv" ]; then if [ ! -d "$BOT_DIR/venv" ]; then
echo -e "${GREEN}[*] Создание виртуального окружения...${NC}" echo -e "${GREEN}[*] Создание виртуального окружения...${NC}"
python3 -m venv "$BOT_DIR/venv" python3 -m venv "$BOT_DIR/venv"
fi fi
echo -e "${GREEN}[*] Установка зависимостей...${NC}" echo -e "${GREEN}[*] Установка зависимостей...${NC}"
"$BOT_DIR/venv/bin/pip" install -r "$BOT_DIR/requirements.txt" -q "$BOT_DIR/venv/bin/pip" install -r "$BOT_DIR/requirements.txt" -q
# ── Конфигурация ───────────────────────────────────────────────────────────── # ── Конфигурация ─────────────────────────────────────────────────────────────
if [ ! -f "$BOT_DIR/.env" ]; then if [ ! -f "$BOT_DIR/.env" ]; then
echo "" echo ""
echo -e "${YELLOW}Введите BOT_TOKEN от @BotFather:${NC}" echo -e "${YELLOW}Введите BOT_TOKEN от @BotFather:${NC}"
TOKEN="" TOKEN=""
while [ -z "$TOKEN" ]; do while [ -z "$TOKEN" ]; do
read -r TOKEN read -r TOKEN
TOKEN=$(echo "$TOKEN" | tr -d '[:space:]') TOKEN=$(echo "$TOKEN" | tr -d '[:space:]')
[ -z "$TOKEN" ] && echo -e "${RED}Токен не может быть пустым.${NC}" [ -z "$TOKEN" ] && echo -e "${RED}Токен не может быть пустым.${NC}"
done done
echo -ne "${YELLOW}ID администратора (Enter = доступ для всех):${NC} " echo -ne "${YELLOW}ID администратора (Enter = доступ для всех):${NC} "
read -r ADMIN_ID read -r ADMIN_ID
{ {
echo "BOT_TOKEN=$TOKEN" echo "BOT_TOKEN=$TOKEN"
[ -n "$ADMIN_ID" ] && echo "ALLOWED_IDS=$ADMIN_ID" [ -n "$ADMIN_ID" ] && echo "ALLOWED_IDS=$ADMIN_ID"
} > "$BOT_DIR/.env" } > "$BOT_DIR/.env"
chmod 600 "$BOT_DIR/.env" chmod 600 "$BOT_DIR/.env"
echo -e "${GREEN}[*] .env создан${NC}" echo -e "${GREEN}[*] .env создан${NC}"
else else
echo -e "${GREEN}[*] .env уже существует${NC}" echo -e "${GREEN}[*] .env уже существует${NC}"
fi fi
# ── Systemd ────────────────────────────────────────────────────────────────── # ── Systemd ──────────────────────────────────────────────────────────────────
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
[Unit] [Unit]
Description=GoTelegram v2.2 Telegram Bot Description=GoTelegram v2.2.1 Telegram Bot
After=network.target After=network.target
[Service] [Service]
Type=simple Type=simple
WorkingDirectory=$BOT_DIR WorkingDirectory=$BOT_DIR
ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py
Restart=always Restart=always
RestartSec=5 RestartSec=5
Environment=PATH=$BOT_DIR/venv/bin:/usr/bin Environment=PATH=$BOT_DIR/venv/bin:/usr/bin
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
systemctl daemon-reload systemctl daemon-reload
systemctl enable "$SERVICE_NAME" systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME" 2>/dev/null || systemctl start "$SERVICE_NAME" systemctl restart "$SERVICE_NAME" 2>/dev/null || systemctl start "$SERVICE_NAME"
echo "" echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}" echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ Бот установлен и запущен! ║${NC}" echo -e "${GREEN}║ ✅ Бот установлен и запущен! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════╝${NC}" echo -e "${GREEN}╚═══════════════════════════════════════════╝${NC}"
echo "" echo ""
echo -e "Проверка: ${CYAN}systemctl status $SERVICE_NAME${NC}" echo -e "Проверка: ${CYAN}systemctl status $SERVICE_NAME${NC}"
echo -e "Логи: ${CYAN}journalctl -u $SERVICE_NAME -f${NC}" echo -e "Логи: ${CYAN}journalctl -u $SERVICE_NAME -f${NC}"
echo -e "Настройки: ${CYAN}$BOT_DIR/.env${NC}" echo -e "Настройки: ${CYAN}$BOT_DIR/.env${NC}"
echo "" echo ""
# Благодарности # Благодарности
echo -e "${CYAN}─────────────────────────────────────────────${NC}" echo -e "${CYAN}─────────────────────────────────────────────${NC}"
echo -e "💜 Спасибо авторам открытых проектов:" echo -e "💜 Спасибо авторам открытых проектов:"
echo -e " ${CYAN}telemt${NC} — MTProxy engine (Rust)" echo -e " ${CYAN}telemt${NC} — MTProxy engine (Rust)"
echo -e " ${CYAN}HTML5 UP${NC} — шаблоны сайтов (CC BY 3.0)" echo -e " ${CYAN}HTML5 UP${NC} — шаблоны сайтов (CC BY 3.0)"
echo -e " ${CYAN}learning-zone${NC} — 150+ HTML5 шаблонов" echo -e " ${CYAN}learning-zone${NC} — 150+ HTML5 шаблонов"
echo -e " ${CYAN}Start Bootstrap${NC} — Bootstrap шаблоны (MIT)" echo -e " ${CYAN}Start Bootstrap${NC} — Bootstrap шаблоны (MIT)"
echo -e "${CYAN}─────────────────────────────────────────────${NC}" echo -e "${CYAN}─────────────────────────────────────────────${NC}"

View File

@@ -1,341 +1,341 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.2 — Бекап и восстановление конфигурации # GoTelegram v2.2 — Бекап и восстановление конфигурации
# ── Создание бекапа ────────────────────────────────────────────────────────── # ── Создание бекапа ──────────────────────────────────────────────────────────
create_backup() { create_backup() {
local password="$1" local password="$1"
local output_dir="${2:-$BACKUP_DIR}" local output_dir="${2:-$BACKUP_DIR}"
local timestamp local timestamp
timestamp=$(date +%Y%m%d_%H%M%S) timestamp=$(date +%Y%m%d_%H%M%S)
local backup_name="gotelegram_backup_${timestamp}" local backup_name="gotelegram_backup_${timestamp}"
local tmp_dir="/tmp/${backup_name}" local tmp_dir="/tmp/${backup_name}"
mkdir -p "$tmp_dir" "$output_dir" mkdir -p "$tmp_dir" "$output_dir"
# Собираем файлы # Собираем файлы
log_info "Собираю конфигурацию..." log_info "Собираю конфигурацию..."
# telemt конфиг # telemt конфиг
if [ -f "$TELEMT_CONFIG" ]; then if [ -f "$TELEMT_CONFIG" ]; then
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml" cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
fi fi
# GoTelegram конфиг # GoTelegram конфиг
if [ -f "$GOTELEGRAM_CONFIG" ]; then if [ -f "$GOTELEGRAM_CONFIG" ]; then
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json" cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
fi fi
# nginx конфиг (stealth mode) # nginx конфиг (stealth mode)
if [ -f "$NGINX_SITE_CONF" ]; then if [ -f "$NGINX_SITE_CONF" ]; then
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf" cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
fi fi
# SSL сертификаты # SSL сертификаты
local domain local domain
domain=$(config_get domain 2>/dev/null) domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
mkdir -p "$tmp_dir/certs" mkdir -p "$tmp_dir/certs"
cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null
cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null
log_dim "SSL сертификаты включены" log_dim "SSL сертификаты включены"
fi fi
# Шаблон сайта (если есть) # Шаблон сайта (если есть)
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
mkdir -p "$tmp_dir/site" mkdir -p "$tmp_dir/site"
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/"
log_dim "Шаблон сайта включён" log_dim "Шаблон сайта включён"
fi fi
# Метаданные # Метаданные
local ip mode engine local ip mode engine
ip=$(get_server_ip) ip=$(get_server_ip)
mode=$(config_get mode 2>/dev/null || echo "unknown") mode=$(config_get mode 2>/dev/null || echo "unknown")
engine=$(config_get engine 2>/dev/null || echo "telemt") engine=$(config_get engine 2>/dev/null || echo "telemt")
cat > "$tmp_dir/metadata.json" << EOMETA cat > "$tmp_dir/metadata.json" << EOMETA
{ {
"backup_version": "1.0", "backup_version": "1.0",
"gotelegram_version": "$GOTELEGRAM_VERSION", "gotelegram_version": "$GOTELEGRAM_VERSION",
"created_at": "$(date -Iseconds)", "created_at": "$(date -Iseconds)",
"hostname": "$(hostname)", "hostname": "$(hostname)",
"ip": "$ip", "ip": "$ip",
"engine": "$engine", "engine": "$engine",
"mode": "$mode", "mode": "$mode",
"port": $(config_get port 2>/dev/null || echo "443"), "port": $(config_get port 2>/dev/null || echo "443"),
"domain": "$(config_get domain 2>/dev/null)" "domain": "$(config_get domain 2>/dev/null)"
} }
EOMETA EOMETA
# Архивируем # Архивируем
local tar_file="/tmp/${backup_name}.tar.gz" local tar_file="/tmp/${backup_name}.tar.gz"
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
log_error "Ошибка создания архива" log_error "Ошибка создания архива"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
rm -f "$tar_file" rm -f "$tar_file"
return 1 return 1
fi fi
if [ ! -f "$tar_file" ]; then if [ ! -f "$tar_file" ]; then
log_error "Архив не создан" log_error "Архив не создан"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
return 1 return 1
fi fi
# Шифруем если задан пароль # Шифруем если задан пароль
local final_file="" local final_file=""
if [ -n "$password" ]; then if [ -n "$password" ]; then
final_file="${output_dir}/${backup_name}.tar.gz.enc" 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 openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
log_error "Ошибка шифрования" log_error "Ошибка шифрования"
rm -f "$tar_file" rm -f "$tar_file"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
return 1 return 1
fi fi
rm -f "$tar_file" rm -f "$tar_file"
log_success "Бекап зашифрован (AES-256-CBC)" log_success "Бекап зашифрован (AES-256-CBC)"
else else
final_file="${output_dir}/${backup_name}.tar.gz" final_file="${output_dir}/${backup_name}.tar.gz"
mv "$tar_file" "$final_file" mv "$tar_file" "$final_file"
fi fi
# SHA256 подпись # SHA256 подпись
sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null
# Очистка # Очистка
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
local size local size
size=$(du -h "$final_file" | cut -f1) size=$(du -h "$final_file" | cut -f1)
log_success "Бекап создан: $final_file ($size)" log_success "Бекап создан: $final_file ($size)"
echo "$final_file" echo "$final_file"
return 0 return 0
} }
# ── Восстановление из бекапа ──────────────────────────────────────────────── # ── Восстановление из бекапа ────────────────────────────────────────────────
restore_backup() { restore_backup() {
local backup_file="$1" local backup_file="$1"
local password="$2" local password="$2"
if [ ! -f "$backup_file" ]; then if [ ! -f "$backup_file" ]; then
log_error "Файл не найден: $backup_file" log_error "Файл не найден: $backup_file"
return 1 return 1
fi fi
local tmp_dir="/tmp/gotelegram_restore_$$" local tmp_dir="/tmp/gotelegram_restore_$$"
mkdir -p "$tmp_dir" mkdir -p "$tmp_dir"
# Расшифровываем если нужно # Расшифровываем если нужно
local tar_file="" local tar_file=""
if echo "$backup_file" | grep -q '\.enc$'; then if echo "$backup_file" | grep -q '\.enc$'; then
if [ -z "$password" ]; then if [ -z "$password" ]; then
echo -ne " Введите пароль от бекапа: " echo -ne " Введите пароль от бекапа: "
read -rs password read -rs password
echo "" echo ""
fi fi
tar_file="/tmp/gotelegram_restore_$$.tar.gz" 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 openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
log_error "Неверный пароль или повреждённый файл" log_error "Неверный пароль или повреждённый файл"
rm -rf "$tmp_dir" "$tar_file" rm -rf "$tmp_dir" "$tar_file"
return 1 return 1
fi fi
else else
tar_file="$backup_file" tar_file="$backup_file"
fi fi
# Распаковываем # Распаковываем
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
log_error "Ошибка распаковки архива" log_error "Ошибка распаковки архива"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
return 1 return 1
fi fi
# Находим папку бекапа # Находим папку бекапа
local backup_dir local backup_dir
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1) backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1)
[ -z "$backup_dir" ] && backup_dir="$tmp_dir" [ -z "$backup_dir" ] && backup_dir="$tmp_dir"
# Проверяем метаданные # Проверяем метаданные
if [ -f "$backup_dir/metadata.json" ]; then if [ -f "$backup_dir/metadata.json" ]; then
local bk_version bk_mode bk_ip local bk_version bk_mode bk_ip
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json") bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json")
bk_mode=$(jq -r '.mode // "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_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json")
echo "" echo ""
echo -e " ${BOLD}${WHITE}📦 Бекап:${NC}" echo -e " ${BOLD}${WHITE}📦 Бекап:${NC}"
echo -e " Версия: $bk_version | Режим: $bk_mode | IP: $bk_ip" echo -e " Версия: $bk_version | Режим: $bk_mode | IP: $bk_ip"
echo -e " Дата: $(jq -r '.created_at' "$backup_dir/metadata.json")" echo -e " Дата: $(jq -r '.created_at' "$backup_dir/metadata.json")"
echo "" echo ""
fi fi
if ! confirm "Восстановить конфигурацию? Текущие настройки будут перезаписаны."; then if ! confirm "Восстановить конфигурацию? Текущие настройки будут перезаписаны."; then
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
return 0 return 0
fi fi
# Останавливаем сервисы # Останавливаем сервисы
stop_telemt 2>/dev/null stop_telemt 2>/dev/null
systemctl stop nginx 2>/dev/null systemctl stop nginx 2>/dev/null
# Восстанавливаем telemt конфиг # Восстанавливаем telemt конфиг
if [ -f "$backup_dir/config.toml" ]; then if [ -f "$backup_dir/config.toml" ]; then
mkdir -p /etc/telemt mkdir -p /etc/telemt
cp "$backup_dir/config.toml" "$TELEMT_CONFIG" cp "$backup_dir/config.toml" "$TELEMT_CONFIG"
chmod 600 "$TELEMT_CONFIG" chmod 600 "$TELEMT_CONFIG"
log_success "telemt конфиг восстановлен" log_success "telemt конфиг восстановлен"
fi fi
# Восстанавливаем GoTelegram конфиг # Восстанавливаем GoTelegram конфиг
if [ -f "$backup_dir/gotelegram.json" ]; then if [ -f "$backup_dir/gotelegram.json" ]; then
mkdir -p "$GOTELEGRAM_DIR" mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG" cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
log_success "GoTelegram конфиг восстановлен" log_success "GoTelegram конфиг восстановлен"
fi fi
# Восстанавливаем nginx конфиг # Восстанавливаем nginx конфиг
if [ -f "$backup_dir/nginx.conf" ]; then if [ -f "$backup_dir/nginx.conf" ]; then
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF" cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF"
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK" ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
log_success "nginx конфиг восстановлен" log_success "nginx конфиг восстановлен"
fi fi
# Восстанавливаем SSL # Восстанавливаем SSL
if [ -d "$backup_dir/certs" ]; then if [ -d "$backup_dir/certs" ]; then
local domain local domain
domain=$(config_get domain 2>/dev/null) domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ]; then if [ -n "$domain" ]; then
local cert_dir="/etc/letsencrypt/live/$domain" local cert_dir="/etc/letsencrypt/live/$domain"
mkdir -p "$cert_dir" mkdir -p "$cert_dir"
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null
log_success "SSL сертификаты восстановлены" log_success "SSL сертификаты восстановлены"
fi fi
fi fi
# Восстанавливаем шаблон сайта # Восстанавливаем шаблон сайта
if [ -d "$backup_dir/site" ]; then if [ -d "$backup_dir/site" ]; then
mkdir -p "$WEBSITE_ROOT" mkdir -p "$WEBSITE_ROOT"
cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/" cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/"
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
log_success "Шаблон сайта восстановлен" log_success "Шаблон сайта восстановлен"
fi fi
# Запускаем сервисы # Запускаем сервисы
if is_telemt_installed; then if is_telemt_installed; then
start_telemt start_telemt
fi fi
systemctl start nginx 2>/dev/null systemctl start nginx 2>/dev/null
# Очистка # Очистка
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file" [ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
log_success "Восстановление завершено!" log_success "Восстановление завершено!"
show_proxy_info show_proxy_info
return 0 return 0
} }
# ── Список бекапов ─────────────────────────────────────────────────────────── # ── Список бекапов ───────────────────────────────────────────────────────────
list_backups() { list_backups() {
if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]; then if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]; then
log_info "Бекапов нет" log_info "Бекапов нет"
return 1 return 1
fi fi
echo "" echo ""
echo -e " ${BOLD}${WHITE}📦 Доступные бекапы:${NC}" echo -e " ${BOLD}${WHITE}📦 Доступные бекапы:${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
local i=1 local i=1
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
[[ "$f" == *.sha256 ]] && continue [[ "$f" == *.sha256 ]] && continue
local size date_str name local size date_str name
size=$(du -h "$f" | cut -f1) size=$(du -h "$f" | cut -f1)
name=$(basename "$f") name=$(basename "$f")
date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1) date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1)
local encrypted="" local encrypted=""
[[ "$f" == *.enc ]] && encrypted=" 🔒" [[ "$f" == *.enc ]] && encrypted=" 🔒"
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}" echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}"
((i++)) ((i++))
done done
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
} }
# ── Очистка старых бекапов ─────────────────────────────────────────────────── # ── Очистка старых бекапов ───────────────────────────────────────────────────
cleanup_old_backups() { cleanup_old_backups() {
local keep="${1:-5}" local keep="${1:-5}"
local count local count
count=$(find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l) count=$(find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l)
if [ "$count" -gt "$keep" ]; then if [ "$count" -gt "$keep" ]; then
local to_delete=$((count - keep)) local to_delete=$((count - keep))
find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do 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" rm -f "$f" "${f}.sha256"
done done
log_dim "Удалено $to_delete старых бекапов (оставлено $keep)" log_dim "Удалено $to_delete старых бекапов (оставлено $keep)"
fi fi
} }
# ── Интерактивный бекап ────────────────────────────────────────────────────── # ── Интерактивный бекап ──────────────────────────────────────────────────────
interactive_backup() { interactive_backup() {
echo "" echo ""
echo -e " ${BOLD}${WHITE}💾 Создание бекапа${NC}" echo -e " ${BOLD}${WHITE}💾 Создание бекапа${NC}"
echo -ne " Зашифровать бекап паролем? [Y/n]: " echo -ne " Зашифровать бекап паролем? [Y/n]: "
read -r use_pass read -r use_pass
local password="" local password=""
if [[ ! "$use_pass" =~ ^[Nn] ]]; then if [[ ! "$use_pass" =~ ^[Nn] ]]; then
echo -ne " Введите пароль: " echo -ne " Введите пароль: "
read -rs password read -rs password
echo "" echo ""
echo -ne " Повторите пароль: " echo -ne " Повторите пароль: "
read -rs password2 read -rs password2
echo "" echo ""
if [ "$password" != "$password2" ]; then if [ "$password" != "$password2" ]; then
log_error "Пароли не совпадают" log_error "Пароли не совпадают"
return 1 return 1
fi fi
if [ ${#password} -lt 6 ]; then if [ ${#password} -lt 6 ]; then
log_error "Пароль слишком короткий (минимум 6 символов)" log_error "Пароль слишком короткий (минимум 6 символов)"
return 1 return 1
fi fi
fi fi
create_backup "$password" create_backup "$password"
cleanup_old_backups cleanup_old_backups
} }
# ── Интерактивное восстановление ───────────────────────────────────────────── # ── Интерактивное восстановление ─────────────────────────────────────────────
interactive_restore() { interactive_restore() {
list_backups || return 1 list_backups || return 1
echo -ne " Номер бекапа (или путь к файлу): " echo -ne " Номер бекапа (или путь к файлу): "
read -r choice read -r choice
local backup_file="" local backup_file=""
if [[ "$choice" =~ ^[0-9]+$ ]]; then if [[ "$choice" =~ ^[0-9]+$ ]]; then
local i=1 local i=1
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
[[ "$f" == *.sha256 ]] && continue [[ "$f" == *.sha256 ]] && continue
if [ "$i" -eq "$choice" ]; then if [ "$i" -eq "$choice" ]; then
backup_file="$f" backup_file="$f"
break break
fi fi
((i++)) ((i++))
done done
elif [ -f "$choice" ]; then elif [ -f "$choice" ]; then
backup_file="$choice" backup_file="$choice"
fi fi
if [ -z "$backup_file" ]; then if [ -z "$backup_file" ]; then
log_error "Бекап не найден" log_error "Бекап не найден"
return 1 return 1
fi fi
restore_backup "$backup_file" restore_backup "$backup_file"
} }

View File

@@ -1,282 +1,282 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.2 — Генерация TOML конфигурации для telemt # GoTelegram v2.2 — Генерация TOML конфигурации для telemt
# ── Популярные домены (не заблокированные в РФ) ────────────────────────────── # ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
QUICK_DOMAINS=( QUICK_DOMAINS=(
"google.com" "google.com"
"microsoft.com" "microsoft.com"
"cloudflare.com" "cloudflare.com"
"apple.com" "apple.com"
"amazon.com" "amazon.com"
"github.com" "github.com"
"stackoverflow.com" "stackoverflow.com"
"medium.com" "medium.com"
"wikipedia.org" "wikipedia.org"
"coursera.org" "coursera.org"
"udemy.com" "udemy.com"
"habr.com" "habr.com"
"stepik.org" "stepik.org"
"duolingo.com" "duolingo.com"
"khanacademy.org" "khanacademy.org"
"bbc.com" "bbc.com"
"reuters.com" "reuters.com"
"nytimes.com" "nytimes.com"
"ted.com" "ted.com"
"zoom.us" "zoom.us"
) )
# ── Генерация TOML конфига ─────────────────────────────────────────────────── # ── Генерация TOML конфига ───────────────────────────────────────────────────
generate_telemt_toml() { generate_telemt_toml() {
local secret="$1" local secret="$1"
local port="${2:-443}" local port="${2:-443}"
local mask_mode="${3:-quick}" # quick | stealth local mask_mode="${3:-quick}" # quick | stealth
local mask_host="${4:-google.com}" local mask_host="${4:-google.com}"
local mask_port="${5:-443}" local mask_port="${5:-443}"
local output="${6:-$TELEMT_CONFIG}" local output="${6:-$TELEMT_CONFIG}"
mkdir -p "$(dirname "$output")" mkdir -p "$(dirname "$output")"
cat > "$output" << EOTOML cat > "$output" << EOTOML
# GoTelegram v${GOTELEGRAM_VERSION} — telemt configuration # GoTelegram v${GOTELEGRAM_VERSION} — telemt configuration
# Сгенерировано: $(date -Iseconds) # Сгенерировано: $(date -Iseconds)
# Режим: ${mask_mode} # Режим: ${mask_mode}
# ── Основные настройки ─────────────────────────────────────────────────────── # ── Основные настройки ───────────────────────────────────────────────────────
[stats] [stats]
statsd_address = "" statsd_address = ""
# ── Секреты ────────────────────────────────────────────────────────────────── # ── Секреты ──────────────────────────────────────────────────────────────────
[[users]] [[users]]
name = "main" name = "main"
secret = "${secret}" secret = "${secret}"
# ── Привязка ───────────────────────────────────────────────────────────────── # ── Привязка ─────────────────────────────────────────────────────────────────
[listen] [listen]
bind_to = "0.0.0.0:${port}" bind_to = "0.0.0.0:${port}"
# ── TLS маскировка ─────────────────────────────────────────────────────────── # ── TLS маскировка ───────────────────────────────────────────────────────────
[security] [security]
# Маскировочный хост — куда перенаправлять неопознанные подключения # Маскировочный хост — куда перенаправлять неопознанные подключения
# quick: внешний сайт | stealth: локальный nginx # quick: внешний сайт | stealth: локальный nginx
host = "${mask_host}:${mask_port}" host = "${mask_host}:${mask_port}"
EOTOML EOTOML
chmod 600 "$output" chmod 600 "$output"
log_success "Конфиг telemt записан: $output" log_success "Конфиг telemt записан: $output"
log_dim "Режим: $mask_mode, маскировка: $mask_host:$mask_port" log_dim "Режим: $mask_mode, маскировка: $mask_host:$mask_port"
} }
# ── Добавление дополнительного секрета ─────────────────────────────────────── # ── Добавление дополнительного секрета ───────────────────────────────────────
add_secret_to_config() { add_secret_to_config() {
local name="$1" local name="$1"
local secret="$2" local secret="$2"
local config="${3:-$TELEMT_CONFIG}" local config="${3:-$TELEMT_CONFIG}"
if [ ! -f "$config" ]; then if [ ! -f "$config" ]; then
log_error "Конфиг не найден: $config" log_error "Конфиг не найден: $config"
return 1 return 1
fi fi
# Добавляем новый блок [[users]] # Добавляем новый блок [[users]]
cat >> "$config" << EOSECRET cat >> "$config" << EOSECRET
[[users]] [[users]]
name = "${name}" name = "${name}"
secret = "${secret}" secret = "${secret}"
EOSECRET EOSECRET
log_success "Добавлен секрет: $name" log_success "Добавлен секрет: $name"
} }
# ── Чтение текущего конфига ────────────────────────────────────────────────── # ── Чтение текущего конфига ──────────────────────────────────────────────────
get_config_value() { get_config_value() {
local key="$1" local key="$1"
local config="${2:-$TELEMT_CONFIG}" local config="${2:-$TELEMT_CONFIG}"
if [ ! -f "$config" ]; then return 1; fi if [ ! -f "$config" ]; then return 1; fi
case "$key" in case "$key" in
secret) secret)
grep -m1 'secret\s*=' "$config" | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' ' grep -m1 'secret\s*=' "$config" | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' '
;; ;;
port) port)
grep 'bind_to\s*=' "$config" | sed 's/.*:\([0-9]*\)".*/\1/' grep 'bind_to\s*=' "$config" | sed 's/.*:\([0-9]*\)".*/\1/'
;; ;;
mask_host) mask_host)
grep -A10 '\[security\]' "$config" | grep 'host\s*=' | sed 's/.*=\s*"\(.*\)".*/\1/' grep -A10 '\[security\]' "$config" | grep 'host\s*=' | sed 's/.*=\s*"\(.*\)".*/\1/'
;; ;;
*) *)
grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "' grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "'
;; ;;
esac esac
} }
# ── Валидация конфига ──────────────────────────────────────────────────────── # ── Валидация конфига ────────────────────────────────────────────────────────
validate_telemt_config() { validate_telemt_config() {
local config="${1:-$TELEMT_CONFIG}" local config="${1:-$TELEMT_CONFIG}"
if [ ! -f "$config" ]; then if [ ! -f "$config" ]; then
log_error "Конфиг не найден: $config" log_error "Конфиг не найден: $config"
return 1 return 1
fi fi
# Проверяем обязательные поля # Проверяем обязательные поля
local secret port host local secret port host
secret=$(get_config_value secret "$config") secret=$(get_config_value secret "$config")
port=$(get_config_value port "$config") port=$(get_config_value port "$config")
host=$(get_config_value mask_host "$config") host=$(get_config_value mask_host "$config")
local errors=0 local errors=0
if [ -z "$secret" ]; then if [ -z "$secret" ]; then
log_error "Не задан secret" log_error "Не задан secret"
((errors++)) ((errors++))
elif [ ${#secret} -lt 32 ]; then elif [ ${#secret} -lt 32 ]; then
log_warning "Secret слишком короткий (${#secret} символов, рекомендуется 32+)" log_warning "Secret слишком короткий (${#secret} символов, рекомендуется 32+)"
fi fi
if [ -z "$port" ]; then if [ -z "$port" ]; then
log_error "Не задан порт (bind_to)" log_error "Не задан порт (bind_to)"
((errors++)) ((errors++))
elif [ "$port" -lt 1 ] || [ "$port" -gt 65535 ] 2>/dev/null; then elif [ "$port" -lt 1 ] || [ "$port" -gt 65535 ] 2>/dev/null; then
log_error "Порт вне диапазона: $port" log_error "Порт вне диапазона: $port"
((errors++)) ((errors++))
fi fi
if [ -z "$host" ]; then if [ -z "$host" ]; then
log_error "Не задан маскировочный хост (security.host)" log_error "Не задан маскировочный хост (security.host)"
((errors++)) ((errors++))
fi fi
if [ $errors -gt 0 ]; then if [ $errors -gt 0 ]; then
log_error "Найдено ошибок: $errors" log_error "Найдено ошибок: $errors"
return 1 return 1
fi fi
log_success "Конфиг валиден" log_success "Конфиг валиден"
return 0 return 0
} }
# ── Выбор домена (интерактивный) ───────────────────────────────────────────── # ── Выбор домена (интерактивный) ─────────────────────────────────────────────
select_quick_domain() { select_quick_domain() {
echo "" echo ""
echo -e " ${BOLD}${WHITE}🌐 Выберите домен для маскировки (Fake TLS):${NC}" echo -e " ${BOLD}${WHITE}🌐 Выберите домен для маскировки (Fake TLS):${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
local i=1 local i=1
local row="" local row=""
for d in "${QUICK_DOMAINS[@]}"; do for d in "${QUICK_DOMAINS[@]}"; do
printf " ${CYAN}%2d)${NC} %-25s" "$i" "$d" printf " ${CYAN}%2d)${NC} %-25s" "$i" "$d"
if (( i % 2 == 0 )); then if (( i % 2 == 0 )); then
echo "" echo ""
fi fi
((i++)) ((i++))
done done
if (( (i-1) % 2 != 0 )); then echo ""; fi if (( (i-1) % 2 != 0 )); then echo ""; fi
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
echo -ne " ${WHITE}Выбор (1-${#QUICK_DOMAINS[@]}):${NC} " echo -ne " ${WHITE}Выбор (1-${#QUICK_DOMAINS[@]}):${NC} "
read -r choice read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#QUICK_DOMAINS[@]} ]; then if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#QUICK_DOMAINS[@]} ]; then
echo "${QUICK_DOMAINS[$((choice-1))]}" echo "${QUICK_DOMAINS[$((choice-1))]}"
return 0 return 0
fi fi
log_error "Неверный выбор" log_error "Неверный выбор"
return 1 return 1
} }
# ── Выбор порта (интерактивный) ────────────────────────────────────────────── # ── Выбор порта (интерактивный) ──────────────────────────────────────────────
select_port() { select_port() {
echo "" echo ""
echo -e " ${BOLD}${WHITE}🔌 Выберите порт:${NC}" echo -e " ${BOLD}${WHITE}🔌 Выберите порт:${NC}"
# Проверяем стандартные порты # Проверяем стандартные порты
local busy_443 busy_8443 local busy_443 busy_8443
busy_443=$(check_port 443) busy_443=$(check_port 443)
busy_8443=$(check_port 8443) busy_8443=$(check_port 8443)
local label_443="443 (рекомендуется)" local label_443="443 (рекомендуется)"
local label_8443="8443" local label_8443="8443"
[ -n "$busy_443" ] && label_443="443 ⚠️ занят" [ -n "$busy_443" ] && label_443="443 ⚠️ занят"
[ -n "$busy_8443" ] && label_8443="8443 ⚠️ занят" [ -n "$busy_8443" ] && label_8443="8443 ⚠️ занят"
echo -e " ${CYAN}1)${NC} $label_443" echo -e " ${CYAN}1)${NC} $label_443"
echo -e " ${CYAN}2)${NC} $label_8443" echo -e " ${CYAN}2)${NC} $label_8443"
echo -e " ${CYAN}3)${NC} Свой порт" echo -e " ${CYAN}3)${NC} Свой порт"
if [ -n "$busy_443" ]; then if [ -n "$busy_443" ]; then
echo -e " ${DIM} ⚠ Порт 443 занят: $(echo "$busy_443" | head -c 60)${NC}" echo -e " ${DIM} ⚠ Порт 443 занят: $(echo "$busy_443" | head -c 60)${NC}"
fi fi
echo -ne " ${WHITE}Выбор:${NC} " echo -ne " ${WHITE}Выбор:${NC} "
read -r choice read -r choice
case "$choice" in case "$choice" in
1) echo "443" ;; 1) echo "443" ;;
2) echo "8443" ;; 2) echo "8443" ;;
3) 3)
echo -ne " Введите порт (1-65535): " echo -ne " Введите порт (1-65535): "
read -r custom_port read -r custom_port
if [[ "$custom_port" =~ ^[0-9]+$ ]] && [ "$custom_port" -ge 1 ] && [ "$custom_port" -le 65535 ]; then if [[ "$custom_port" =~ ^[0-9]+$ ]] && [ "$custom_port" -ge 1 ] && [ "$custom_port" -le 65535 ]; then
echo "$custom_port" echo "$custom_port"
else else
log_error "Неверный порт" log_error "Неверный порт"
return 1 return 1
fi fi
;; ;;
*) echo "443" ;; *) echo "443" ;;
esac esac
} }
# ── Генерация ссылки tg://proxy ────────────────────────────────────────────── # ── Генерация ссылки tg://proxy ──────────────────────────────────────────────
generate_proxy_link() { generate_proxy_link() {
local ip="${1:-$(get_server_ip)}" local ip="${1:-$(get_server_ip)}"
local port="${2:-443}" local port="${2:-443}"
local secret="$3" local secret="$3"
echo "tg://proxy?server=${ip}&port=${port}&secret=${secret}" echo "tg://proxy?server=${ip}&port=${port}&secret=${secret}"
} }
# ── Вывод информации о прокси ──────────────────────────────────────────────── # ── Вывод информации о прокси ────────────────────────────────────────────────
show_proxy_info() { show_proxy_info() {
local config="${1:-$TELEMT_CONFIG}" local config="${1:-$TELEMT_CONFIG}"
local secret port mask_host ip link status local secret port mask_host ip link status
secret=$(get_config_value secret "$config") secret=$(get_config_value secret "$config")
port=$(get_config_value port "$config") port=$(get_config_value port "$config")
mask_host=$(get_config_value mask_host "$config") mask_host=$(get_config_value mask_host "$config")
ip=$(get_server_ip) ip=$(get_server_ip)
link=$(generate_proxy_link "$ip" "$port" "$secret") link=$(generate_proxy_link "$ip" "$port" "$secret")
status=$(telemt_status) status=$(telemt_status)
local mode local mode
mode=$(config_get mode 2>/dev/null || echo "quick") mode=$(config_get mode 2>/dev/null || echo "quick")
local status_icon status_text local status_icon status_text
case "$status" in case "$status" in
running) status_icon="✅"; status_text="Работает" ;; running) status_icon="✅"; status_text="Работает" ;;
stopped) status_icon="⏸️"; status_text="Остановлен" ;; stopped) status_icon="⏸️"; status_text="Остановлен" ;;
*) status_icon="❌"; status_text="Не установлен" ;; *) status_icon="❌"; status_text="Не установлен" ;;
esac esac
echo "" echo ""
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}" echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)" echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}" echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}" echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}" echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}" echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
echo -e " ${WHITE}Secret:${NC} ${CYAN}${secret:0:16}...${NC}" echo -e " ${WHITE}Secret:${NC} ${CYAN}${secret:0:16}...${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
echo -e " ${WHITE}Ссылка:${NC}" echo -e " ${WHITE}Ссылка:${NC}"
echo -e " ${GREEN}${link}${NC}" echo -e " ${GREEN}${link}${NC}"
echo "" echo ""
# QR если доступен # QR если доступен
if command -v qrencode &>/dev/null; then if command -v qrencode &>/dev/null; then
qrencode -t UTF8 -m 2 "$link" 2>/dev/null qrencode -t UTF8 -m 2 "$link" 2>/dev/null
fi fi
} }

View File

@@ -1,280 +1,280 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.2 — Каталог шаблонов сайтов # GoTelegram v2.2 — Каталог шаблонов сайтов
# Выбор из ~200 шаблонов, превью-ссылки, скачивание через git sparse-checkout # Выбор из ~200 шаблонов, превью-ссылки, скачивание через git sparse-checkout
CATALOG_FILE="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")/templates_catalog.json" CATALOG_FILE="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")/templates_catalog.json"
TEMPLATES_CACHE="/tmp/gotelegram_templates" TEMPLATES_CACHE="/tmp/gotelegram_templates"
# ── Загрузка каталога ──────────────────────────────────────────────────────── # ── Загрузка каталога ────────────────────────────────────────────────────────
load_catalog() { load_catalog() {
if [ ! -f "$CATALOG_FILE" ]; then if [ ! -f "$CATALOG_FILE" ]; then
log_error "Каталог шаблонов не найден: $CATALOG_FILE" log_error "Каталог шаблонов не найден: $CATALOG_FILE"
return 1 return 1
fi fi
return 0 return 0
} }
# ── Категории ──────────────────────────────────────────────────────────────── # ── Категории ────────────────────────────────────────────────────────────────
get_categories() { get_categories() {
jq -r '.categories[] | "\(.id)|\(.name)|\(.icon)|\(.templates | length)"' "$CATALOG_FILE" 2>/dev/null jq -r '.categories[] | "\(.id)|\(.name)|\(.icon)|\(.templates | length)"' "$CATALOG_FILE" 2>/dev/null
} }
get_category_name() { get_category_name() {
local cat_id="$1" local cat_id="$1"
jq -r ".categories[] | select(.id == \"$cat_id\") | .name" "$CATALOG_FILE" 2>/dev/null jq -r ".categories[] | select(.id == \"$cat_id\") | .name" "$CATALOG_FILE" 2>/dev/null
} }
# ── Шаблоны по категории ──────────────────────────────────────────────────── # ── Шаблоны по категории ────────────────────────────────────────────────────
get_templates_by_category() { get_templates_by_category() {
local cat_id="$1" local cat_id="$1"
jq -r ".categories[] | select(.id == \"$cat_id\") | .templates[] | \"\(.id)|\(.name)|\(.source)|\(.preview_url)\"" "$CATALOG_FILE" 2>/dev/null jq -r ".categories[] | select(.id == \"$cat_id\") | .templates[] | \"\(.id)|\(.name)|\(.source)|\(.preview_url)\"" "$CATALOG_FILE" 2>/dev/null
} }
# ── Информация о шаблоне ──────────────────────────────────────────────────── # ── Информация о шаблоне ────────────────────────────────────────────────────
get_template_info() { get_template_info() {
local tpl_id="$1" local tpl_id="$1"
jq ".categories[].templates[] | select(.id == \"$tpl_id\")" "$CATALOG_FILE" 2>/dev/null jq ".categories[].templates[] | select(.id == \"$tpl_id\")" "$CATALOG_FILE" 2>/dev/null
} }
get_template_field() { get_template_field() {
local tpl_id="$1" local tpl_id="$1"
local field="$2" local field="$2"
jq -r ".categories[].templates[] | select(.id == \"$tpl_id\") | .$field" "$CATALOG_FILE" 2>/dev/null jq -r ".categories[].templates[] | select(.id == \"$tpl_id\") | .$field" "$CATALOG_FILE" 2>/dev/null
} }
# ── Интерактивный выбор категории ──────────────────────────────────────────── # ── Интерактивный выбор категории ────────────────────────────────────────────
select_category() { select_category() {
load_catalog || return 1 load_catalog || return 1
echo "" echo ""
echo -e " ${BOLD}${WHITE}📂 Категории шаблонов сайтов:${NC}" echo -e " ${BOLD}${WHITE}📂 Категории шаблонов сайтов:${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
local cats=() local cats=()
local i=1 local i=1
while IFS='|' read -r id name icon count; do while IFS='|' read -r id name icon count; do
[ "$count" -eq 0 ] && continue [ "$count" -eq 0 ] && continue
local emoji local emoji
case "$icon" in case "$icon" in
briefcase) emoji="🏢" ;; briefcase) emoji="🏢" ;;
shopping-cart) emoji="🛒" ;; shopping-cart) emoji="🛒" ;;
heart) emoji="🏥" ;; heart) emoji="🏥" ;;
book) emoji="🎓" ;; book) emoji="🎓" ;;
palette) emoji="📸" ;; palette) emoji="📸" ;;
home) emoji="🏠" ;; home) emoji="🏠" ;;
utensils) emoji="🍕" ;; utensils) emoji="🍕" ;;
rocket) emoji="🎨" ;; rocket) emoji="🎨" ;;
chart-bar) emoji="🔧" ;; chart-bar) emoji="🔧" ;;
*) emoji="📄" ;; *) emoji="📄" ;;
esac esac
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}(%d шаблонов)${NC}\n" "$i" "$name" "$count" printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}(%d шаблонов)${NC}\n" "$i" "$name" "$count"
cats+=("$id") cats+=("$id")
((i++)) ((i++))
done < <(get_categories) done < <(get_categories)
printf " ${CYAN}%2d)${NC} 🎲 Случайный шаблон\n" "$i" printf " ${CYAN}%2d)${NC} 🎲 Случайный шаблон\n" "$i"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -ne " ${WHITE}Выбор:${NC} " echo -ne " ${WHITE}Выбор:${NC} "
read -r choice read -r choice
# Случайный # Случайный
if [ "$choice" -eq "$i" ] 2>/dev/null; then if [ "$choice" -eq "$i" ] 2>/dev/null; then
local random_cat="${cats[$((RANDOM % ${#cats[@]}))]}" local random_cat="${cats[$((RANDOM % ${#cats[@]}))]}"
echo "$random_cat" echo "$random_cat"
return 0 return 0
fi fi
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
echo "${cats[$((choice-1))]}" echo "${cats[$((choice-1))]}"
return 0 return 0
fi fi
log_error "Неверный выбор" log_error "Неверный выбор"
return 1 return 1
} }
# ── Интерактивный выбор шаблона ────────────────────────────────────────────── # ── Интерактивный выбор шаблона ──────────────────────────────────────────────
select_template() { select_template() {
local cat_id="$1" local cat_id="$1"
local cat_name local cat_name
cat_name=$(get_category_name "$cat_id") cat_name=$(get_category_name "$cat_id")
echo "" echo ""
echo -e " ${BOLD}${WHITE}📋 $cat_name — доступные шаблоны:${NC}" echo -e " ${BOLD}${WHITE}📋 $cat_name — доступные шаблоны:${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
local tpls=() local tpls=()
local i=1 local i=1
while IFS='|' read -r id name source preview; do while IFS='|' read -r id name source preview; do
printf " ${CYAN}%2d)${NC} %-30s ${DIM}[%s]${NC}\n" "$i" "$name" "$source" printf " ${CYAN}%2d)${NC} %-30s ${DIM}[%s]${NC}\n" "$i" "$name" "$source"
tpls+=("$id") tpls+=("$id")
((i++)) ((i++))
done < <(get_templates_by_category "$cat_id") done < <(get_templates_by_category "$cat_id")
if [ ${#tpls[@]} -eq 0 ]; then if [ ${#tpls[@]} -eq 0 ]; then
log_info "В этой категории нет шаблонов" log_info "В этой категории нет шаблонов"
return 1 return 1
fi fi
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
echo -ne " ${WHITE}Выбор (1-$((i-1))):${NC} " echo -ne " ${WHITE}Выбор (1-$((i-1))):${NC} "
read -r choice read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
local selected_id="${tpls[$((choice-1))]}" local selected_id="${tpls[$((choice-1))]}"
# Показываем превью # Показываем превью
show_template_preview "$selected_id" show_template_preview "$selected_id"
echo "$selected_id" echo "$selected_id"
return 0 return 0
fi fi
log_error "Неверный выбор" log_error "Неверный выбор"
return 1 return 1
} }
# ── Показ превью шаблона ──────────────────────────────────────────────────── # ── Показ превью шаблона ────────────────────────────────────────────────────
show_template_preview() { show_template_preview() {
local tpl_id="$1" local tpl_id="$1"
local info local info
info=$(get_template_info "$tpl_id") info=$(get_template_info "$tpl_id")
local name source preview_url repo_url description local name source preview_url repo_url description
name=$(echo "$info" | jq -r '.name') name=$(echo "$info" | jq -r '.name')
source=$(echo "$info" | jq -r '.source') source=$(echo "$info" | jq -r '.source')
preview_url=$(echo "$info" | jq -r '.preview_url // empty') preview_url=$(echo "$info" | jq -r '.preview_url // empty')
repo_url=$(echo "$info" | jq -r '.repo_url // empty') repo_url=$(echo "$info" | jq -r '.repo_url // empty')
description=$(echo "$info" | jq -r '.description // "—"') description=$(echo "$info" | jq -r '.description // "—"')
echo "" echo ""
echo -e " ${BOLD}${WHITE}🔍 Превью шаблона:${NC}" echo -e " ${BOLD}${WHITE}🔍 Превью шаблона:${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${WHITE}Название:${NC} $name" echo -e " ${WHITE}Название:${NC} $name"
echo -e " ${WHITE}Источник:${NC} $source" echo -e " ${WHITE}Источник:${NC} $source"
echo -e " ${WHITE}Описание:${NC} $description" echo -e " ${WHITE}Описание:${NC} $description"
if [ -n "$preview_url" ]; then if [ -n "$preview_url" ]; then
echo "" echo ""
echo -e " ${GREEN}👁 Превью:${NC} ${CYAN}${preview_url}${NC}" echo -e " ${GREEN}👁 Превью:${NC} ${CYAN}${preview_url}${NC}"
echo -e " ${DIM}Откройте ссылку в браузере для просмотра шаблона${NC}" echo -e " ${DIM}Откройте ссылку в браузере для просмотра шаблона${NC}"
fi fi
if [ -n "$repo_url" ]; then if [ -n "$repo_url" ]; then
echo -e " ${DIM}📦 Репо: ${repo_url}${NC}" echo -e " ${DIM}📦 Репо: ${repo_url}${NC}"
fi fi
# Благодарность автору # Благодарность автору
echo "" echo ""
echo -e " ${MAGENTA}💜 Спасибо авторам ${source} за открытый код!${NC}" echo -e " ${MAGENTA}💜 Спасибо авторам ${source} за открытый код!${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo "" echo ""
if ! confirm "Установить этот шаблон?"; then if ! confirm "Установить этот шаблон?"; then
return 1 return 1
fi fi
return 0 return 0
} }
# ── Скачивание шаблона ─────────────────────────────────────────────────────── # ── Скачивание шаблона ───────────────────────────────────────────────────────
download_template() { download_template() {
local tpl_id="$1" local tpl_id="$1"
local output_dir="${2:-$TEMPLATES_CACHE}" local output_dir="${2:-$TEMPLATES_CACHE}"
local info local info
info=$(get_template_info "$tpl_id") info=$(get_template_info "$tpl_id")
local repo_url sparse_path source name local repo_url sparse_path source name
repo_url=$(echo "$info" | jq -r '.repo_url') repo_url=$(echo "$info" | jq -r '.repo_url')
sparse_path=$(echo "$info" | jq -r '.sparse_path') sparse_path=$(echo "$info" | jq -r '.sparse_path')
source=$(echo "$info" | jq -r '.source') source=$(echo "$info" | jq -r '.source')
name=$(echo "$info" | jq -r '.name') name=$(echo "$info" | jq -r '.name')
local clone_dir="$output_dir/${tpl_id}" local clone_dir="$output_dir/${tpl_id}"
rm -rf "$clone_dir" rm -rf "$clone_dir"
mkdir -p "$clone_dir" mkdir -p "$clone_dir"
log_info "Скачивание шаблона \"$name\"..." log_info "Скачивание шаблона \"$name\"..."
# Для HTML5 UP — отдельный репо с папками # Для HTML5 UP — отдельный репо с папками
if [ "$source" = "html5up" ]; then if [ "$source" = "html5up" ]; then
local tmp_clone="/tmp/html5up_clone_$$" local tmp_clone="/tmp/html5up_clone_$$"
rm -rf "$tmp_clone" rm -rf "$tmp_clone"
# Sparse checkout # Sparse checkout
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
# Fallback: полный clone # Fallback: полный clone
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
fi fi
if [ -d "$tmp_clone" ]; then if [ -d "$tmp_clone" ]; then
cd "$tmp_clone" && git sparse-checkout set "$sparse_path" 2>/dev/null cd "$tmp_clone" && git sparse-checkout set "$sparse_path" 2>/dev/null
if [ -d "$tmp_clone/$sparse_path" ]; then if [ -d "$tmp_clone/$sparse_path" ]; then
cp -r "$tmp_clone/$sparse_path"/* "$clone_dir/" cp -r "$tmp_clone/$sparse_path"/* "$clone_dir/"
fi fi
cd - >/dev/null cd - >/dev/null
fi fi
rm -rf "$tmp_clone" rm -rf "$tmp_clone"
# Для learning-zone — один большой репо # Для learning-zone — один большой репо
elif [ "$source" = "learning-zone" ]; then elif [ "$source" = "learning-zone" ]; then
local tmp_clone="/tmp/lz_clone_$$" local tmp_clone="/tmp/lz_clone_$$"
rm -rf "$tmp_clone" rm -rf "$tmp_clone"
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
fi fi
if [ -d "$tmp_clone" ]; then if [ -d "$tmp_clone" ]; then
cd "$tmp_clone" && git sparse-checkout set "$sparse_path" 2>/dev/null cd "$tmp_clone" && git sparse-checkout set "$sparse_path" 2>/dev/null
if [ -d "$tmp_clone/$sparse_path" ]; then if [ -d "$tmp_clone/$sparse_path" ]; then
cp -r "$tmp_clone/$sparse_path"/* "$clone_dir/" cp -r "$tmp_clone/$sparse_path"/* "$clone_dir/"
fi fi
cd - >/dev/null cd - >/dev/null
fi fi
rm -rf "$tmp_clone" rm -rf "$tmp_clone"
# Для StartBootstrap — каждый шаблон в своём репо # Для StartBootstrap — каждый шаблон в своём репо
elif [ "$source" = "startbootstrap" ]; then elif [ "$source" = "startbootstrap" ]; then
git clone --depth 1 "$repo_url" "$clone_dir" 2>/dev/null git clone --depth 1 "$repo_url" "$clone_dir" 2>/dev/null
# Убираем .git # Убираем .git
rm -rf "$clone_dir/.git" rm -rf "$clone_dir/.git"
fi fi
# Проверяем результат # Проверяем результат
if [ -f "$clone_dir/index.html" ]; then if [ -f "$clone_dir/index.html" ]; then
log_success "Шаблон \"$name\" скачан" log_success "Шаблон \"$name\" скачан"
echo "$clone_dir" echo "$clone_dir"
return 0 return 0
else else
log_error "Шаблон не содержит index.html" log_error "Шаблон не содержит index.html"
log_dim "Путь: $clone_dir" log_dim "Путь: $clone_dir"
ls -la "$clone_dir" 2>/dev/null ls -la "$clone_dir" 2>/dev/null
return 1 return 1
fi fi
} }
# ── Полный интерактивный процесс выбора ────────────────────────────────────── # ── Полный интерактивный процесс выбора ──────────────────────────────────────
interactive_template_selection() { interactive_template_selection() {
load_catalog || return 1 load_catalog || return 1
# Выбор категории # Выбор категории
local cat_id local cat_id
cat_id=$(select_category) cat_id=$(select_category)
[ $? -ne 0 ] && return 1 [ $? -ne 0 ] && return 1
# Выбор шаблона # Выбор шаблона
local tpl_id local tpl_id
tpl_id=$(select_template "$cat_id") tpl_id=$(select_template "$cat_id")
[ $? -ne 0 ] && return 1 [ $? -ne 0 ] && return 1
# Скачивание # Скачивание
local template_dir local template_dir
template_dir=$(download_template "$tpl_id") template_dir=$(download_template "$tpl_id")
[ $? -ne 0 ] && return 1 [ $? -ne 0 ] && return 1
echo "$template_dir" echo "$template_dir"
return 0 return 0
} }

View File

@@ -1,340 +1,340 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны) # GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны)
# ── Установка nginx ────────────────────────────────────────────────────────── # ── Установка nginx ──────────────────────────────────────────────────────────
install_nginx() { install_nginx() {
if command -v nginx &>/dev/null; then if command -v nginx &>/dev/null; then
log_dim "nginx уже установлен" log_dim "nginx уже установлен"
return 0 return 0
fi fi
log_info "Установка nginx..." log_info "Установка nginx..."
case "$(get_pkg_manager)" in case "$(get_pkg_manager)" in
apt) apt-get update -qq && apt-get install -y -qq nginx ;; apt) apt-get update -qq && apt-get install -y -qq nginx ;;
dnf) dnf install -y -q nginx ;; dnf) dnf install -y -q nginx ;;
yum) yum install -y -q nginx ;; yum) yum install -y -q nginx ;;
esac esac
systemctl enable nginx 2>/dev/null systemctl enable nginx 2>/dev/null
} }
# ── Установка certbot ──────────────────────────────────────────────────────── # ── Установка certbot ────────────────────────────────────────────────────────
install_certbot() { install_certbot() {
if command -v certbot &>/dev/null; then if command -v certbot &>/dev/null; then
log_dim "certbot уже установлен" log_dim "certbot уже установлен"
return 0 return 0
fi fi
log_info "Установка certbot..." log_info "Установка certbot..."
case "$(get_pkg_manager)" in case "$(get_pkg_manager)" in
apt) apt-get install -y -qq certbot python3-certbot-nginx ;; apt) apt-get install -y -qq certbot python3-certbot-nginx ;;
dnf) dnf install -y -q certbot python3-certbot-nginx ;; dnf) dnf install -y -q certbot python3-certbot-nginx ;;
yum) yum install -y -q certbot python3-certbot-nginx ;; yum) yum install -y -q certbot python3-certbot-nginx ;;
esac esac
} }
# ── Генерация nginx конфига ────────────────────────────────────────────────── # ── Генерация nginx конфига ──────────────────────────────────────────────────
generate_nginx_config() { generate_nginx_config() {
local domain="$1" local domain="$1"
local proxy_port="${2:-443}" local proxy_port="${2:-443}"
local use_ssl="${3:-true}" local use_ssl="${3:-true}"
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cat > "$NGINX_SITE_CONF" << 'EONGINX' cat > "$NGINX_SITE_CONF" << 'EONGINX'
# GoTelegram v2.2 — nginx config # GoTelegram v2.2 — nginx config
# Обслуживает сайт-маскировку для telemt stealth mode # Обслуживает сайт-маскировку для telemt stealth mode
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name DOMAIN_PLACEHOLDER; server_name DOMAIN_PLACEHOLDER;
# Let's Encrypt ACME challenge # Let's Encrypt ACME challenge
location /.well-known/acme-challenge/ { location /.well-known/acme-challenge/ {
root /var/www/certbot; root /var/www/certbot;
allow all; allow all;
} }
# Редирект на HTTPS # Редирект на HTTPS
location / { location / {
return 301 https://$server_name$request_uri; return 301 https://$server_name$request_uri;
} }
} }
server { server {
listen SSL_PORT_PLACEHOLDER ssl http2; listen SSL_PORT_PLACEHOLDER ssl http2;
listen [::]:SSL_PORT_PLACEHOLDER ssl http2; listen [::]:SSL_PORT_PLACEHOLDER ssl http2;
server_name DOMAIN_PLACEHOLDER; server_name DOMAIN_PLACEHOLDER;
# SSL сертификаты # SSL сертификаты
ssl_certificate /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/fullchain.pem; ssl_certificate /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/privkey.pem;
# Современные TLS настройки # Современные TLS настройки
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off; ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d; ssl_session_timeout 1d;
ssl_session_tickets off; ssl_session_tickets off;
# OCSP stapling # OCSP stapling
ssl_stapling on; ssl_stapling on;
ssl_stapling_verify on; ssl_stapling_verify on;
# Security headers # Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Корень сайта # Корень сайта
root /var/www/gotelegram-site; root /var/www/gotelegram-site;
index index.html; index index.html;
location / { location / {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
expires 30d; expires 30d;
} }
# Кеширование статики # Кеширование статики
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
} }
# Скрываем служебные файлы # Скрываем служебные файлы
location ~ /\. { deny all; } location ~ /\. { deny all; }
location = /robots.txt { allow all; log_not_found off; access_log off; } location = /robots.txt { allow all; log_not_found off; access_log off; }
location = /favicon.ico { log_not_found off; access_log off; } location = /favicon.ico { log_not_found off; access_log off; }
} }
EONGINX EONGINX
# Подставляем значения (используем | как разделитель, чтобы / в домене не ломал sed) # Подставляем значения (используем | как разделитель, чтобы / в домене не ломал sed)
local escaped_domain local escaped_domain
escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g') escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g')
sed -i "s|DOMAIN_PLACEHOLDER|${escaped_domain}|g" "$NGINX_SITE_CONF" sed -i "s|DOMAIN_PLACEHOLDER|${escaped_domain}|g" "$NGINX_SITE_CONF"
sed -i "s|SSL_PORT_PLACEHOLDER|443|g" "$NGINX_SITE_CONF" sed -i "s|SSL_PORT_PLACEHOLDER|443|g" "$NGINX_SITE_CONF"
# Активируем сайт # Активируем сайт
rm -f /etc/nginx/sites-enabled/default 2>/dev/null rm -f /etc/nginx/sites-enabled/default 2>/dev/null
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK" ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
log_success "nginx конфиг создан для $domain" log_success "nginx конфиг создан для $domain"
} }
# ── Временный конфиг (до получения SSL) ────────────────────────────────────── # ── Временный конфиг (до получения SSL) ──────────────────────────────────────
generate_nginx_temp_config() { generate_nginx_temp_config() {
local domain="$1" local domain="$1"
cat > "$NGINX_SITE_CONF" << EONGINX_TEMP cat > "$NGINX_SITE_CONF" << EONGINX_TEMP
# GoTelegram — временный конфиг (до получения SSL) # GoTelegram — временный конфиг (до получения SSL)
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name ${domain}; server_name ${domain};
location /.well-known/acme-challenge/ { location /.well-known/acme-challenge/ {
root /var/www/certbot; root /var/www/certbot;
allow all; allow all;
} }
root /var/www/gotelegram-site; root /var/www/gotelegram-site;
index index.html; index index.html;
location / { location / {
try_files \$uri \$uri/ =404; try_files \$uri \$uri/ =404;
} }
} }
EONGINX_TEMP EONGINX_TEMP
rm -f /etc/nginx/sites-enabled/default 2>/dev/null rm -f /etc/nginx/sites-enabled/default 2>/dev/null
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK" ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
mkdir -p /var/www/certbot mkdir -p /var/www/certbot
} }
# ── Получение SSL сертификата ──────────────────────────────────────────────── # ── Получение SSL сертификата ────────────────────────────────────────────────
obtain_ssl_certificate() { obtain_ssl_certificate() {
local domain="$1" local domain="$1"
local email="${2:-}" local email="${2:-}"
if [ ! -d "/etc/letsencrypt/live/$domain" ]; then if [ ! -d "/etc/letsencrypt/live/$domain" ]; then
log_info "Получение SSL сертификата для $domain..." log_info "Получение SSL сертификата для $domain..."
# Временный конфиг для ACME challenge # Временный конфиг для ACME challenge
generate_nginx_temp_config "$domain" generate_nginx_temp_config "$domain"
systemctl restart nginx 2>/dev/null systemctl restart nginx 2>/dev/null
local certbot_args=( local certbot_args=(
certonly certonly
--webroot --webroot
-w /var/www/certbot -w /var/www/certbot
-d "$domain" -d "$domain"
--non-interactive --non-interactive
--agree-tos --agree-tos
) )
if [ -n "$email" ]; then if [ -n "$email" ]; then
certbot_args+=(--email "$email") certbot_args+=(--email "$email")
else else
certbot_args+=(--register-unsafely-without-email) certbot_args+=(--register-unsafely-without-email)
fi fi
if certbot "${certbot_args[@]}" 2>/dev/null; then if certbot "${certbot_args[@]}" 2>/dev/null; then
log_success "SSL сертификат получен для $domain" log_success "SSL сертификат получен для $domain"
return 0 return 0
else else
log_error "Не удалось получить SSL сертификат" log_error "Не удалось получить SSL сертификат"
log_dim "Убедитесь что домен $domain направлен на IP этого сервера" log_dim "Убедитесь что домен $domain направлен на IP этого сервера"
log_dim "и порт 80 открыт в файрволе." log_dim "и порт 80 открыт в файрволе."
return 1 return 1
fi fi
else else
log_dim "SSL сертификат уже существует для $domain" log_dim "SSL сертификат уже существует для $domain"
return 0 return 0
fi fi
} }
# ── Авто-обновление сертификата ────────────────────────────────────────────── # ── Авто-обновление сертификата ──────────────────────────────────────────────
setup_ssl_auto_renewal() { setup_ssl_auto_renewal() {
# Certbot systemd timer (предпочтительно) # Certbot systemd timer (предпочтительно)
if [ -f /etc/systemd/system/certbot.timer ] || [ -f /lib/systemd/system/certbot.timer ]; then if [ -f /etc/systemd/system/certbot.timer ] || [ -f /lib/systemd/system/certbot.timer ]; then
systemctl enable certbot.timer 2>/dev/null systemctl enable certbot.timer 2>/dev/null
systemctl start certbot.timer 2>/dev/null systemctl start certbot.timer 2>/dev/null
log_success "Авто-обновление SSL через systemd timer" log_success "Авто-обновление SSL через systemd timer"
return 0 return 0
fi fi
# Fallback: cron # Fallback: cron
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab - (crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
log_success "Авто-обновление SSL через cron (3:00 ежедневно)" log_success "Авто-обновление SSL через cron (3:00 ежедневно)"
fi fi
} }
# ── Обновление сертификата вручную ─────────────────────────────────────────── # ── Обновление сертификата вручную ───────────────────────────────────────────
renew_ssl_certificate() { renew_ssl_certificate() {
log_info "Обновление SSL сертификата..." log_info "Обновление SSL сертификата..."
if certbot renew --quiet --post-hook "systemctl reload nginx" 2>/dev/null; then if certbot renew --quiet --post-hook "systemctl reload nginx" 2>/dev/null; then
log_success "Сертификат обновлён" log_success "Сертификат обновлён"
return 0 return 0
else else
log_error "Ошибка обновления сертификата" log_error "Ошибка обновления сертификата"
return 1 return 1
fi fi
} }
# ── Дата истечения SSL ─────────────────────────────────────────────────────── # ── Дата истечения SSL ───────────────────────────────────────────────────────
get_ssl_expiry() { get_ssl_expiry() {
local domain="$1" local domain="$1"
local cert="/etc/letsencrypt/live/$domain/fullchain.pem" local cert="/etc/letsencrypt/live/$domain/fullchain.pem"
if [ -f "$cert" ]; then if [ -f "$cert" ]; then
openssl x509 -enddate -noout -in "$cert" 2>/dev/null | sed 's/notAfter=//' openssl x509 -enddate -noout -in "$cert" 2>/dev/null | sed 's/notAfter=//'
else else
echo "N/A" echo "N/A"
fi fi
} }
# ── Деплой шаблона сайта ───────────────────────────────────────────────────── # ── Деплой шаблона сайта ─────────────────────────────────────────────────────
deploy_template_to_nginx() { deploy_template_to_nginx() {
local template_dir="$1" local template_dir="$1"
if [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then if [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then
log_error "Шаблон не содержит index.html: $template_dir" log_error "Шаблон не содержит index.html: $template_dir"
return 1 return 1
fi fi
# Бекапим старый сайт # Бекапим старый сайт
if [ -d "$WEBSITE_ROOT" ] && [ "$(ls -A "$WEBSITE_ROOT" 2>/dev/null)" ]; then if [ -d "$WEBSITE_ROOT" ] && [ "$(ls -A "$WEBSITE_ROOT" 2>/dev/null)" ]; then
local backup_name="site_backup_$(date +%Y%m%d_%H%M%S)" local backup_name="site_backup_$(date +%Y%m%d_%H%M%S)"
mv "$WEBSITE_ROOT" "/tmp/$backup_name" 2>/dev/null mv "$WEBSITE_ROOT" "/tmp/$backup_name" 2>/dev/null
log_dim "Старый сайт сохранён в /tmp/$backup_name" log_dim "Старый сайт сохранён в /tmp/$backup_name"
fi fi
mkdir -p "$WEBSITE_ROOT" mkdir -p "$WEBSITE_ROOT"
cp -r "$template_dir"/* "$WEBSITE_ROOT/" cp -r "$template_dir"/* "$WEBSITE_ROOT/"
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null || chown -R nginx:nginx "$WEBSITE_ROOT" 2>/dev/null chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null || chown -R nginx:nginx "$WEBSITE_ROOT" 2>/dev/null
chmod -R 755 "$WEBSITE_ROOT" chmod -R 755 "$WEBSITE_ROOT"
log_success "Шаблон развёрнут в $WEBSITE_ROOT" log_success "Шаблон развёрнут в $WEBSITE_ROOT"
} }
# ── Полная установка stealth-режима ────────────────────────────────────────── # ── Полная установка stealth-режима ──────────────────────────────────────────
setup_stealth_mode() { setup_stealth_mode() {
local domain="$1" local domain="$1"
local template_dir="$2" local template_dir="$2"
local proxy_port="${3:-443}" local proxy_port="${3:-443}"
local email="${4:-}" local email="${4:-}"
log_step "Настройка stealth-режима" log_step "Настройка stealth-режима"
# 1. Устанавливаем nginx # 1. Устанавливаем nginx
run_with_spinner "Установка nginx" install_nginx || return 1 run_with_spinner "Установка nginx" install_nginx || return 1
# 2. Устанавливаем certbot # 2. Устанавливаем certbot
run_with_spinner "Установка certbot" install_certbot || return 1 run_with_spinner "Установка certbot" install_certbot || return 1
# 3. Деплоим шаблон сайта # 3. Деплоим шаблон сайта
deploy_template_to_nginx "$template_dir" || return 1 deploy_template_to_nginx "$template_dir" || return 1
# 4. Получаем SSL # 4. Получаем SSL
obtain_ssl_certificate "$domain" "$email" || return 1 obtain_ssl_certificate "$domain" "$email" || return 1
# 5. Генерируем полный nginx конфиг с SSL # 5. Генерируем полный nginx конфиг с SSL
generate_nginx_config "$domain" "$proxy_port" generate_nginx_config "$domain" "$proxy_port"
# 6. Тестируем и перезапускаем nginx # 6. Тестируем и перезапускаем nginx
if nginx -t 2>/dev/null; then if nginx -t 2>/dev/null; then
systemctl restart nginx systemctl restart nginx
log_success "nginx запущен с SSL" log_success "nginx запущен с SSL"
else else
log_error "Ошибка в конфигурации nginx" log_error "Ошибка в конфигурации nginx"
nginx -t nginx -t
return 1 return 1
fi fi
# 7. Настраиваем авто-обновление SSL # 7. Настраиваем авто-обновление SSL
setup_ssl_auto_renewal setup_ssl_auto_renewal
# 8. Показываем благодарности авторам шаблонов # 8. Показываем благодарности авторам шаблонов
show_credits show_credits
log_success "Stealth-режим настроен: https://${domain}" log_success "Stealth-режим настроен: https://${domain}"
return 0 return 0
} }
# ── Управление nginx ──────────────────────────────────────────────────────── # ── Управление nginx ────────────────────────────────────────────────────────
nginx_status() { nginx_status() {
if systemctl is-active --quiet nginx 2>/dev/null; then if systemctl is-active --quiet nginx 2>/dev/null; then
echo "running" echo "running"
else else
echo "stopped" echo "stopped"
fi fi
} }
restart_nginx() { restart_nginx() {
if nginx -t 2>/dev/null; then if nginx -t 2>/dev/null; then
systemctl restart nginx 2>/dev/null systemctl restart nginx 2>/dev/null
log_success "nginx перезапущен" log_success "nginx перезапущен"
else else
log_error "Ошибка конфигурации nginx" log_error "Ошибка конфигурации nginx"
nginx -t nginx -t
return 1 return 1
fi fi
} }
# ── Удаление stealth-режима ────────────────────────────────────────────────── # ── Удаление stealth-режима ──────────────────────────────────────────────────
remove_stealth_mode() { remove_stealth_mode() {
log_info "Удаление stealth-режима..." log_info "Удаление stealth-режима..."
rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK" rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
rm -rf "$WEBSITE_ROOT" rm -rf "$WEBSITE_ROOT"
systemctl restart nginx 2>/dev/null systemctl restart nginx 2>/dev/null
log_success "Stealth-режим удалён (nginx оставлен)" log_success "Stealth-режим удалён (nginx оставлен)"
} }
# ── Смена шаблона ──────────────────────────────────────────────────────────── # ── Смена шаблона ────────────────────────────────────────────────────────────
switch_template() { switch_template() {
local new_template_dir="$1" local new_template_dir="$1"
deploy_template_to_nginx "$new_template_dir" deploy_template_to_nginx "$new_template_dir"
# nginx не требует перезапуска — статика обновилась на месте # nginx не требует перезапуска — статика обновилась на месте
log_success "Шаблон сайта обновлён" log_success "Шаблон сайта обновлён"
} }