#!/usr/bin/env bash set -euo pipefail canonicalize_path() { local input_path=${1:-} if [[ -z "$input_path" ]]; then return 1 fi if command -v realpath >/dev/null 2>&1; then realpath -m "$input_path" return 0 fi if command -v python3 >/dev/null 2>&1; then python3 - "$input_path" <<'PY' import os import sys path = os.path.expanduser(sys.argv[1]) print(os.path.realpath(path)) PY return 0 fi local dir_part local base_part dir_part=$(dirname -- "$input_path") || dir_part="." base_part=$(basename -- "$input_path") || base_part="$input_path" local dir_resolved if dir_resolved=$(cd "$dir_part" 2>/dev/null && pwd); then printf '%s/%s\n' "$dir_resolved" "$base_part" return 0 fi printf '%s\n' "$input_path" } SCRIPT_PATH=$(canonicalize_path "${BASH_SOURCE[0]}") SCRIPT_DIR=$(cd "$(dirname "$SCRIPT_PATH")" && pwd) STATE_FILE="$SCRIPT_DIR/.bot_install_state" BACKUP_DIR="$SCRIPT_DIR/backups" save_state() { local state_dir state_dir=$(dirname -- "$STATE_FILE") mkdir -p "$state_dir" local tmp_file if ! tmp_file=$(mktemp "$state_dir/.bot_install_state.XXXXXX" 2>/dev/null); then tmp_file="$STATE_FILE.tmp.$$" fi { printf 'INSTALL_PATH=%q\n' "$INSTALL_PATH" } >"$tmp_file" chmod 600 "$tmp_file" mv "$tmp_file" "$STATE_FILE" } RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' NC='\033[0m' BOLD='\033[1m' CHECK="✓" CROSS="✗" ARROW="➜" STAR="★" GEAR="⚙" print_header() { echo -e "\n${CYAN}${BOLD}╔════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}${BOLD}║${NC} ${WHITE}${BOLD}$1${NC}${CYAN}${BOLD}║${NC}" echo -e "${CYAN}${BOLD}╚════════════════════════════════════════════════════════════╝${NC}\n" } print_section() { echo -e "\n${BLUE}${BOLD}${ARROW} $1${NC}" echo -e "${BLUE}─────────────────────────────────────────────────────${NC}" } print_success() { echo -e "${GREEN}${CHECK} $1${NC}" } print_error() { echo -e "${RED}${CROSS} $1${NC}" >&2 } print_warning() { echo -e "${YELLOW}⚠ $1${NC}" } print_info() { echo -e "${CYAN}ℹ $1${NC}" } print_status() { local status=$1 local text=$2 if [[ "$status" == "running" ]]; then echo -e "${GREEN}● ${text}${NC}" elif [[ "$status" == "stopped" ]]; then echo -e "${RED}● ${text}${NC}" else echo -e "${YELLOW}● ${text}${NC}" fi } initialize_state() { local reason=${1:-missing} case "$reason" in missing) print_warning "Файл состояния установки не найден. Выполняем начальную настройку." ;; unreadable) print_warning "Не удалось прочитать файл состояния $STATE_FILE. Требуется повторная настройка." ;; invalid) print_warning "Файл состояния $STATE_FILE повреждён или не содержит путь установки." ;; *) print_warning "$reason" ;; esac local default_path default_path=${INSTALL_PATH:-$SCRIPT_DIR} local install_path_input="" if [[ -t 0 ]]; then read -rp "Укажите путь установки [${default_path}]: " install_path_input elif read -r -t 1 install_path_input; then print_info "Путь установки получен из стандартного ввода" else print_info "Используем путь по умолчанию: ${default_path}" fi install_path_input=${install_path_input:-$default_path} local resolved_path resolved_path=$(canonicalize_path "$install_path_input") || resolved_path="$install_path_input" INSTALL_PATH="$resolved_path" save_state print_success "Путь установки сохранён: $INSTALL_PATH" } load_state() { if [[ -f "$STATE_FILE" ]]; then if source "$STATE_FILE" 2>/dev/null; then if [[ -n "${INSTALL_PATH:-}" ]]; then local resolved_path resolved_path=$(canonicalize_path "$INSTALL_PATH") || resolved_path="$INSTALL_PATH" INSTALL_PATH="$resolved_path" print_info "Используем сохранённый путь установки: $INSTALL_PATH" else initialize_state invalid fi else initialize_state unreadable fi else initialize_state missing fi if [[ ! -f "$INSTALL_PATH/.env" ]]; then print_warning ".env файл не найден!" read -rp "Выполнить первичную настройку .env? [Y/n]: " setup_env_confirm if [[ "${setup_env_confirm,,}" != "n" ]]; then setup_env else print_error "Бот не может работать без .env файла!" print_info "Вы можете настроить его позже через пункт меню [10]" fi fi BACKUP_DIR="$INSTALL_PATH/backups" mkdir -p "$BACKUP_DIR" } check_env_exists() { [[ -f "$INSTALL_PATH/.env" ]] } setup_env() { print_header "ПЕРВИЧНАЯ НАСТРОЙКА КОНФИГУРАЦИИ (.env)" local env_file="$INSTALL_PATH/.env" if [[ -f "$env_file" ]]; then cp "$env_file" "$env_file.backup.$(date +%Y%m%d_%H%M%S)" print_info "Создана резервная копия существующего .env" fi print_section "Обязательные параметры" local bot_token="" while [[ -z "$bot_token" ]]; do read -rp "Введите токен бота (BOT_TOKEN): " bot_token if [[ -z "$bot_token" ]]; then print_error "Токен бота обязателен!" fi done local admin_ids="" while [[ -z "$admin_ids" ]]; do read -rp "Введите ID администраторов через запятую (ADMIN_IDS): " admin_ids if [[ -z "$admin_ids" ]]; then print_error "Хотя бы один ID администратора обязателен!" fi done local web_api_token="" read -rp "Введите токен для Web API (WEB_API_DEFAULT_TOKEN, Enter для автогенерации): " web_api_token if [[ -z "$web_api_token" ]]; then print_warning "Токен Web API не указан, будет сгенерирован случайный" web_api_token=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 64) print_success "Сгенерирован токен: ${web_api_token:0:16}..." fi local remnawave_url="" while [[ -z "$remnawave_url" ]]; do read -rp "Введите URL API Remnawave (REMNAWAVE_API_URL): " remnawave_url if [[ -z "$remnawave_url" ]]; then print_error "URL API Remnawave обязателен!" fi done local remnawave_key="" while [[ -z "$remnawave_key" ]]; do read -rp "Введите ключ API Remnawave (REMNAWAVE_API_KEY): " remnawave_key if [[ -z "$remnawave_key" ]]; then print_error "Ключ API Remnawave обязателен!" fi done print_section "Дополнительные параметры авторизации" echo -e "${CYAN}[1]${NC} API Key (по умолчанию)" echo -e "${CYAN}[2]${NC} Basic Auth" echo "" read -rp "Выберите тип авторизации [1]: " auth_choice auth_choice=${auth_choice:-1} local auth_type="api_key" local remnawave_username="" local remnawave_password="" local remnawave_secret="" if [[ "$auth_choice" == "2" ]]; then auth_type="basic_auth" read -rp "Введите имя пользователя для Basic Auth (REMNAWAVE_USERNAME): " remnawave_username read -rsp "Введите пароль для Basic Auth (REMNAWAVE_PASSWORD): " remnawave_password echo "" fi echo "" read -rp "Используете панель, установленную скриптом eGames? [y/N]: " use_egames if [[ "${use_egames,,}" == "y" ]]; then read -rp "Введите секретный ключ в формате XXXXXXX:DDDDDDDD (REMNAWAVE_SECRET_KEY): " remnawave_secret fi local postgres_password="" read -rp "Введите пароль для PostgreSQL (Enter для генерации): " postgres_password if [[ -z "$postgres_password" ]]; then postgres_password=$(openssl rand -base64 24 2>/dev/null || head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 32) print_success "Сгенерирован пароль PostgreSQL" fi print_section "Создание .env файла" cat > "$env_file" <> "$env_file" <> "$env_file" <> "$env_file" <> "$env_file" print_success "Параметр $param_name добавлен" fi print_warning "Необходимо перезапустить сервисы для применения изменений" read -rp "Перезапустить сервисы сейчас? [Y/n]: " restart_now if [[ "${restart_now,,}" != "n" ]]; then run_compose restart print_success "Сервисы перезапущены" fi } resolve_compose_command() { if docker compose version >/dev/null 2>&1; then COMPOSE_BIN=(docker compose) elif docker-compose version >/dev/null 2>&1; then COMPOSE_BIN=(docker-compose) else print_error "Docker Compose не найден." exit 1 fi } run_compose() { (cd "$INSTALL_PATH" && "${COMPOSE_BIN[@]}" "$@") } get_service_status() { local service=$1 local status status=$(run_compose ps -q "$service" 2>/dev/null | xargs -r docker inspect -f '{{.State.Status}}' 2>/dev/null || echo "not_found") echo "$status" } check_webserver() { local caddy_installed=false local caddy_path="" if docker ps -a --format '{{.Names}}' | grep -q "caddy"; then caddy_installed=true local caddy_container caddy_container=$(docker ps -a --format '{{.Names}}' | grep "caddy" | head -n1) caddy_path=$(docker inspect "$caddy_container" 2>/dev/null | \ grep -A 1 'Caddyfile' | \ grep 'Source' | \ sed 's/.*"Source": "\(.*\)".*/\1/' | \ sed 's/\/Caddyfile$//') if [[ -z "$caddy_path" ]] || [[ ! -d "$caddy_path" ]]; then if [[ -f "/opt/caddy/Caddyfile" ]]; then caddy_path="/opt/caddy" elif [[ -f "$INSTALL_PATH/caddy/Caddyfile" ]]; then caddy_path="$INSTALL_PATH/caddy" fi fi fi echo "$caddy_installed|$caddy_path" } update_existing_caddy_compose() { local caddy_compose_path=$1 print_info "Обновляем docker-compose.yml существующего Caddy..." if [[ ! -f "$caddy_compose_path" ]]; then print_error "Файл $caddy_compose_path не найден" return 1 fi cp "$caddy_compose_path" "$caddy_compose_path.backup.$(date +%Y%m%d_%H%M%S)" print_info "Создана резервная копия" if grep -q "network_mode:" "$caddy_compose_path"; then if ! grep -q 'network_mode:.*host' "$caddy_compose_path"; then print_warning "Caddy использует другой network_mode, требуется ручная настройка" return 1 fi else if command -v python3 >/dev/null 2>&1; then python3 - "$caddy_compose_path" "$INSTALL_PATH" <<'PY' import sys import yaml import os compose_path = sys.argv[1] install_path = sys.argv[2] with open(compose_path, 'r') as f: compose = yaml.safe_load(f) if 'services' in compose: for service_name, service in compose['services'].items(): if 'caddy' in service_name.lower(): service['network_mode'] = 'host' if 'networks' in service: del service['networks'] if 'ports' in service: del service['ports'] # Ensure volumes list exists if 'volumes' not in service: service['volumes'] = [] # Add miniapp volume if not present miniapp_volume = f"{install_path}/miniapp:/var/www/remnawave-miniapp:ro" if not any(miniapp_volume in str(v) for v in service['volumes']): service['volumes'].append(miniapp_volume) break if 'networks' not in compose: compose['networks'] = {} compose['networks']['default'] = { 'name': 'bot_network', 'external': True } with open(compose_path, 'w') as f: yaml.dump(compose, f, default_flow_style=False, sort_keys=False) PY print_success "docker-compose.yml обновлен" else print_warning "Python3 не найден, добавьте вручную network_mode: host и volume для miniapp в docker-compose.yml" return 1 fi fi return 0 } install_caddy() { print_section "Установка Caddy" local caddy_dir="$INSTALL_PATH/caddy" mkdir -p "$caddy_dir/logs" mkdir -p "$INSTALL_PATH/miniapp/redirect" cat > "$caddy_dir/Caddyfile" <<'EOF' # Caddy configuration EOF cat > "$caddy_dir/docker-compose.yml" <&2 echo -e "${BLUE}─────────────────────────────────────────────────────${NC}" >&2 local webhook_domain read -rp "Введите домен для webhook (например, webhook.example.com): " webhook_domain webhook_domain=$(echo "$webhook_domain" | tr -d '\r\n\t' | xargs | LC_ALL=C sed 's/[^a-zA-Z0-9.-]//g') if [[ -z "$webhook_domain" ]]; then echo -e "${RED}${CROSS} Домен не указан${NC}" >&2 return 1 fi if ! [[ "$webhook_domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then echo -e "${RED}${CROSS} Невалидный домен: $webhook_domain${NC}" >&2 return 1 fi echo -e "${CYAN}ℹ Используем домен: ${YELLOW}$webhook_domain${NC}" >&2 cat <&2 echo -e "${BLUE}─────────────────────────────────────────────────────${NC}" >&2 local miniapp_domain read -rp "Введите домен для miniapp (например, miniapp.example.com): " miniapp_domain miniapp_domain=$(echo "$miniapp_domain" | tr -d '\r\n\t' | xargs | LC_ALL=C sed 's/[^a-zA-Z0-9.-]//g') if [[ -z "$miniapp_domain" ]]; then echo -e "${RED}${CROSS} Домен не указан${NC}" >&2 return 1 fi if ! [[ "$miniapp_domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then echo -e "${RED}${CROSS} Невалидный домен: $miniapp_domain${NC}" >&2 return 1 fi echo -e "${CYAN}ℹ Используем домен: ${YELLOW}$miniapp_domain${NC}" >&2 cat </dev/null 2>&1; then print_error "Python3 не найден, не могу обновить существующую конфигурацию домена $domain" return 1 fi print_info "Обновляем конфигурацию домена $domain" python3 - "$caddy_file" "$domain" <<'PY' import os import sys path, domain = sys.argv[1:] if not os.path.exists(path): sys.exit(0) with open(path, encoding="utf-8") as fh: lines = fh.read().splitlines() result = [] skip = False brace_level = 0 for line in lines: stripped = line.lstrip() if not skip: if stripped.startswith(domain) and stripped[len(domain):].lstrip().startswith('{'): skip = True brace_level = line.count('{') - line.count('}') continue result.append(line) continue brace_level += line.count('{') - line.count('}') if brace_level <= 0: skip = False text = "\n".join(result) if text and not text.endswith("\n"): text += "\n" with open(path, "w", encoding="utf-8") as fh: fh.write(text) PY else print_info "Добавляем новый домен $domain" fi if [[ -s "$caddy_file" ]]; then if [[ $(tail -c1 "$caddy_file" 2>/dev/null | od -An -tx1) != "0a" ]]; then echo >> "$caddy_file" fi local last_line last_line=$(tail -n1 "$caddy_file" 2>/dev/null || echo '') if [[ -n "$last_line" ]]; then echo >> "$caddy_file" fi fi printf '%s\n' "$config" >> "$caddy_file" print_success "Конфигурация для домена $domain обновлена" } apply_caddy_config() { local caddy_dir=$1 local webhook_config=$2 local miniapp_config=$3 local caddy_file="$caddy_dir/Caddyfile" mkdir -p "$caddy_dir" if [[ -f "$caddy_file" ]]; then cp "$caddy_file" "$caddy_dir/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)" print_info "Резервная копия создана" else print_info "Создаем новый Caddyfile" fi if [[ ! -s "$caddy_file" ]]; then cat > "$caddy_file" </dev/null; then print_success "Конфигурация валидна" if docker exec "$caddy_container" caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then print_success "Caddy перезагружен успешно" else print_warning "Перезагрузка через reload не удалась, перезапускаем контейнер..." docker restart "$caddy_container" sleep 3 print_success "Контейнер перезапущен" fi else print_error "Ошибка валидации конфигурации Caddy" print_warning "Восстанавливаем предыдущую конфигурацию..." local last_backup last_backup=$(ls -t "$caddy_dir"/Caddyfile.backup.* 2>/dev/null | head -n1) if [[ -n "$last_backup" ]]; then cp "$last_backup" "$caddy_dir/Caddyfile" print_info "Предыдущая конфигурация восстановлена" fi return 1 fi else print_error "Caddy контейнер не найден или не запущен" print_info "Попробуйте запустить: docker start caddy" return 1 fi } show_proxy_status() { print_header "СТАТУС ОБРАТНОГО ПРОКСИ" local webserver_info webserver_info=$(check_webserver) IFS='|' read -r caddy_installed caddy_path <<< "$webserver_info" print_section "Установленные веб-серверы" if [[ "$caddy_installed" == "true" ]]; then local caddy_container caddy_container=$(docker ps --filter "name=caddy" --format "{{.Names}}" | head -n1) local caddy_status caddy_status=$(docker inspect -f '{{.State.Status}}' "$caddy_container" 2>/dev/null || echo "not_found") print_status "$caddy_status" "Caddy: $caddy_status" if [[ -n "$caddy_path" ]]; then echo -e " ${CYAN}Путь к конфигурации: ${YELLOW}$caddy_path${NC}" fi if [[ -f "$caddy_path/Caddyfile" ]]; then print_info "Настроенные домены в Caddy:" grep -E "^[a-zA-Z0-9\.-]+ \{" "$caddy_path/Caddyfile" | sed 's/ {//' | while read -r domain; do echo -e " ${GREEN}→${NC} $domain" done fi print_info "Caddy работает в режиме host network" else print_warning "Caddy не установлен" fi } configure_reverse_proxy() { while true; do print_header "НАСТРОЙКА ОБРАТНОГО ПРОКСИ" local webserver_info webserver_info=$(check_webserver) IFS='|' read -r caddy_installed caddy_path <<< "$webserver_info" echo -e "${CYAN}[1]${NC} 📊 Показать статус прокси" echo -e "${CYAN}[2]${NC} ⚙️ Настроить Caddy (webhook + miniapp)" if [[ "$caddy_installed" == "false" ]]; then echo -e "${CYAN}[3]${NC} 📦 Установить Caddy" else echo -e "${CYAN}[3]${NC} 📝 Редактировать Caddyfile вручную" echo -e "${CYAN}[4]${NC} 🔧 Обновить docker-compose.yml Caddy" fi echo -e "${CYAN}[5]${NC} 🔄 Перезагрузить Caddy" echo -e "${CYAN}[0]${NC} 🔙 Вернуться в главное меню" echo "" read -rp "$(echo -e ${WHITE}${BOLD}Выберите опцию: ${NC})" choice case $choice in 1) show_proxy_status ;; 2) if [[ "$caddy_installed" == "false" ]]; then print_warning "Caddy не установлен" read -rp "Установить Caddy сейчас? [y/N]: " install_confirm if [[ "${install_confirm,,}" == "y" ]]; then install_caddy caddy_path="$INSTALL_PATH/caddy" else continue fi fi if [[ -z "$caddy_path" ]] || [[ ! -d "$caddy_path" ]]; then print_warning "Автоматически определить путь не удалось" echo -e "${CYAN}Обнаруженные пути с Caddyfile:${NC}" local found_paths=() while IFS= read -r caddyfile; do local dir_path dir_path=$(dirname "$caddyfile") echo -e " ${GREEN}→${NC} $dir_path" found_paths+=("$dir_path") done < <(find /opt /root "$INSTALL_PATH" -name "Caddyfile" 2>/dev/null | head -n 5) if [[ ${#found_paths[@]} -eq 1 ]]; then caddy_path="${found_paths[0]}" print_info "Используем найденный путь: $caddy_path" else read -rp "Введите путь к директории с Caddyfile: " caddy_path fi fi if [[ ! -d "$caddy_path" ]]; then print_error "Директория не найдена: $caddy_path" continue fi if [[ ! -f "$caddy_path/Caddyfile" ]]; then print_error "Файл Caddyfile не найден в $caddy_path" read -rp "Создать новый Caddyfile? [y/N]: " create_new if [[ "${create_new,,}" != "y" ]]; then continue fi touch "$caddy_path/Caddyfile" fi local webhook_config local miniapp_config webhook_config=$(configure_webhook_proxy) miniapp_config=$(configure_miniapp_proxy) echo "" print_info "Предпросмотр конфигурации:" echo -e "${YELLOW}$webhook_config${NC}" echo -e "${YELLOW}$miniapp_config${NC}" read -rp "Применить эту конфигурацию? [y/N]: " confirm if [[ "${confirm,,}" == "y" ]]; then apply_caddy_config "$caddy_path" "$webhook_config" "$miniapp_config" fi ;; 3) if [[ "$caddy_installed" == "false" ]]; then install_caddy else if [[ -z "$caddy_path" ]]; then read -rp "Введите путь к директории с Caddyfile: " caddy_path fi if [[ ! -f "$caddy_path/Caddyfile" ]]; then print_error "Caddyfile не найден в $caddy_path" continue fi print_info "Открываем Caddyfile для редактирования..." print_warning "Будет создана резервная копия" cp "$caddy_path/Caddyfile" "$caddy_path/Caddyfile.backup.$(date +%Y%m%d_%H%M%S)" ${EDITOR:-nano} "$caddy_path/Caddyfile" print_info "Проверяем конфигурацию..." local caddy_container caddy_container=$(docker ps --filter "name=caddy" --format "{{.Names}}" | head -n1) if [[ -n "$caddy_container" ]]; then if docker exec "$caddy_container" caddy validate --config /etc/caddy/Caddyfile 2>&1; then print_success "Конфигурация валидна" read -rp "Перезагрузить Caddy? [Y/n]: " reload_confirm if [[ "${reload_confirm,,}" != "n" ]]; then docker exec "$caddy_container" caddy reload --config /etc/caddy/Caddyfile print_success "Caddy перезагружен" fi else print_error "Конфигурация содержит ошибки!" read -rp "Восстановить из резервной копии? [Y/n]: " restore_confirm if [[ "${restore_confirm,,}" != "n" ]]; then local last_backup last_backup=$(ls -t "$caddy_path"/Caddyfile.backup.* 2>/dev/null | head -n1) if [[ -n "$last_backup" ]]; then cp "$last_backup" "$caddy_path/Caddyfile" print_success "Конфигурация восстановлена" fi fi fi fi fi ;; 4) if [[ "$caddy_installed" == "true" ]] && [[ -n "$caddy_path" ]]; then if [[ -f "$caddy_path/docker-compose.yml" ]]; then update_existing_caddy_compose "$caddy_path/docker-compose.yml" print_info "Перезапускаем Caddy с новой конфигурацией..." (cd "$caddy_path" && docker compose down && docker compose up -d) sleep 2 if docker ps | grep -q "caddy"; then print_success "Caddy перезапущен с обновленной конфигурацией" else print_error "Ошибка при перезапуске Caddy" fi else print_error "docker-compose.yml не найден в $caddy_path" fi else print_error "Caddy не установлен или путь не определен" fi ;; 5) print_section "Перезагрузка Caddy" if [[ "$caddy_installed" == "true" ]]; then local caddy_container caddy_container=$(docker ps --filter "name=caddy" --format "{{.Names}}" | head -n1) if [[ -n "$caddy_container" ]]; then print_info "Перезагружаем Caddy..." docker restart "$caddy_container" sleep 2 if docker ps --filter "name=caddy" --filter "status=running" | grep -q caddy; then print_success "Caddy перезапущен успешно" else print_error "Ошибка при перезапуске Caddy" fi fi fi ;; 0) return 0 ;; *) print_error "Неверный выбор" ;; esac echo "" read -rp "$(echo -e ${CYAN}Нажмите Enter для продолжения...${NC})" done } show_monitoring() { print_header "МОНИТОРИНГ СЕРВИСОВ БОТА" print_section "Статус контейнеров" local services=("bot" "postgres" "redis") local all_running=true for service in "${services[@]}"; do local status status=$(get_service_status "$service") local uptime="" if [[ "$status" == "running" ]]; then uptime=$(run_compose ps "$service" 2>/dev/null | tail -n1 | awk '{for(i=1;i<=NF;i++){if($i~/Up/){print $(i+1), $(i+2); break}}}') print_status "running" "$service: работает (uptime: $uptime)" elif [[ "$status" == "exited" ]] || [[ "$status" == "stopped" ]]; then print_status "stopped" "$service: остановлен" all_running=false else print_status "unknown" "$service: не найден" all_running=false fi done print_section "Использование ресурсов" local stats stats=$(docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" 2>/dev/null | grep -E "bot|postgres|redis" || echo "") if [[ -n "$stats" ]]; then echo -e "${WHITE}${BOLD}КОНТЕЙНЕР CPU ПАМЯТЬ${NC}" echo "$stats" | tail -n+2 | while IFS="$(printf '\t')" read -r name cpu mem; do echo -e "${CYAN}${name}${NC} ${YELLOW}${cpu}${NC} ${PURPLE}${mem}${NC}" done else print_warning "Статистика недоступна" fi print_section "Размер логов" if [[ -d "$INSTALL_PATH/logs" ]]; then local log_size log_size=$(du -sh "$INSTALL_PATH/logs" 2>/dev/null | cut -f1) echo -e "${CYAN}Логи: ${YELLOW}${log_size}${NC}" fi print_section "Последние ошибки (если есть)" local errors errors=$(run_compose logs --tail=100 bot 2>/dev/null | grep -i "error\|exception\|critical" | tail -n 5 || echo "") if [[ -n "$errors" ]]; then echo "$errors" | while read -r line; do print_error "$line" done else print_success "Ошибок не обнаружено" fi echo "" if $all_running; then print_success "Все сервисы работают нормально!" else print_warning "Некоторые сервисы не запущены" fi } update_containers() { print_header "ОБНОВЛЕНИЕ ОБРАЗОВ КОНТЕЙНЕРОВ" print_info "Получаем последние версии образов через Docker Compose..." if run_compose pull; then print_success "Образы успешно обновлены" else print_error "Не удалось обновить образы контейнеров" return 1 fi echo "" read -rp "$(echo -e ${YELLOW}Перезапустить сервисы после обновления? [Y/n]: ${NC})" restart_after_pull if [[ "${restart_after_pull,,}" != "n" ]]; then print_info "Запускаем сервисы с обновленными образами..." if run_compose up -d; then print_success "Сервисы запущены" show_monitoring else print_error "Не удалось запустить сервисы" return 1 fi else print_warning "Перезапустите сервисы позже для применения обновлений" fi } update_from_git() { print_header "ОБНОВЛЕНИЕ ИЗ GIT РЕПОЗИТОРИЯ" if [[ ! -d "$INSTALL_PATH/.git" ]]; then print_error "Git репозиторий не найден в $INSTALL_PATH" print_info "Инициализируем репозиторий..." local repo_url read -rp "Введите URL Git репозитория: " repo_url if [[ -z "$repo_url" ]]; then print_error "URL не указан" return 1 fi (cd "$INSTALL_PATH" && git init && git remote add origin "$repo_url") fi print_section "Проверка обновлений" (cd "$INSTALL_PATH" && git fetch origin 2>&1) local current_commit local remote_commit current_commit=$(cd "$INSTALL_PATH" && git rev-parse HEAD 2>/dev/null || echo "unknown") remote_commit=$(cd "$INSTALL_PATH" && git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "unknown") if [[ "$current_commit" == "$remote_commit" ]]; then print_success "Бот уже имеет последнюю версию" return 0 fi print_info "Найдены обновления" echo -e "${CYAN}Текущий коммит: ${YELLOW}${current_commit:0:8}${NC}" echo -e "${CYAN}Новый коммит: ${YELLOW}${remote_commit:0:8}${NC}" print_section "Список изменений" (cd "$INSTALL_PATH" && git log --oneline HEAD..origin/main 2>/dev/null || git log --oneline HEAD..origin/master 2>/dev/null || true) echo "" read -rp "$(echo -e ${YELLOW}Применить обновления? [y/N]: ${NC})" confirm if [[ "${confirm,,}" != "y" ]]; then print_warning "Обновление отменено" return 1 fi print_info "Создаем резервную копию перед обновлением..." create_backup "pre-update" print_section "Применение обновлений" print_info "Останавливаем сервисы..." run_compose down print_info "Обновляем код..." (cd "$INSTALL_PATH" && git pull origin main 2>/dev/null || git pull origin master 2>/dev/null) print_info "Пересобираем и запускаем сервисы..." run_compose up -d --build print_success "Обновление завершено!" echo "" read -rp "$(echo -e ${YELLOW}Показать логи запуска? [y/N]: ${NC})" show_logs if [[ "${show_logs,,}" == "y" ]]; then run_compose logs --tail=50 -f bot fi } create_backup() { local backup_type=${1:-manual} local timestamp timestamp=$(date +%Y%m%d_%H%M%S) local backup_name="backup_${backup_type}_${timestamp}" local backup_path="$BACKUP_DIR/$backup_name" print_header "СОЗДАНИЕ РЕЗЕРВНОЙ КОПИИ" mkdir -p "$BACKUP_DIR" mkdir -p "$backup_path" print_section "Архивирование данных" print_info "Сохраняем конфигурацию..." cp "$INSTALL_PATH/.env" "$backup_path/" 2>/dev/null || true cp "$INSTALL_PATH/docker-compose.yml" "$backup_path/" 2>/dev/null || true if [[ $(get_service_status "postgres") == "running" ]]; then print_info "Экспортируем базу данных PostgreSQL..." run_compose exec -T postgres pg_dump -U postgres remnawave_bot > "$backup_path/database.sql" 2>/dev/null || { print_warning "Не удалось экспортировать БД" } fi if [[ -d "$INSTALL_PATH/data" ]]; then print_info "Копируем пользовательские данные..." cp -r "$INSTALL_PATH/data" "$backup_path/" 2>/dev/null || true fi print_info "Создаем архив..." (cd "$BACKUP_DIR" && tar -czf "${backup_name}.tar.gz" "$backup_name" && rm -rf "$backup_name") local backup_size backup_size=$(du -h "$BACKUP_DIR/${backup_name}.tar.gz" | cut -f1) print_success "Резервная копия создана: $BACKUP_DIR/${backup_name}.tar.gz" echo -e "${CYAN}Размер: ${YELLOW}${backup_size}${NC}" print_info "Очистка старых бэкапов..." (cd "$BACKUP_DIR" && ls -t backup_*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f) local backup_count backup_count=$(ls -1 "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l) print_info "Всего резервных копий: $backup_count" } restore_backup() { print_header "ВОССТАНОВЛЕНИЕ ИЗ РЕЗЕРВНОЙ КОПИИ" if [[ ! -d "$BACKUP_DIR" ]] || [[ -z "$(ls -A "$BACKUP_DIR"/*.tar.gz 2>/dev/null)" ]]; then print_error "Резервные копии не найдены" return 1 fi print_section "Доступные резервные копии" local backups=() local i=1 while IFS= read -r backup; do local backup_name local backup_size local backup_date backup_name=$(basename "$backup") backup_size=$(du -h "$backup" | cut -f1) backup_date=$(stat -c %y "$backup" 2>/dev/null | cut -d' ' -f1,2 | cut -d'.' -f1 || stat -f "%Sm" "$backup") echo -e "${CYAN}[$i]${NC} ${WHITE}$backup_name${NC}" echo -e " Размер: ${YELLOW}$backup_size${NC}, Дата: ${PURPLE}$backup_date${NC}" backups+=("$backup") ((i++)) done < <(ls -t "$BACKUP_DIR"/*.tar.gz 2>/dev/null) echo "" read -rp "Выберите номер резервной копии для восстановления [1-$((i-1))]: " selection if [[ ! "$selection" =~ ^[0-9]+$ ]] || [[ "$selection" -lt 1 ]] || [[ "$selection" -ge "$i" ]]; then print_error "Неверный выбор" return 1 fi local selected_backup="${backups[$((selection-1))]}" print_warning "ВНИМАНИЕ: Текущие данные будут перезаписаны!" read -rp "$(echo -e ${RED}${BOLD}Продолжить восстановление? [y/N]: ${NC})" confirm if [[ "${confirm,,}" != "y" ]]; then print_warning "Восстановление отменено" return 1 fi print_info "Создаем резервную копию текущего состояния..." create_backup "pre-restore" print_section "Восстановление данных" print_info "Останавливаем сервисы..." run_compose down print_info "Распаковываем резервную копию..." local temp_dir temp_dir=$(mktemp -d) tar -xzf "$selected_backup" -C "$temp_dir" local backup_folder backup_folder=$(ls "$temp_dir") if [[ -f "$temp_dir/$backup_folder/.env" ]]; then print_info "Восстанавливаем конфигурацию..." cp "$temp_dir/$backup_folder/.env" "$INSTALL_PATH/" fi if [[ -d "$temp_dir/$backup_folder/data" ]]; then print_info "Восстанавливаем пользовательские данные..." rm -rf "$INSTALL_PATH/data" cp -r "$temp_dir/$backup_folder/data" "$INSTALL_PATH/" fi print_info "Запускаем сервисы..." run_compose up -d if [[ -f "$temp_dir/$backup_folder/database.sql" ]]; then print_info "Ожидаем запуска PostgreSQL..." sleep 5 print_info "Восстанавливаем базу данных..." run_compose exec -T postgres psql -U postgres remnawave_bot < "$temp_dir/$backup_folder/database.sql" 2>/dev/null || { print_warning "Не удалось восстановить БД (возможно, структура уже актуальна)" } fi rm -rf "$temp_dir" print_success "Восстановление завершено!" echo "" show_monitoring } view_logs() { print_header "ПРОСМОТР ЛОГОВ" echo -e "${CYAN}[1]${NC} Логи бота (последние 100 строк)" echo -e "${CYAN}[2]${NC} Логи PostgreSQL (последние 100 строк)" echo -e "${CYAN}[3]${NC} Логи Redis (последние 100 строк)" echo -e "${CYAN}[4]${NC} Все логи (последние 100 строк)" echo -e "${CYAN}[5]${NC} Следить за логами в реальном времени" echo -e "${CYAN}[6]${NC} Поиск по логам" echo "" read -rp "Выберите опцию [1-6]: " choice case $choice in 1) run_compose logs --tail=100 bot ;; 2) run_compose logs --tail=100 postgres ;; 3) run_compose logs --tail=100 redis ;; 4) run_compose logs --tail=100 ;; 5) print_info "Нажмите Ctrl+C для выхода" run_compose logs -f ;; 6) read -rp "Введите текст для поиска: " search_term run_compose logs | grep -i "$search_term" --color=always | tail -n 50 ;; *) print_error "Неверный выбор" ;; esac } manage_services() { print_header "УПРАВЛЕНИЕ СЕРВИСАМИ" echo -e "${CYAN}[1]${NC} Запустить все сервисы" echo -e "${CYAN}[2]${NC} Остановить все сервисы" echo -e "${CYAN}[3]${NC} Перезапустить все сервисы" echo -e "${CYAN}[4]${NC} Пересобрать и запустить" echo -e "${CYAN}[5]${NC} Остановить и удалить контейнеры" echo "" read -rp "Выберите опцию [1-5]: " choice case $choice in 1) print_info "Запускаем сервисы..." run_compose up -d print_success "Сервисы запущены" show_monitoring ;; 2) print_info "Останавливаем сервисы..." run_compose stop print_success "Сервисы остановлены" ;; 3) print_info "Перезапускаем сервисы..." run_compose restart print_success "Сервисы перезапущены" show_monitoring ;; 4) print_info "Пересобираем и запускаем..." run_compose up -d --build print_success "Сервисы пересобраны и запущены" show_monitoring ;; 5) print_warning "Контейнеры будут удалены (данные сохранятся в volumes)" read -rp "$(echo -e ${YELLOW}Продолжить? [y/N]: ${NC})" confirm if [[ "${confirm,,}" == "y" ]]; then run_compose down print_success "Контейнеры остановлены и удалены" fi ;; *) print_error "Неверный выбор" ;; esac } cleanup_system() { print_header "ОЧИСТКА СИСТЕМЫ" echo -e "${CYAN}[1]${NC} Очистить старые логи (старше 7 дней)" echo -e "${CYAN}[2]${NC} Очистить старые резервные копии (оставить 5 последних)" echo -e "${CYAN}[3]${NC} Очистить неиспользуемые Docker образы" echo -e "${CYAN}[4]${NC} Полная очистка (всё вышеперечисленное)" echo "" read -rp "Выберите опцию [1-4]: " choice case $choice in 1) print_info "Очищаем старые логи..." find "$INSTALL_PATH/logs" -type f -mtime +7 -delete 2>/dev/null || true print_success "Старые логи удалены" ;; 2) print_info "Очищаем старые бэкапы..." (cd "$BACKUP_DIR" && ls -t backup_*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm -f) print_success "Старые бэкапы удалены" ;; 3) print_info "Очищаем неиспользуемые Docker образы..." docker image prune -f print_success "Неиспользуемые образы удалены" ;; 4) print_info "Выполняем полную очистку..." find "$INSTALL_PATH/logs" -type f -mtime +7 -delete 2>/dev/null || true (cd "$BACKUP_DIR" && ls -t backup_*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm -f) docker image prune -f docker volume prune -f print_success "Полная очистка завершена" ;; *) print_error "Неверный выбор" ;; esac } show_menu() { clear echo -e "${PURPLE}${BOLD}" cat << "EOF" ╔════════════════════════════════════════════════════════════╗ ║ ║ ║ ██████╗ ██████╗ ████████╗ ███╗ ███╗ ██████╗ ██████╗ ║ ██╔══██╗██╔═══██╗╚══██╔══╝ ████╗ ████║██╔════╝ ██╔══██╗ ║ ██████╔╝██║ ██║ ██║ ██╔████╔██║██║ ███╗██████╔╝ ║ ██╔══██╗██║ ██║ ██║ ██║╚██╔╝██║██║ ██║██╔══██╗ ║ ██████╔╝╚██████╔╝ ██║ ██║ ╚═╝ ██║╚██████╔╝██║ ██║ ║ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ║ ║ ║ Система управления Telegram ботом ║ ╚════════════════════════════════════════════════════════════╝ EOF echo -e "${NC}" echo -e "${WHITE}${BOLD}Путь установки:${NC} ${CYAN}$INSTALL_PATH${NC}" echo "" echo -e "${WHITE}${BOLD}Статус контейнеров:${NC}" local services=("bot" "postgres" "redis") local status="" for service in "${services[@]}"; do status=$(get_service_status "$service") if [[ "$status" == "running" ]]; then print_status "running" "$service: работает" elif [[ "$status" == "exited" ]] || [[ "$status" == "stopped" ]]; then print_status "stopped" "$service: остановлен" else print_status "unknown" "$service: статус неизвестен" fi done echo "" echo -e "${GREEN}${BOLD}[1]${NC} ${STAR} Мониторинг и статус сервисов" echo -e "${BLUE}${BOLD}[2]${NC} ${GEAR} Управление сервисами" echo -e "${YELLOW}${BOLD}[3]${NC} 📋 Просмотр логов" echo -e "${PURPLE}${BOLD}[4]${NC} ⬇️ Обновить контейнеры (docker compose pull)" echo -e "${PURPLE}${BOLD}[5]${NC} 🔄 Обновление из Git" echo -e "${CYAN}${BOLD}[6]${NC} 💾 Создать резервную копию" echo -e "${YELLOW}${BOLD}[7]${NC} 📦 Восстановить из резервной копии" echo -e "${RED}${BOLD}[8]${NC} 🧹 Очистка системы" echo -e "${PURPLE}${BOLD}[9]${NC} 🌐 Настройка обратного прокси (Caddy)" echo -e "${GREEN}${BOLD}[10]${NC} ⚙️ Настройка конфигурации (.env)" echo -e "${WHITE}${BOLD}[0]${NC} 🚪 Выход" echo "" } main() { load_state resolve_compose_command while true; do show_menu read -rp "$(echo -e ${WHITE}${BOLD}Выберите опцию: ${NC})" choice case $choice in 1) show_monitoring ;; 2) manage_services ;; 3) view_logs ;; 4) update_containers ;; 5) update_from_git ;; 6) create_backup "manual" ;; 7) restore_backup ;; 8) cleanup_system ;; 9) configure_reverse_proxy ;; 10) if check_env_exists; then edit_env else setup_env fi ;; 0) print_success "До свидания!" exit 0 ;; *) print_error "Неверный выбор. Попробуйте снова." ;; esac echo "" read -rp "$(echo -e ${CYAN}Нажмите Enter для продолжения...${NC})" done } main "$@"