Files
amnezia-bot/install.sh
T
viktor 11b52cf5ad Обновление зависимостей, устранение хардкода, оплата через Telegram
Библиотеки:
- миграция с aiogram 2.25.2 (EOL) на aiogram 3.31: aiohttp обновился
  с 3.8.6 до 3.14.3, так как aiogram 2 жёстко требовал aiohttp<3.9
- самодельный словарь состояний заменён на FSM aiogram (MemoryStorage);
  это убирает падение на user_state.startswith(), когда состояния нет
- из requirements убраны неиспользуемые пакеты (Babel 2.9.1, humanize,
  aiofiles, yookassa, YooMoney); оставлены только прямые зависимости

Хардкод:
- пути вычисляются от расположения файлов бота (awg/config.py), больше
  нет привязки к /root/amnezia-bot и к запуску из конкретного каталога
- вместо 'python3.11' используется sys.executable; awg-decode.py
  переименован в awg_decode.py и импортируется напрямую вместо запуска
  через subprocess
- newclient.sh и removeclient.sh определяют каталог по своему
  расположению, а не по cwd
- install.sh: каталог, репозиторий, ветка и Python настраиваются
  переменными окружения, зависимости ставятся из requirements.txt
- секреты можно задавать переменными окружения вместо config.json

Оплата (встроенные платежи Telegram, провайдер ЮKassa):
- счёт, pre_checkout_query и successful_payment; публичный HTTPS-адрес
  и вебхук не нужны
- обработчик платежей вынесен в отдельный роутер и подключён первым:
  successful_payment приходит без текста и иначе перехватывался бы
  обработчиком, ожидающим ввода в FSM
- выдача ключа идемпотентна по id платежа, повторная доставка апдейта
  не создаёт второй ключ
- если после оплаты ключ создать не удалось, уведомляются администраторы
- промокоды разделены на validate/consume: использование засчитывается
  только после фактической выдачи ключа, а не при проверке кода
- поддержаны скидочные промокоды, уменьшающие сумму счёта
- опциональные чеки по 54-ФЗ через provider_data

Исправленные ошибки:
- db.root_add и db.deactive_user_db передавали скриптам 2 аргумента
  вместо 4, из-за чего создание и удаление клиентов всегда падало;
  публичный ключ для удаления теперь вычисляется из конфига клиента
- в create_backup не был импортирован zipfile (NameError при бэкапе)
- планировщик запускался без задач, подписки никогда не истекали;
  добавлена ежечасная проверка с отключением доступа и уведомлением
- callback_data вида renew_period_<имя>_<период> разбирался неверно для
  имён с подчёркиваниями; разделителем стало двоеточие
- статус клиента брался из status.json, который никто не создавал;
  теперь используется wg show latest-handshakes
- продление подписки сбрасывало остаток, теперь период добавляется к нему
- Markdown-разметка ломалась на именах с подчёркиваниями; используется
  HTML с экранированием
- vpn:// ключ уходит отдельным сообщением, а не в подписи к файлу,
  где мог не поместиться в лимит 1024 символа
- блокирующие вызовы docker и скриптов вынесены в отдельные потоки
- при удалении клиента связь с Telegram ID удаляется, а не пишется None

Удалён мёртвый код: awg/wg.py (нигде не импортировался, содержал
подсеть 10.0.0.0/24 вместо используемой 10.8.1.0/24) и
handlers/add_client.py (импортировал несуществующий модуль loader).
2026-09-04 14:44:26 +00:00

217 строки
11 KiB
Bash

#!/bin/bash
# Конфигурация. Любое значение можно переопределить переменной окружения.
SERVICE_NAME="${AWG_SERVICE_NAME:-awg_bot}"
# Каталог установки бота (по умолчанию — как в прежних версиях).
BASE_DIR="${AWG_BOT_HOME:-/root/amnezia-bot}"
PARENT_DIR="$(dirname "$BASE_DIR")"
BOT_DIRNAME="$(basename "$BASE_DIR")"
# Репозиторий: берём origin уже установленной копии, иначе значение по умолчанию.
DEFAULT_REPO_URL="https://github.com/viktor138irk/amnezia-bot.git"
if [[ -z "$AWG_REPO_URL" && -d "$BASE_DIR/.git" ]]; then
AWG_REPO_URL="$(git -C "$BASE_DIR" remote get-url origin 2>/dev/null)"
fi
REPO_URL="${AWG_REPO_URL:-$DEFAULT_REPO_URL}"
REPO_BRANCH="${AWG_REPO_BRANCH:-main}"
# https://github.com/<owner>/<repo>.git -> https://api.github.com/repos/<owner>/<repo>
REPO_API="https://api.github.com/repos/$(echo "$REPO_URL" | sed -E 's#^.*github\.com[:/]##; s#\.git$##')"
LOCAL_VERSION_FILE="$BASE_DIR/.version"
# Python: нужен 3.10+, конкретная минорная версия не важна.
PYTHON_BIN="${AWG_PYTHON_BIN:-}"
if [[ -z "$PYTHON_BIN" ]]; then
for candidate in python3.13 python3.12 python3.11 python3.10 python3; do
if command -v "$candidate" &>/dev/null; then PYTHON_BIN="$candidate"; break; fi
done
fi
# Цвета для вывода
GREEN=$'\033[0;32m'
YELLOW=$'\033[1;33m'
RED=$'\033[0;31m'
BLUE=$'\033[0;34m'
NC=$'\033[0m'
ENABLE_LOGS=true
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$(basename "$0")"
SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_NAME"
# Проверка прав суперпользователя
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Этот скрипт должен быть запущен с правами суперпользователя (root)${NC}"
exit 1
fi
# Функция для вывода ошибок и завершения
error_exit() {
echo -e "${RED}Ошибка: $1${NC}" >&2
exit 1
}
# Функция запуска команд с индикатором
run_with_spinner() {
local description="$1"; shift
local cmd="$@"
if [ "$ENABLE_LOGS" = true ]; then
echo -e "${BLUE}${description}...${NC}"
eval "$cmd" 2>&1
local stat=$?
if [ $stat -eq 0 ]; then echo -e "${GREEN}${description}... Done!${NC}\n"; else echo -e "${RED}${description}... Failed!${NC}\n"; error_exit "$cmd"; fi
else
local out=$(mktemp) err=$(mktemp)
eval "$cmd" >"$out" 2>"$err" & pid=$!
local spinner='|/-\\' i=0
while kill -0 "$pid" 2>/dev/null; do printf "\r${BLUE}${description}...${NC} ${spinner:i++%${#spinner}:1}"; sleep 0.1; done
wait "$pid"; stat=$?
if [ $stat -eq 0 ]; then printf "\r${BLUE}${description}...${NC} ${GREEN}Done!${NC}\n\n"; else printf "\r${BLUE}${description}...${NC} ${RED}Failed!${NC}\n\n"; echo -e "${RED}Ошибка: $cmd${NC}"; cat "$err"; rm -f "$out" "$err"; error_exit "$cmd"; fi
rm -f "$out" "$err"
fi
}
# Функция проверки обновлений на GitHub
check_github_updates() {
local current_sha local_sha latest_sha auto_mode="$1"
cd "$BASE_DIR" || { echo -e "${RED}Каталог $BASE_DIR не найден${NC}"; return 1; }
echo -e "${YELLOW}Текущая директория: $(pwd)${NC}"
echo -e "${YELLOW}Содержимое $BASE_DIR:${NC}"
ls -la
# Получение текущего SHA коммита
local_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
# Получение последнего коммита через GitHub API
if command -v curl &>/dev/null; then
latest_sha=$(curl -s "$REPO_API/commits/$REPO_BRANCH" | jq -r '.sha' 2>/dev/null)
[[ -z "$latest_sha" ]] && { echo -e "${RED}Не удалось получить данные с GitHub${NC}"; cd ..; return 1; }
else
echo -e "${RED}curl не установлен${NC}"; cd ..; return 1
fi
# Сравнение версий
if [[ "$local_sha" == "$latest_sha" ]]; then
echo -e "${GREEN}Репозиторий актуален (SHA: $local_sha)${NC}"
cd ..; return 0
fi
echo -e "${YELLOW}Доступно обновление (текущий SHA: $local_sha, последний SHA: $latest_sha)${NC}"
if [[ "$auto_mode" == "--auto" ]]; then
# Проверка на наличие локальных изменений
if git status --porcelain | grep -q .; then
echo -e "${YELLOW}Обнаружены локальные изменения. Сбрасываем их...${NC}"
run_with_spinner "Сброс локальных изменений" "git reset --hard && git clean -fd"
fi
run_with_spinner "Обновление репозитория" "git pull origin $REPO_BRANCH"
echo "$latest_sha" > "$LOCAL_VERSION_FILE"
check_script_update
# Проверка и создание виртуального окружения, если отсутствует
if [[ ! -d "myenv" ]]; then
run_with_spinner "Создание виртуального окружения" "$PYTHON_BIN -m venv myenv"
fi
run_with_spinner "Обновление Python-зависимостей" "source myenv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt && deactivate"
run_with_spinner "Перезапуск службы" "systemctl restart $SERVICE_NAME"
else
echo -ne "${BLUE}1) Установить 2) Отменить: ${NC}"; read choice
if [[ "$choice" == "1" ]]; then
if git status --porcelain | grep -q .; then
echo -e "${YELLOW}Обнаружены локальные изменения. Сбрасываем их...${NC}"
run_with_spinner "Сброс локальных изменений" "git reset --hard && git clean -fd"
fi
run_with_spinner "Обновление репозитория" "git pull origin $REPO_BRANCH"
echo "$latest_sha" > "$LOCAL_VERSION_FILE"
check_script_update
# Проверка и создание виртуального окружения, если отсутствует
if [[ ! -d "myenv" ]]; then
run_with_spinner "Создание виртуального окружения" "$PYTHON_BIN -m venv myenv"
fi
run_with_spinner "Обновление Python-зависимостей" "source myenv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt && deactivate"
run_with_spinner "Перезапуск службы" "systemctl restart $SERVICE_NAME"
else
echo -e "${YELLOW}Обновление отменено${NC}"
fi
fi
cd ..
}
# Проверка обновления самого скрипта
check_script_update() {
local temp_script=$(mktemp)
if [[ -f "$BASE_DIR/install.sh" ]]; then
cp "$BASE_DIR/install.sh" "$temp_script"
if ! cmp -s "$SCRIPT_PATH" "$temp_script"; then
echo -e "${YELLOW}Обнаружено обновление скрипта install.sh${NC}"
run_with_spinner "Обновление скрипта" "mv $temp_script $SCRIPT_PATH && chmod +x $SCRIPT_PATH"
echo -e "${GREEN}Скрипт обновлён, перезапускаю...${NC}"
exec "$SCRIPT_PATH" --check-update
else
rm -f "$temp_script"
fi
else
echo -e "${YELLOW}Скрипт install.sh не найден в репозитории, копируем локальную версию${NC}"
cp "$SCRIPT_PATH" "$BASE_DIR/install.sh"
chmod +x "$BASE_DIR/install.sh"
fi
}
# Проверка и применение обновлений
check_updates() {
# Проверка и установка jq, если отсутствует
if ! command -v jq &>/dev/null; then
echo -e "${YELLOW}jq не установлен. Устанавливаем jq...${NC}"
run_with_spinner "Установка jq" "apt-get update -qq && apt-get install -y jq -qq"
if ! command -v jq &>/dev/null; then
error_exit "Не удалось установить jq"
fi
fi
# Проверка остальных зависимостей
if [[ -z "$PYTHON_BIN" ]]; then
echo -e "${RED}Python 3 не найден. Установите python3 (3.10 или новее).${NC}"
exit 1
fi
for cmd in git curl "$PYTHON_BIN"; do
if ! command -v "$cmd" &>/dev/null; then
echo -e "${RED}$cmd не установлен. Установите $cmd для продолжения.${NC}"
exit 1
fi
done
# Проверка доступности репозитория
echo -e "${YELLOW}Проверка доступности репозитория...${NC}"
if ! curl -s -I "$REPO_URL" | grep -q "HTTP/.* 200"; then
error_exit "Репозиторий $REPO_URL недоступен. Проверьте URL или сетевое подключение."
fi
# Проверка и клонирование репозитория, если он отсутствует
if [[ ! -d "$BASE_DIR/.git" ]]; then
echo -e "${YELLOW}Репозиторий не найден. Проверяем $BASE_DIR...${NC}"
echo -e "${YELLOW}Содержимое $PARENT_DIR:${NC}"
ls -la "$PARENT_DIR"
if [[ -d "$BASE_DIR" ]]; then
echo -e "${YELLOW}Директория $BASE_DIR существует, но не является git-репозиторием. Удаляем её...${NC}"
rm -rf $BASE_DIR || error_exit "Не удалось удалить поврежденную директорию $BASE_DIR"
fi
echo -e "${YELLOW}Клонируем репозиторий...${NC}"
mkdir -p "$PARENT_DIR" || error_exit "Не удалось создать $PARENT_DIR"
cd "$PARENT_DIR" || error_exit "Не удалось перейти в $PARENT_DIR"
run_with_spinner "Клонирование репозитория" "git clone -b $REPO_BRANCH $REPO_URL $BOT_DIRNAME"
cd "$BOT_DIRNAME" || error_exit "Не удалось перейти в каталог $BOT_DIRNAME"
if [[ ! -d ".git" ]]; then
error_exit "Клонирование репозитория не удалось. Проверьте доступ к $REPO_URL"
fi
echo -e "${YELLOW}Содержимое $BASE_DIR после клонирования:${NC}"
ls -la
fi
check_github_updates "$1"
}
# Точка входа
if [[ "$1" == "--check-update" ]]; then
check_updates --auto
else
check_updates
fi