Обновление зависимостей, устранение хардкода, оплата через 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
родитель 90b00d971d
Коммит 11b52cf5ad
17 изменённых файлов: 1904 добавлений и 1277 удалений
+65
Просмотреть файл
@@ -0,0 +1,65 @@
"""Пути и настройки бота.
Все пути вычисляются относительно расположения этого файла, поэтому бот
не привязан к конкретному каталогу установки и не зависит от текущего cwd.
Каталог можно переопределить переменной окружения AWG_BOT_HOME.
"""
import os
import re
import sys
from pathlib import Path
BASE_DIR = Path(os.environ.get('AWG_BOT_HOME') or Path(__file__).resolve().parent)
FILES_DIR = BASE_DIR / 'files'
USERS_DIR = BASE_DIR / 'users'
CONFIG_FILE = FILES_DIR / 'config.json'
USER_EXPIRATION_FILE = FILES_DIR / 'user_expiration.json'
USER_TELEGRAM_FILE = FILES_DIR / 'user_telegram.json'
PROMOCODES_FILE = FILES_DIR / 'promocodes.json'
PAYMENTS_FILE = FILES_DIR / 'payments.json'
SERVER_CONF_FILE = FILES_DIR / 'server.conf'
CLIENTS_TABLE_FILE = FILES_DIR / 'clientsTable'
NEWCLIENT_SCRIPT = BASE_DIR / 'newclient.sh'
REMOVECLIENT_SCRIPT = BASE_DIR / 'removeclient.sh'
# Интерпретатор, которым запущен бот, — не захардкоженная версия Python.
PYTHON_EXECUTABLE = sys.executable or 'python3'
# Имя клиента должно быть безопасным для файловой системы и для newclient.sh.
USERNAME_RE = re.compile(r'^[a-zA-Z0-9_-]{1,32}$')
DEFAULT_PRICING = {
'1_month': 1000.0,
'3_months': 2500.0,
'6_months': 4500.0,
'12_months': 8000.0,
}
# Периоды подписки и их длительность в месяцах.
PERIODS = {
'1_month': 1,
'3_months': 3,
'6_months': 6,
'12_months': 12,
}
PERIOD_LABELS = {
'1_month': '1 месяц',
'3_months': '3 месяца',
'6_months': '6 месяцев',
'12_months': '12 месяцев',
}
def period_label(period):
"""Человекочитаемое название периода подписки."""
return PERIOD_LABELS.get(period, str(period).replace('_', ' '))
def ensure_dirs():
"""Создаёт рабочие каталоги, если их ещё нет."""
FILES_DIR.mkdir(parents=True, exist_ok=True)
USERS_DIR.mkdir(parents=True, exist_ok=True)