summaryrefslogtreecommitdiff
path: root/files/TG_bot_VPN-main/utils
diff options
context:
space:
mode:
authorvlapa <vlapa@ya.ru>2026-06-13 20:33:26 +0300
committervlapa <vlapa@ya.ru>2026-06-13 20:33:26 +0300
commite5abc1fe99340b979b18864d978179f71fe8f5c5 (patch)
tree98fd0ad7c6d0744894d6e74e446daacd9e6dd1b5 /files/TG_bot_VPN-main/utils
First
Diffstat (limited to 'files/TG_bot_VPN-main/utils')
-rw-r--r--files/TG_bot_VPN-main/utils/__init__.py1
-rw-r--r--files/TG_bot_VPN-main/utils/functions.py65
-rw-r--r--files/TG_bot_VPN-main/utils/generate_vpn_keys.py458
-rw-r--r--files/TG_bot_VPN-main/utils/misc/__init__.py1
-rw-r--r--files/TG_bot_VPN-main/utils/set_bot_commands.py8
-rw-r--r--files/TG_bot_VPN-main/utils/tasks.py112
-rw-r--r--files/TG_bot_VPN-main/utils/work_vpn_keys.py183
7 files changed, 828 insertions, 0 deletions
diff --git a/files/TG_bot_VPN-main/utils/__init__.py b/files/TG_bot_VPN-main/utils/__init__.py
new file mode 100644
index 0000000..ab665e1
--- /dev/null
+++ b/files/TG_bot_VPN-main/utils/__init__.py
@@ -0,0 +1 @@
+from . import misc
diff --git a/files/TG_bot_VPN-main/utils/functions.py b/files/TG_bot_VPN-main/utils/functions.py
new file mode 100644
index 0000000..44e9433
--- /dev/null
+++ b/files/TG_bot_VPN-main/utils/functions.py
@@ -0,0 +1,65 @@
+from config_data.config import DEFAULT_COMMANDS, ADMIN_COMMANDS
+from loader import bot, app_logger
+import json
+
+
+
+def is_subscribed(chat_id, user_id):
+ """
+ Функция для проверки, подписан ли пользователь на канал.
+ :param chat_id: id канала
+ :param user_id: id пользователя
+ :return: bool
+ """
+ result = bot.get_chat_member(chat_id, user_id)
+ if result.status in ("creator", "administrator", "member", "restricted"):
+ return True
+ return False
+
+
+def valid_ip(address):
+ try:
+ host_bytes = address.split('.')
+ valid = [int(b) for b in host_bytes]
+ valid = [b for b in valid if 0 <= b <= 255]
+ return len(host_bytes) == 4 and len(valid) == 4
+ except (TypeError, ValueError, IndexError):
+ return False
+
+def convert_amnezia_xray_json_to_vless_str(amnezia_str: str) -> str | None:
+ """
+ Функция для конвертации AMnezia Xray JSON в VLESS строчку
+ :param amnezia_str: JSON-строка с настройками Amnezia Xray
+ :return: VLESS-строка либо None объект
+ """
+ try:
+ json_object = json.loads(amnezia_str)
+ except Exception:
+ app_logger.error("Не удалось преобразовать JSON в объект!")
+ return None
+ try:
+ outbounds = json_object["outbounds"][0]["settings"]["vnext"][0]
+ stream_settings = json_object["outbounds"][0]["streamSettings"]
+
+ address_and_port = f"{outbounds['address']}:{outbounds['port']}"
+ flow = outbounds["users"][0]["flow"]
+ user_id = outbounds["users"][0]["id"]
+ type_of_net = stream_settings["network"]
+ security = stream_settings["security"]
+ fp = stream_settings["realitySettings"]["fingerprint"]
+ pbk = stream_settings["realitySettings"]["publicKey"]
+ sni = stream_settings["realitySettings"]["serverName"]
+ sid = stream_settings["realitySettings"]["shortId"]
+
+ url = (f"vless://{user_id}@{address_and_port}?flow={flow}&type={type_of_net}&"
+ f"security={security}&fp={fp}&sni={sni}&pbk={pbk}&sid={sid}")
+ except Exception as ex:
+ app_logger.error(f"Не получилось конвертировать конфиг Amnezia!\n{ex}")
+ return None
+ return url
+
+def get_all_commands_bot():
+ total_commands = [f"/{elem[0]}" for elem in DEFAULT_COMMANDS]
+ total_commands.extend([f"/{elem[0]}" for elem in ADMIN_COMMANDS])
+ total_commands.extend(["🌍 Серверы", "❓ Справка", "📖 Инструкция"])
+ return total_commands
diff --git a/files/TG_bot_VPN-main/utils/generate_vpn_keys.py b/files/TG_bot_VPN-main/utils/generate_vpn_keys.py
new file mode 100644
index 0000000..b9981c7
--- /dev/null
+++ b/files/TG_bot_VPN-main/utils/generate_vpn_keys.py
@@ -0,0 +1,458 @@
+import base64
+import os
+import json
+import uuid
+import tempfile
+import paramiko
+import qrcode
+import secrets
+import random
+from cryptography.hazmat.primitives.asymmetric import x25519
+from cryptography.hazmat.primitives import serialization
+from loader import app_logger
+from database.models import VPNKey, Server
+import copy
+from config_data.config import (
+ DEFAULT_SERVER_USER,
+ DEFAULT_SERVER_PASSWORD,
+ XRAY_CONFIG_PATH,
+ QR_CODE_DIR,
+ XRAY_REALITY_FINGERPRINT,
+ DOMAINS_LIST,
+)
+
+def generate_x25519_keys_base64() -> dict:
+ """
+ Генерирует пару ключей X25519 и возвращает их в base64-формате,
+ что соответствует требованиям Xray Reality.
+ """
+ private_key = x25519.X25519PrivateKey.generate()
+ public_key = private_key.public_key()
+ private_bytes = private_key.private_bytes(
+ encoding=serialization.Encoding.Raw,
+ format=serialization.PrivateFormat.Raw,
+ encryption_algorithm=serialization.NoEncryption()
+ )
+ public_bytes = public_key.public_bytes(
+ encoding=serialization.Encoding.Raw,
+ format=serialization.PublicFormat.Raw
+ )
+ private_b64 = base64.b64encode(private_bytes).decode('utf-8')
+ public_b64 = base64.b64encode(public_bytes).decode('utf-8')
+ return {"private": private_b64, "public": public_b64}
+
+
+def remote_generate_xray_keys(server_obj: Server) -> dict:
+ cmd = "xray x25519"
+ output = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=cmd,
+ timeout=30
+ )
+ keys = {}
+ for line in output.splitlines():
+ if line.lower().startswith("private"):
+ keys["private"] = line.split(":", 1)[1].strip()
+ elif line.lower().startswith("public"):
+ keys["public"] = line.split(":", 1)[1].strip()
+ if "private" in keys and "public" in keys:
+ return keys
+ else:
+ app_logger.error("Не удалось сгенерировать ключи через 'xray x25519' на сервере.")
+ return None
+
+
+def ensure_reality_params(config_template: dict, server_obj: Server) -> tuple:
+ # Выбираем случайный домен из DOMAINS_LIST
+ domain = random.choice(DOMAINS_LIST)
+ # Генерируем ключи (приватный и публичный) на сервере
+ keys = remote_generate_xray_keys(server_obj)
+ if not keys:
+ raise Exception("Удаленная генерация ключей не удалась")
+ # Генерируем список shortIds (например, 6 значений)
+ short_ids = [secrets.token_hex(4) for _ in range(6)]
+
+ # Создаем глубокую копию шаблона, чтобы не изменять оригинал
+ config = copy.deepcopy(config_template)
+
+ # Обновляем параметры в секции realitySettings
+ reality = config["inbounds"][0]["streamSettings"]["realitySettings"]
+ reality["dest"] = f"{domain}:443"
+ reality["serverNames"] = [domain]
+ reality["privateKey"] = keys["private"]
+ reality["publicKey"] = keys["public"] # <-- Добавляем поле publicKey
+ reality["shortIds"] = short_ids
+
+ # Обновляем домены в routing.rules (если указаны)
+ for rule in config.get("routing", {}).get("rules", []):
+ if "domain" in rule:
+ rule["domain"] = [domain]
+
+ # Возвращаем обновленный конфиг и публичный ключ для формирования VLESS-ссылки
+ return config, keys["public"]
+
+
+SECURE_XRAY_CONFIG = {
+ "log": {
+ "loglevel": "debug"
+ },
+ "inbounds": [
+ {
+ "listen": "0.0.0.0",
+ "port": 443,
+ "protocol": "vless",
+ "settings": {
+ "clients": [],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "dest": "",
+ "serverNames": [],
+ "privateKey": "",
+ "shortIds": [],
+ "publicKey": ""
+ }
+ },
+ "sniffing": {
+ "enabled": True,
+ "destOverride": ["http", "tls", "quic"]
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "tag": "block"
+ }
+ ]
+}
+
+
+def execute_ssh_command(ip: str, username: str, password: str, command: str, timeout: int = 60) -> str:
+ client = paramiko.SSHClient()
+ try:
+ client.load_system_host_keys()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(
+ hostname=ip,
+ username=username,
+ password=password,
+ timeout=10,
+ allow_agent=False,
+ look_for_keys=False
+ )
+ stdin, stdout, stderr = client.exec_command(command, timeout=timeout, get_pty=True)
+ output = stdout.read().decode('utf-8').strip()
+ err = stderr.read().decode('utf-8').strip()
+ if err:
+ app_logger.error(f"Ошибка выполнения команды на {ip}: {err}")
+ return output
+ except Exception as ex:
+ app_logger.error(f"Ошибка при подключении к {ip}: {ex}")
+ return ""
+ finally:
+ client.close()
+
+
+
+
+def setup_server(server_obj: Server) -> bool:
+ """
+ Настраивает сервер для работы VPN.
+ Если возникают какие-то ошибки при установке пакетов -
+ https://timeweb.cloud/docs/unix-guides/troubleshooting-unix/ustranenie-oshibki-could-not-get-lock-var-lib-dpkg-lock
+
+ Алгоритм:
+ 1. Подключение по SSH с правами администратора.
+ 2. Проверка наличия DEFAULT_SERVER_USER. Если отсутствует, создаём пользователя и устанавливаем пароль.
+ Для установки пароля используется команда chpasswd, что исключает необходимость интерактивного ввода.
+ 3. С использованием SFTP загружается обновлённый и безопасный конфиг Xray.
+ 4. Перезапуск службы Xray.
+
+ :param server_obj: объект Server с данными для подключения.
+ :return: True, если настройка прошла успешно, иначе False.
+ """
+ try:
+ # Подключаемся к серверу под администратором
+ with paramiko.SSHClient() as ssh_admin:
+ ssh_admin.load_system_host_keys()
+ ssh_admin.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh_admin.connect(
+ hostname=server_obj.ip_address,
+ username=server_obj.username,
+ password=server_obj.password,
+ timeout=10,
+ allow_agent=False,
+ look_for_keys=False
+ )
+ # Проверяем наличие пользователя DEFAULT_SERVER_USER
+ check_cmd = f"id {DEFAULT_SERVER_USER}"
+ stdin, stdout, stderr = ssh_admin.exec_command(check_cmd, timeout=30)
+ error_output = stderr.read().decode('utf-8').lower()
+ if "no such user" in error_output:
+ app_logger.info(
+ f"Пользователь {DEFAULT_SERVER_USER} не найден на сервере {server_obj.location}. Создаем его.")
+ # Создаем пользователя
+ ssh_admin.exec_command(f"useradd -m -s /bin/bash -G sudo {DEFAULT_SERVER_USER}", timeout=20)
+ # Устанавливаем пароль с помощью chpasswd (без интерактивного ввода)
+ passwd_cmd = f'echo "{DEFAULT_SERVER_USER}:{DEFAULT_SERVER_PASSWORD}" | chpasswd'
+ ssh_admin.exec_command(passwd_cmd, timeout=20)
+ app_logger.info(f"Пользователь {DEFAULT_SERVER_USER} создан и пароль установлен.")
+ else:
+ app_logger.info(f"Пользователь {DEFAULT_SERVER_USER} уже существует")
+ update_jq_cmd = (
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt update && "
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt upgrade -y && "
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt install -y jq"
+ )
+ execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=update_jq_cmd,
+ timeout=300
+ )
+ app_logger.info(f"Системные компоненты обновлены, зависимости установлены.")
+ xray_check_cmd = "which xray"
+ xray_installed = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=xray_check_cmd,
+ timeout=30
+ )
+ if not xray_installed:
+ app_logger.info("Xray не найден, начинаю установку с использованием официального скрипта.")
+ install_xray_cmd = (
+ "curl -O https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh && "
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S bash install-release.sh"
+ )
+ output_install_xray = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=install_xray_cmd,
+ timeout=300
+ )
+ app_logger.info(f"Установка Xray выполнена успешно. Вывод: {output_install_xray}")
+ else:
+ app_logger.info("Xray уже установлен. Сервер настроен")
+ return True
+
+ # Разрешаем 443 порт
+ allow_443_port_command = (
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S ufw allow 443/tcp && "
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S netstat -tulpn | grep ':443'"
+ )
+ port_output = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=allow_443_port_command,
+ timeout=300
+ )
+ app_logger.info(f"Порт 443 разрешен. Проверка: {port_output}")
+ # Подключаемся для загрузки конфигурации Xray
+ ssh = paramiko.SSHClient()
+ ssh.load_system_host_keys()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(
+ hostname=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ timeout=10,
+ allow_agent=False,
+ look_for_keys=False
+ )
+ sftp = ssh.open_sftp()
+
+ # заполняем конфигурацию данными
+ final_config, public_key = ensure_reality_params(SECURE_XRAY_CONFIG, server_obj)
+
+ # Сохраняем public key в модель сервера
+ server_obj.public_key = public_key
+ server_obj.save()
+
+ # Сохраняем локально безопасный конфиг Xray
+ local_config_path = os.path.join(tempfile.gettempdir(), f"secure_xray_config_{server_obj.id}.json")
+ with open(local_config_path, "w") as f:
+ json.dump(final_config, f, indent=2)
+ # Загружаем конфиг на сервер
+
+ temp_remote_path = f"/tmp/secure_xray_config_{server_obj.id}.json"
+ sftp.put(local_config_path, temp_remote_path)
+ app_logger.info(f"Конфигурационный файл загружен во временную директорию: {temp_remote_path}")
+
+ # Перемещаем файл с помощью sudo, чтобы обойти ограничение прав доступа.
+ move_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S mv {temp_remote_path} {XRAY_CONFIG_PATH}'
+ move_output = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=move_cmd,
+ timeout=30
+ )
+ app_logger.info(f"Файл конфигурации перемещён в {XRAY_CONFIG_PATH}. Вывод: {move_output}")
+ sftp.close()
+ ssh.close()
+
+ # Проверяем конфиг файл на валидность
+ test_config = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S xray run -test -confdir /usr/local/etc/xray/'
+ test_output = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=test_config,
+ timeout=30
+ )
+ app_logger.info(f"Конфиг проверен. Вывод: {test_output}")
+
+ # Синхронизируем время на сервере
+ time_syn_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S timedatectl set-ntp true'
+ execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=time_syn_cmd,
+ timeout=30
+ )
+ app_logger.info(f"Время на сервере синхронизировано.")
+
+ # Перезапускаем службу Xray для применения конфигурации
+ restart_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray'
+ restart_output = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=restart_cmd,
+ timeout=30
+ )
+ app_logger.info(f"Служба Xray перезапущена. Вывод: {restart_output}")
+ return True
+ except Exception as ex:
+ app_logger.error(f"Ошибка при настройке сервера {server_obj.location}: {ex}")
+ return False
+
+
+def generate_key(server_obj: Server) -> VPNKey | None:
+ """
+ Генерирует новый VPN ключ для сервера.
+
+ Алгоритм:
+ 1. Генерация уникального UUID для клиента.
+ 2. Обновление конфигурации Xray на сервере с помощью утилиты jq (добавление нового клиента в список).
+ 3. Перезапуск службы Xray.
+ 4. Формирование VLESS-ссылки с параметрами Xray Reality.
+ 5. Генерация QR-кода для VLESS-ссылки.
+ 6. Создание записи VPNKey в базе данных.
+
+ :param server_obj: объект Server.
+ :return: объект VPNKey с рабочей VLESS‑ссылкой и путем к QR‑коду, либо None при ошибке.
+ """
+ try:
+ # Шаг 1. Генерация UUID для нового клиента
+ client_uuid = str(uuid.uuid4())
+ app_logger.info(f"Сгенерирован новый UUID для клиента: {client_uuid}")
+
+ # Шаг 2. Обновление конфигурации Xray через jq
+ update_cmd = (
+ f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S sh -c '
+ f'"jq \'.inbounds[0].settings.clients += [{{\\"id\\": \\"{client_uuid}\\", '
+ f'\\"flow\\": \\"xtls-rprx-vision\\"}}]\' {XRAY_CONFIG_PATH} > {XRAY_CONFIG_PATH}.tmp && '
+ f'mv {XRAY_CONFIG_PATH}.tmp {XRAY_CONFIG_PATH}"'
+ )
+ update_output = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=update_cmd,
+ timeout=30
+ )
+ app_logger.info(f"Конфигурация Xray обновлена. Вывод: {update_output}")
+
+ # Шаг 3. Перезапуск службы Xray
+ restart_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray'
+ restart_output = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=restart_cmd,
+ timeout=30
+ )
+ app_logger.info(f"Служба Xray перезапущена. Вывод: {restart_output}")
+
+ # Шаг 4. Формирование VLESS-ссылки
+ # Получаем данные из конфиг файла
+ config_content = execute_ssh_command(
+ ip=server_obj.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=f"cat {XRAY_CONFIG_PATH}",
+ timeout=30
+ )
+ try:
+ config_json = json.loads(config_content)
+ # Извлекаем данные из секции realitySettings во втором inbound (индекс 1)
+ reality_settings = config_json["inbounds"][0]["streamSettings"]["realitySettings"]
+ # Предполагаем, что в конфиге теперь есть оба параметра: publicKey и serverNames
+ server_name = reality_settings.get("serverNames", [None])[0]
+ public_key = server_obj.public_key
+ short_id = random.choice(reality_settings["shortIds"])
+ if not server_name or not public_key or not short_id:
+ raise ValueError("Недостаточно параметров Xray Reality в конфиге.")
+ except Exception as e:
+ app_logger.error(f"Ошибка при парсинге конфигурационного файла: {e}")
+ return None
+
+ vless_link = (
+ f"vless://{client_uuid}@{server_obj.ip_address}:443?"
+ f"security=reality&"
+ f"encryption=none&"
+ f"flow=xtls-rprx-vision&"
+ f"type=tcp&"
+ f"fp={XRAY_REALITY_FINGERPRINT}&"
+ f"sni={server_name}&"
+ f"pbk={public_key}&"
+ f"sid={short_id}#GuardVPN"
+ )
+ app_logger.info(f"Сформирована VLESS ссылка: {vless_link}")
+
+
+ # Шаг 5. Генерация QR-кода
+ key_number = len(server_obj.keys) + 1 if hasattr(server_obj, "keys") else 1
+ qr_code_filename = f"vpn_key_{server_obj.id}_{key_number}.png"
+ qr_code_path = os.path.join(QR_CODE_DIR, qr_code_filename)
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_H,
+ box_size=10,
+ border=4
+ )
+ qr.add_data(vless_link)
+ qr.make(fit=True)
+ img = qr.make_image(fill_color="black", back_color="white")
+ img.save(qr_code_path)
+ app_logger.info(f"QR-код сгенерирован и сохранён по пути: {qr_code_path}")
+
+ # Шаг 6. Создание записи VPNKey в БД с использованием peewee.
+ vpn_key = VPNKey.create(
+ server=server_obj,
+ name=f"VPN Key {server_obj.location} #{key_number}",
+ key=vless_link,
+ qr_code=qr_code_path,
+ is_valid=True
+ )
+ app_logger.info(f"VPN ключ успешно создан: {vpn_key.key}")
+ return vpn_key
+ except Exception as ex:
+ app_logger.error(f"Ошибка при генерации VPN ключа для сервера {server_obj.location}: {ex}")
+ return None
diff --git a/files/TG_bot_VPN-main/utils/misc/__init__.py b/files/TG_bot_VPN-main/utils/misc/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/files/TG_bot_VPN-main/utils/misc/__init__.py
@@ -0,0 +1 @@
+
diff --git a/files/TG_bot_VPN-main/utils/set_bot_commands.py b/files/TG_bot_VPN-main/utils/set_bot_commands.py
new file mode 100644
index 0000000..6494889
--- /dev/null
+++ b/files/TG_bot_VPN-main/utils/set_bot_commands.py
@@ -0,0 +1,8 @@
+from telebot.types import BotCommand
+from config_data.config import DEFAULT_COMMANDS
+
+
+def set_default_commands(bot):
+ bot.set_my_commands(
+ [BotCommand(*i) for i in DEFAULT_COMMANDS]
+ )
diff --git a/files/TG_bot_VPN-main/utils/tasks.py b/files/TG_bot_VPN-main/utils/tasks.py
new file mode 100644
index 0000000..be6e7c8
--- /dev/null
+++ b/files/TG_bot_VPN-main/utils/tasks.py
@@ -0,0 +1,112 @@
+import datetime
+
+from apscheduler.schedulers.background import BackgroundScheduler
+
+from keyboards.inline.subscribed import get_renew_markup
+from loader import bot, app_logger
+from database.models import VPNKey, User
+from config_data.config import CHANNEL_ID, ALLOWED_USERS
+from utils.functions import is_subscribed
+from utils.work_vpn_keys import revoke_key
+
+# Глобальный словарь для хранения job_id запланированных задач по отзыву ключей
+pending_revocation_jobs = {}
+
+
+def check_and_revoke_keys():
+ """
+ Проверяет, подписаны ли пользователи, которым выданы VPN ключи.
+ Если обнаруживается, что хотя бы один пользователь, привязанный к ключу, отписался,
+ отправляет ему уведомление и отзывает ключ.
+ """
+ # Получаем активные ключи
+ app_logger.info("Проверка пользователей...")
+
+ active_keys = VPNKey.select().where(VPNKey.is_valid == False)
+ for vpn_key in active_keys:
+ revoke_this = False
+ for user in vpn_key.users:
+ # Если пользователь не подписан, отправляем уведомление
+ if not is_subscribed(CHANNEL_ID, user.user_id):
+ user.is_subscribed = False
+ user.vpn_key = None
+ user.save()
+ try:
+ bot.send_message(user.user_id,
+ "Ваш VPN ключ отозван, так как вы отписались от канала.")
+ app_logger.info(f"Пользователь {user.full_name} отписался от канала, "
+ f"поэтому его ключ {vpn_key.name} был отозван!")
+ # for admin_id in ALLOWED_USERS:
+ # bot.send_message(admin_id,
+ # f"Пользователь {user.full_name} отписался от канала, "
+ # f"поэтому его ключ {vpn_key.name} был отозван!")
+ except Exception as e:
+ app_logger.error(f"Не удалось отправить уведомление пользователю {user.user_id}: {e}")
+ revoke_this = True
+ if revoke_this:
+ if revoke_key(vpn_key):
+ app_logger.info(f"VPN ключ {vpn_key.id} отозван из-за отписки пользователя(ей).")
+ else:
+ app_logger.error(f"Ошибка при отзыве ключа {vpn_key.id}.")
+
+
+def schedule_key_revocation(vpn_key: VPNKey, user_obj: User, scheduler: BackgroundScheduler):
+ """
+ Планирует задачу отзыва ключа через 1 час.
+ Сохраняет идентификатор задачи в глобальном словаре по vpn_key.id.
+ """
+ def revoke_job():
+ # Если задача не была отменена (то есть пользователь не продлил ключ)
+ if vpn_key.id in pending_revocation_jobs:
+ try:
+ bot.send_message(
+ user_obj.user_id,
+ f"⌛ Время продления VPN ключа «{vpn_key.name}» истекло! Ключ был аннулирован.\n"
+ f"Вы можете создать новый ключ, нажав на кнопку Серверы 👇"
+ )
+ except Exception as e:
+ app_logger.error(f"Ошибка отправки уведомления пользователю {user_obj.full_name}: {e}")
+ revoke_key(vpn_key)
+ user_obj.vpn_key = None
+ user_obj.save()
+ app_logger.info(f"VPN ключ {vpn_key.name} аннулирован по истечении времени продления!")
+ # Убираем задачу из pending_revocation_jobs
+ pending_revocation_jobs.pop(vpn_key.id, None)
+
+ # Планируем выполнение через 1 час
+ job = scheduler.add_job(revoke_job, 'date', run_date=datetime.datetime.now() + datetime.timedelta(hours=1))
+ pending_revocation_jobs[vpn_key.id] = job.id
+ app_logger.info(f"Запланирована задача отзыва для ключа {vpn_key.name} через 1 час.")
+
+def cancel_key_revocation(vpn_key: VPNKey, scheduler: BackgroundScheduler):
+ """
+ Отменяет запланированную задачу отзыва ключа, если она существует.
+ """
+ job_id = pending_revocation_jobs.pop(vpn_key.id, None)
+ if job_id:
+ scheduler.remove_job(job_id)
+ app_logger.info(f"Запланированная задача отзыва для ключа {vpn_key.name} отменена.")
+
+
+def send_renewal_notifications(scheduler: BackgroundScheduler):
+ """
+ Каждые 8 часов рассылает пользователям уведомления о продлении VPN ключей.
+ Для каждого активного (зарезервированного) ключа отправляется сообщение с inline-кнопкой.
+ При отправке планируется задача, которая через 1 час отзовет ключ, если пользователь не нажмёт кнопку.
+ """
+ # Получаем список пользователей, у которых назначен VPN ключ (предполагается, что у пользователя есть vpn_key)
+ app_logger.info("Отправка уведомлений о продлении VPN ключей...")
+
+ for user in User.select().where(User.vpn_key.is_null(False)):
+ vpn_key = user.vpn_key
+ try:
+ bot.send_message(
+ user.user_id,
+ f"⏰ Напоминаем, что срок действия вашего VPN ключа «{vpn_key.name}» скоро истечёт.\n"
+ "Нажмите кнопку ниже, чтобы продлить его действие.",
+ reply_markup=get_renew_markup(vpn_key.id)
+ )
+ # Планируем задачу отзыва ключа через 1 час
+ schedule_key_revocation(vpn_key, user, scheduler)
+ except Exception as e:
+ app_logger.error(f"Ошибка отправки уведомления пользователю {user.user_id}: {e}")
diff --git a/files/TG_bot_VPN-main/utils/work_vpn_keys.py b/files/TG_bot_VPN-main/utils/work_vpn_keys.py
new file mode 100644
index 0000000..f030ec3
--- /dev/null
+++ b/files/TG_bot_VPN-main/utils/work_vpn_keys.py
@@ -0,0 +1,183 @@
+import os
+
+from config_data.config import DEFAULT_SERVER_PASSWORD, XRAY_CONFIG_PATH, DEFAULT_SERVER_USER
+from database.models import VPNKey, Server
+from loader import app_logger
+from utils.generate_vpn_keys import execute_ssh_command
+
+
+def suspend_key(vpn_key: VPNKey) -> bool:
+ """
+ Приостанавливает действие ключа, удаляя его из конфигурации Xray на сервере.
+ Обновляет статус is_valid в базе данных.
+ """
+ client_uuid = vpn_key.extract_uuid()
+ if not client_uuid:
+ app_logger.error(f"Не удалось извлечь UUID из ключа {vpn_key.id}")
+ return False
+
+ server = vpn_key.server
+ try:
+ # Удаление клиента из конфигурации с помощью jq
+ remove_cmd = (
+ f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S sh -c '
+ f'"jq \'del(.inbounds[0].settings.clients[] | select(.id == \\"{client_uuid}\\"))\' '
+ f'{XRAY_CONFIG_PATH} > {XRAY_CONFIG_PATH}.tmp && '
+ f'mv {XRAY_CONFIG_PATH}.tmp {XRAY_CONFIG_PATH}"'
+ )
+ output = execute_ssh_command(
+ ip=server.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=remove_cmd,
+ timeout=30
+ )
+ app_logger.info(f"Клиент {client_uuid} удалён из конфига. Вывод: {output}")
+
+ # Перезапуск Xray
+ restart_output = execute_ssh_command(
+ ip=server.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray',
+ timeout=30
+ )
+ app_logger.info(f"Xray перезапущен: {restart_output}")
+
+ # Обновление статуса ключа
+ vpn_key.is_valid = False
+ vpn_key.save()
+ return True
+ except Exception as ex:
+ app_logger.error(f"Ошибка при приостановке ключа {vpn_key.id}: {ex}")
+ return False
+
+def resume_key(vpn_key: VPNKey) -> bool:
+ """
+ Возобновляет действие ключа, добавляя его обратно в конфигурацию Xray.
+ Обновляет статус is_valid в базе данных.
+ """
+ client_uuid = vpn_key.extract_uuid()
+ if not client_uuid:
+ app_logger.error(f"Не удалось извлечь UUID из ключа {vpn_key.id}")
+ return False
+
+ server = vpn_key.server
+ try:
+ # Добавление клиента в конфигурацию
+ add_cmd = (
+ f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S sh -c '
+ f'"jq \'.inbounds[0].settings.clients += [{{"id": "{client_uuid}", "flow": "xtls-rprx-vision"}}]\' '
+ f'{XRAY_CONFIG_PATH} > {XRAY_CONFIG_PATH}.tmp && '
+ f'mv {XRAY_CONFIG_PATH}.tmp {XRAY_CONFIG_PATH}"'
+ )
+ output = execute_ssh_command(
+ ip=server.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=add_cmd,
+ timeout=30
+ )
+ app_logger.info(f"Клиент {client_uuid} добавлен в конфиг. Вывод: {output}")
+
+ # Перезапуск Xray
+ restart_output = execute_ssh_command(
+ ip=server.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray',
+ timeout=30
+ )
+ app_logger.info(f"Xray перезапущен: {restart_output}")
+
+ # Обновление статуса ключа
+ vpn_key.is_valid = True
+ vpn_key.save()
+ return True
+ except Exception as ex:
+ app_logger.error(f"Ошибка при возобновлении ключа {vpn_key.id}: {ex}")
+ return False
+
+def revoke_key(vpn_key: VPNKey) -> bool:
+ """
+ Полностью отзывает ключ: удаляет из конфигурации Xray и из базы данных.
+ """
+ try:
+ # Удаление из конфигурации
+ if not suspend_key(vpn_key):
+ return False
+
+ # Удаление QR-кода, если он существует
+ if os.path.exists(vpn_key.qr_code):
+ os.remove(vpn_key.qr_code)
+
+ # Удаление из базы данных
+ vpn_key.delete_instance()
+
+ app_logger.info(f"Ключ {vpn_key.id} полностью отозван.")
+ return True
+ except Exception as ex:
+ app_logger.error(f"Ошибка при отзыве ключа {vpn_key.id}: {ex}")
+ return False
+
+def get_active_keys(server: Server) -> list[VPNKey]:
+ """
+ Возвращает список активных ключей для указанного сервера.
+ """
+ return list(server.keys.where(VPNKey.is_valid == True))
+
+def get_inactive_keys(server: Server) -> list[VPNKey]:
+ """
+ Возвращает список неактивных ключей для указанного сервера.
+ """
+ return list(server.keys.where(VPNKey.is_valid == False))
+
+
+def cleanup_server(server: Server) -> bool:
+ """Полная очистка сервера от Xray и конфигов, с подробным логированием."""
+ app_logger.info(f"Администратор запросил полное удаление севрера {server.location}. Начинаю очистку...")
+ try:
+ cmds = [
+ # Остановка и отключение службы Xray
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl stop xray",
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl disable xray",
+ # Удаление Xray с помощью официального скрипта
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S bash -c 'curl -sL https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh | bash -s -- remove'",
+ # Очистка пакетов и конфигов
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt-get purge xray -y",
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S rm -rf /usr/local/etc/xray/",
+ # Восстановление фаервола: удаление правила для 443/tcp
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S ufw delete allow 443/tcp",
+ # Удаление созданного пользователя
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S pkill -9 -u {DEFAULT_SERVER_USER} || true",
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S userdel -rf {DEFAULT_SERVER_USER} || true",
+ # Очистка cron задач
+ f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S crontab -u {DEFAULT_SERVER_USER} -r || true"
+ ]
+ full_output = ""
+ for cmd in cmds:
+ output = execute_ssh_command(
+ ip=server.ip_address,
+ username=DEFAULT_SERVER_USER,
+ password=DEFAULT_SERVER_PASSWORD,
+ command=cmd,
+ timeout=60
+ )
+ full_output += f"\nКоманда: {cmd}\nВывод: {output}"
+
+ # Удаляем запись о сервере из БД
+ server.delete_instance()
+
+ # Удаление связанных VPN ключей
+ for vpn_key in VPNKey.select().where(VPNKey.server == server):
+ for user in vpn_key.users:
+ user.vpn_key = None
+ user.save()
+ app_logger.info(f"VPN ключ {vpn_key.name} удален!")
+ vpn_key.delete_instance()
+
+ app_logger.info(f"Cleanup завершён. Общий вывод: {full_output}")
+ return True
+ except Exception as ex:
+ app_logger.error(f"Server cleanup failed: {ex}")
+ return False \ No newline at end of file