summaryrefslogtreecommitdiff
path: root/files/TG_bot_VPN-main
diff options
context:
space:
mode:
Diffstat (limited to 'files/TG_bot_VPN-main')
-rw-r--r--files/TG_bot_VPN-main/.env.template5
-rw-r--r--files/TG_bot_VPN-main/.gitignore172
-rw-r--r--files/TG_bot_VPN-main/LICENSE21
-rw-r--r--files/TG_bot_VPN-main/README.md27
-rw-r--r--files/TG_bot_VPN-main/config_data/__init__.py1
-rw-r--r--files/TG_bot_VPN-main/config_data/config.py48
-rw-r--r--files/TG_bot_VPN-main/database/__init__.py1
-rw-r--r--files/TG_bot_VPN-main/database/models.py69
-rw-r--r--files/TG_bot_VPN-main/handlers/__init__.py2
-rw-r--r--files/TG_bot_VPN-main/handlers/custom_heandlers/__init__.py3
-rw-r--r--files/TG_bot_VPN-main/handlers/custom_heandlers/admin_handlers.py405
-rw-r--r--files/TG_bot_VPN-main/handlers/custom_heandlers/instruction_handlers.py26
-rw-r--r--files/TG_bot_VPN-main/handlers/custom_heandlers/location_handlers.py135
-rw-r--r--files/TG_bot_VPN-main/handlers/default_heandlers/__init__.py6
-rw-r--r--files/TG_bot_VPN-main/handlers/default_heandlers/echo.py21
-rw-r--r--files/TG_bot_VPN-main/handlers/default_heandlers/help.py13
-rw-r--r--files/TG_bot_VPN-main/handlers/default_heandlers/start.py99
-rw-r--r--files/TG_bot_VPN-main/keyboards/__init__.py2
-rw-r--r--files/TG_bot_VPN-main/keyboards/inline/__init__.py3
-rw-r--r--files/TG_bot_VPN-main/keyboards/inline/admin_buttons.py71
-rw-r--r--files/TG_bot_VPN-main/keyboards/inline/app_buttons.py22
-rw-r--r--files/TG_bot_VPN-main/keyboards/inline/servers.py19
-rw-r--r--files/TG_bot_VPN-main/keyboards/inline/subscribed.py17
-rw-r--r--files/TG_bot_VPN-main/keyboards/reply/__init__.py0
-rw-r--r--files/TG_bot_VPN-main/keyboards/reply/handlers_reply.py9
-rw-r--r--files/TG_bot_VPN-main/loader.py30
-rw-r--r--files/TG_bot_VPN-main/main.py31
-rw-r--r--files/TG_bot_VPN-main/requirements.txt9
-rw-r--r--files/TG_bot_VPN-main/states/__init__.py1
-rw-r--r--files/TG_bot_VPN-main/states/states.py24
-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
37 files changed, 2120 insertions, 0 deletions
diff --git a/files/TG_bot_VPN-main/.env.template b/files/TG_bot_VPN-main/.env.template
new file mode 100644
index 0000000..3bb0162
--- /dev/null
+++ b/files/TG_bot_VPN-main/.env.template
@@ -0,0 +1,5 @@
+BOT_TOKEN="Токен бота"
+ADMIN_ID="Telegram id админа"
+CHANNEL_ID="ID телеграмм канала (формата @channel)"
+DEFAULT_SERVER_USER="имя пользователя серверов по умолчанию"
+DEFAULT_SERVER_PASSWORD="Пароль пользователя серверов"
diff --git a/files/TG_bot_VPN-main/.gitignore b/files/TG_bot_VPN-main/.gitignore
new file mode 100644
index 0000000..245e6b6
--- /dev/null
+++ b/files/TG_bot_VPN-main/.gitignore
@@ -0,0 +1,172 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+database/database.db
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+# PyPI configuration file
+.pypirc
+qr_codes \ No newline at end of file
diff --git a/files/TG_bot_VPN-main/LICENSE b/files/TG_bot_VPN-main/LICENSE
new file mode 100644
index 0000000..4fd52d7
--- /dev/null
+++ b/files/TG_bot_VPN-main/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Semen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/files/TG_bot_VPN-main/README.md b/files/TG_bot_VPN-main/README.md
new file mode 100644
index 0000000..703861c
--- /dev/null
+++ b/files/TG_bot_VPN-main/README.md
@@ -0,0 +1,27 @@
+# Telegram Bot VPN
+
+**Подключение должно происходить через стороннее приложение Hiddify**
+
+У пользователя должно быть следующие меню:
+* /start
+ * Приветствие, "ссылка на канал"
+ * Бот должен делать проверку подписки на канал
+ * Ниже кнопка "Я подписался"
+ * Если пользователь не подписался — сообщение: "_для того что бы пользоваться ботом вам необходимо подписаться на канал_"
+ * Если успешно подписался: "_Спасибо, что выбрали наш сервис, приятного пользования._"
+* 3 кнопки: Локация, Инструкции, Поддержка
+ * При клике на кнопку Локацию (/location):
+ * Выбор серверов (страна подключения) и кнопка смена локации. По нажатию на выбранную локацию, должен появляться QR-код, Текст инструкция, защищенный ключ для подключения.
+ * При клике на кнопку Поддержка (/help):
+ * Должно быть описание, ниже кнопка Support
+
+Для каждого пользователя должен создаваться уникальный ключ для подключения
+
+Ограничения по устройствам: **на один уникальный ключ не более 3 устройств**
+
+Бот должен сохранять в БД всех пользователей
+
+**Админ панель:**
+* Настройка серверов
+* Рассылки пользователям
+* Редактирование текста в постах и название кнопок
diff --git a/files/TG_bot_VPN-main/config_data/__init__.py b/files/TG_bot_VPN-main/config_data/__init__.py
new file mode 100644
index 0000000..d63bc18
--- /dev/null
+++ b/files/TG_bot_VPN-main/config_data/__init__.py
@@ -0,0 +1 @@
+from . import config
diff --git a/files/TG_bot_VPN-main/config_data/config.py b/files/TG_bot_VPN-main/config_data/config.py
new file mode 100644
index 0000000..c04ba3e
--- /dev/null
+++ b/files/TG_bot_VPN-main/config_data/config.py
@@ -0,0 +1,48 @@
+import os
+from dotenv import load_dotenv, find_dotenv
+
+if not find_dotenv():
+ exit('Переменные окружения не загружены, т.к отсутствует файл .env')
+else:
+ load_dotenv()
+
+BOT_TOKEN = os.getenv('BOT_TOKEN')
+DEFAULT_COMMANDS = (
+ ('start', "Запустить бота"),
+ ('help', "Вывести справку"),
+ ('location', "Выбрать сервера для подключения"),
+ ('instruction', "Запросить инструкцию для подключения VPN"),
+)
+ADMIN_COMMANDS = (
+ ("admin_panel", "Админка"),
+ ("message_sending", "Рассылка сообщений"),
+ ("add_vpn_key", "Вручную добавить VPN ключ")
+)
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+PATH_TO_PYTHON = os.path.normpath(os.path.join(BASE_DIR, ".venv/bin/python.exe"))
+ADMIN_ID = os.getenv('ADMIN_ID')
+ALLOWED_USERS = [int(ADMIN_ID),
+ 418333240, # владелец канала
+ 610640621, # Второй админ
+ ]
+
+CHANNEL_ID = os.getenv('CHANNEL_ID')
+
+DEFAULT_SERVER_USER = os.getenv("DEFAULT_SERVER_USER")
+DEFAULT_SERVER_PASSWORD = os.getenv("DEFAULT_SERVER_PASSWORD")
+
+XRAY_CONFIG_PATH = "/usr/local/etc/xray/config.json"
+QR_CODE_DIR = os.path.join(BASE_DIR, "qr_codes") # Папка для хранения сгенерированных QR-кодов
+
+# Убедимся, что папка для QR-кодов существует
+if not os.path.exists(QR_CODE_DIR):
+ os.makedirs(QR_CODE_DIR)
+
+XRAY_REALITY_FINGERPRINT = "chrome"
+
+DOMAINS_LIST = [
+ "www.google.com",
+ "speed.cloudflare.com",
+ "www.microsoft.com",
+] \ No newline at end of file
diff --git a/files/TG_bot_VPN-main/database/__init__.py b/files/TG_bot_VPN-main/database/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/files/TG_bot_VPN-main/database/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/files/TG_bot_VPN-main/database/models.py b/files/TG_bot_VPN-main/database/models.py
new file mode 100644
index 0000000..ded6bae
--- /dev/null
+++ b/files/TG_bot_VPN-main/database/models.py
@@ -0,0 +1,69 @@
+import datetime
+import os.path
+import re
+
+from config_data.config import BASE_DIR
+import peewee
+
+db = peewee.SqliteDatabase(os.path.join(BASE_DIR, "database/database.db"))
+
+
+class BaseModel(peewee.Model):
+ """ Базовая модель """
+ class Meta:
+ database = db
+
+
+class Server(BaseModel):
+ """ Модель сервера для подключения VPN """
+ username = peewee.CharField()
+ password = peewee.CharField()
+ location = peewee.CharField()
+ ip_address = peewee.CharField(unique=True)
+ public_key = peewee.CharField(null=True, unique=True)
+
+
+class VPNKey(BaseModel):
+ """ Модель для VPN ключа. Привязан к серверу. Имеет qr код (картинка) для подключения """
+ server = peewee.ForeignKeyField(Server, related_name="keys",
+ on_delete="cascade",
+ on_update="cascade")
+ name = peewee.CharField()
+ key = peewee.CharField(unique=True)
+ qr_code = peewee.CharField()
+ is_valid = peewee.BooleanField(default=True)
+ created_at = peewee.DateTimeField(default=datetime.datetime.now())
+ updated_at = peewee.DateTimeField(default=datetime.datetime.now())
+
+ def extract_uuid(self) -> str | None:
+ """
+ Извлекает UUID из VLESS-ссылки ключа.
+ """
+ match = re.match(r'vless://([a-fA-F0-9-]{36})@', self.key)
+ return match.group(1) if match else None
+
+
+class User(BaseModel):
+ """ Модель пользователя """
+ user_id = peewee.CharField(unique=True)
+ full_name = peewee.CharField()
+ username = peewee.CharField()
+ is_premium = peewee.BooleanField(null=True)
+ is_subscribed = peewee.BooleanField(default=False)
+ vpn_key = peewee.ForeignKeyField(VPNKey, related_name="users", null=True, on_update="set null",
+ on_delete="set null")
+
+
+class Group(BaseModel):
+ """ Класс группы """
+ group_id = peewee.CharField(unique=True)
+ title = peewee.CharField()
+ description = peewee.TextField(null=True)
+ bio = peewee.TextField(null=True)
+ invite_link = peewee.CharField(null=True)
+ location = peewee.CharField(null=True)
+ username = peewee.CharField(null=True)
+
+
+def create_models():
+ db.create_tables(BaseModel.__subclasses__())
diff --git a/files/TG_bot_VPN-main/handlers/__init__.py b/files/TG_bot_VPN-main/handlers/__init__.py
new file mode 100644
index 0000000..3fb88a6
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/__init__.py
@@ -0,0 +1,2 @@
+from . import custom_heandlers
+from . import default_heandlers
diff --git a/files/TG_bot_VPN-main/handlers/custom_heandlers/__init__.py b/files/TG_bot_VPN-main/handlers/custom_heandlers/__init__.py
new file mode 100644
index 0000000..f34731f
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/custom_heandlers/__init__.py
@@ -0,0 +1,3 @@
+from . import admin_handlers
+from. import location_handlers
+from. import instruction_handlers
diff --git a/files/TG_bot_VPN-main/handlers/custom_heandlers/admin_handlers.py b/files/TG_bot_VPN-main/handlers/custom_heandlers/admin_handlers.py
new file mode 100644
index 0000000..51fe601
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/custom_heandlers/admin_handlers.py
@@ -0,0 +1,405 @@
+import os
+
+import peewee
+import qrcode
+from telebot.types import Message
+from config_data.config import ALLOWED_USERS, DEFAULT_COMMANDS, ADMIN_COMMANDS, QR_CODE_DIR
+from database.models import User, Server, VPNKey
+from keyboards.inline.admin_buttons import (
+ users_markup,
+ admin_markup,
+ get_vpn_markup,
+ get_servers_markup,
+ delete_vpn_markup,
+ key_actions_markup
+)
+from loader import bot, app_logger
+from states.states import AdminPanel
+from utils.functions import valid_ip, convert_amnezia_xray_json_to_vless_str, get_all_commands_bot
+from utils.generate_vpn_keys import setup_server, generate_key
+from utils.work_vpn_keys import suspend_key, resume_key, revoke_key, cleanup_server
+
+
+@bot.message_handler(commands=["admin_panel"])
+def admin_panel(message: Message):
+ """ Хендлер для админ панели """
+ if message.from_user.id in ALLOWED_USERS:
+ app_logger.info(f"Администратор {message.from_user.full_name} зашел в админ панель.")
+ bot.send_message(message.from_user.id, "🔧 Добро пожаловать в админ панель!\n\nВыберите нужную опцию ниже 👇",
+ reply_markup=admin_markup())
+ bot.set_state(message.from_user.id, AdminPanel.get_option)
+ else:
+ bot.send_message(message.from_user.id, "❌ У вас недостаточно прав")
+ app_logger.warning(f"Пользователь {message.from_user.full_name} пытался войти в админ панель")
+
+
+@bot.callback_query_handler(func=None, state=AdminPanel.get_option)
+def admin_panel_handler(call):
+ """ Callback handler для выбора раздела админ панели """
+ bot.answer_callback_query(callback_query_id=call.id)
+
+ if call.data == "Exit":
+ bot.send_message(call.message.chat.id, "🚪 Вы успешно вышли из админ панели. До встречи!")
+ bot.set_state(call.message.chat.id, None)
+ app_logger.info(f"Администратор {call.from_user.full_name} вышел из админ панели.")
+ elif call.data == "users":
+ app_logger.info(f"Администратор {call.from_user.full_name} зашел в юзер панель.")
+ bot.send_message(call.message.chat.id, "👥 Список всех пользователей базы данных:", reply_markup=users_markup())
+ bot.set_state(call.message.chat.id, AdminPanel.get_users)
+ elif call.data == "servers":
+ app_logger.info(f"Администратор {call.from_user.full_name} зашел в панель управления серверами.")
+ bot.send_message(call.message.chat.id, "🖥 Панель управления серверами:", reply_markup=get_servers_markup())
+ bot.set_state(call.message.chat.id, AdminPanel.get_servers)
+
+
+@bot.callback_query_handler(func=None, state=AdminPanel.get_users)
+def get_user(call):
+ """ Хендлер для работы с юзерами из админ панели """
+ bot.answer_callback_query(callback_query_id=call.id)
+ if call.data == "Exit":
+
+ bot.send_message(call.message.chat.id, "Выберите опцию", reply_markup=admin_markup())
+ bot.set_state(call.message.chat.id, AdminPanel.get_option)
+ app_logger.info(f"Администратор {call.from_user.full_name} вернулся обратно к выбору опций.")
+ else:
+ user_obj: User = User.get_by_id(call.data)
+ try:
+ vpn_key: VPNKey = user_obj.vpn_key
+ vpn_key = vpn_key.name
+ except Exception:
+ vpn_key = "отсутствует"
+ app_logger.info(f"Администратор {call.from_user.full_name} запросил "
+ f"информацию о пользователе {user_obj.full_name}")
+ bot.send_message(call.message.chat.id,
+ f"👤 Имя: {user_obj.full_name}\n"
+ f"📱 Телеграм: @{user_obj.username}\n"
+ f"📢 Подписан на канал: {user_obj.is_subscribed}\n"
+ f"🔑 VPN ключ: {vpn_key}"
+ )
+
+
+@bot.callback_query_handler(func=None, state=AdminPanel.get_servers)
+def server_panel_handler(call):
+ """ Хендлер для управления серверами """
+ bot.answer_callback_query(callback_query_id=call.id)
+
+ if call.data == "Add":
+ bot.send_message(call.message.chat.id,
+ "📄 Введите данные сервера в следующем формате:\n"
+ "🏙 Location (например, США)\n"
+ "👤 Username (например, root)\n"
+ "🔒 Password (пароль от root)\n"
+ "🌐 IP address"
+ )
+ bot.set_state(call.message.chat.id, AdminPanel.add_server)
+ return
+
+ server_id = call.data
+ server_obj: Server = Server.get_by_id(server_id)
+
+ app_logger.info(f"Администратор {call.from_user.full_name} запросил информацию о сервере {server_obj.location}")
+
+ # Проверка настроен ли сервер
+ status = "✅ Настроен" if server_obj.public_key else "❌ Требует настройки"
+
+ bot.send_message(call.message.chat.id,
+ f"🖥 Сервер: {server_obj.location}\n"
+ f"🔰 Статус: {status}\n"
+ f"🌐 IP адрес: {server_obj.ip_address}\n"
+ f"🔑 Количество ключей: {server_obj.keys.count()}",
+ reply_markup=get_vpn_markup(server_id))
+ bot.set_state(call.message.chat.id, AdminPanel.get_vpn_keys)
+
+
+@bot.message_handler(state=AdminPanel.add_server)
+def add_server(message: Message):
+ """ Добавление нового сервера """
+ try:
+ server_data = [item.strip() for item in message.text.split("\n")]
+ if len(server_data) != 4: # Убрали порт X-UI
+ raise ValueError("❌ Неверное количество полей!")
+ elif valid_ip(server_data[3]) is False:
+ raise ValueError("❌ Неверный формат IP адреса!")
+
+ # Создаем сервер и настраиваем
+ server = Server.create(
+ location=server_data[0],
+ username=server_data[1],
+ password=server_data[2],
+ ip_address=server_data[3]
+ )
+ app_logger.info(f"Администратор {message.from_user.full_name} начинает настройку сервера {server.location}")
+ bot.send_message(message.from_user.id, "Начинаю настройку сервера, подождите...")
+ # Автоматическая настройка сервера
+ if setup_server(server):
+ bot.send_message(message.from_user.id, "✅ Сервер успешно настроен!")
+ else:
+ bot.send_message(message.from_user.id, "❌ Ошибка настройки сервера!")
+ server.delete_instance()
+ bot.set_state(message.from_user.id, None)
+ return
+
+ bot.set_state(message.from_user.id, None)
+ app_logger.info(f"Администратор {message.from_user.full_name} добавил сервер {server.location}")
+
+ except Exception as ex:
+ bot.send_message(message.from_user.id, f"❌ Некорректные данные!\n{ex}")
+ app_logger.error(f"Ошибка при добавлении сервера {ex}")
+ bot.set_state(message.from_user.id, None)
+
+
+@bot.callback_query_handler(func=None, state=AdminPanel.get_vpn_keys)
+def vpn_panel_handler(call):
+ """ Хендлер для управления всеми привязанными к серверу VPN ключами """
+ bot.answer_callback_query(callback_query_id=call.id)
+
+ if "Generate" in call.data:
+ # Генерация нового ключа
+ server_id = call.data.split()[1]
+ server = Server.get_by_id(server_id)
+ app_logger.info(f"Администратор {call.from_user.full_name} запросил генерацию"
+ f" VPN ключа для сервера {server.location}")
+ bot.send_message(call.message.chat.id, f"⌛ Пожалуйста, подождите... Идет генерация нового VPN ключа...")
+ key = generate_key(server)
+ if key:
+ bot.send_message(call.message.chat.id, f"✅ Отлично! VPN ключ «{key.name}» успешно создан!")
+ app_logger.info(f"Администратор {call.from_user.full_name} успешно создал "
+ f"VPN ключ {key.name} для сервера {server.location}")
+ else:
+ bot.send_message(call.message.chat.id, "❌ Ошибка генерации ключа!")
+
+ bot.set_state(call.message.chat.id, AdminPanel.get_vpn_keys)
+ return
+
+ if "Delete" in call.data:
+ server_id = call.data.split()[1]
+ server_obj: Server = Server.get_by_id(server_id)
+ bot.send_message(call.message.chat.id, f"🗑 Начинается полная очистка сервера. Пожалуйста, подождите...")
+ if cleanup_server(server_obj):
+ app_logger.info(f"Администратор {call.from_user.full_name} удалил сервер {server_obj.location}")
+ bot.send_message(call.message.chat.id, f"✅ Сервер {server_obj.location} успешно удалён вместе "
+ f"с привязанными VPN ключами!")
+ bot.set_state(call.message.chat.id, AdminPanel.get_servers)
+ else:
+ bot.send_message(call.message.chat.id, "❌ Ошибка удаления сервера!")
+ bot.set_state(call.message.chat.id, AdminPanel.get_servers)
+ return
+
+ if "VPN - " in call.data:
+ # Выдача всей информации по VPN ключу
+ vpn_obj: VPNKey = VPNKey.get_by_id(call.data.split("VPN - ")[1])
+ app_logger.info(f"Администратор {call.from_user.full_name} запросил информацию о VPN ключе {vpn_obj.name}")
+ status = "✅ Активен" if vpn_obj.is_valid else "⏸ Приостановлен / Занят"
+ users = ", ".join([user.full_name for user in vpn_obj.users]) if vpn_obj.users else "Нет пользователей"
+ text = (
+ f"🔑 Ключ: {vpn_obj.name}\n"
+ f"📍 Сервер: {vpn_obj.server.location}\n"
+ f"📡 Статус: {status}\n"
+ f"👤 Пользователи: {users}\n"
+ f"🕒 Создан: {vpn_obj.created_at.strftime('%d.%m.%Y %H:%M')}"
+ )
+
+ # Отправляем QR-код если есть
+ if os.path.exists(vpn_obj.qr_code):
+ with open(vpn_obj.qr_code, 'rb') as qr_file:
+ bot.send_photo(call.message.chat.id, qr_file, caption=text,
+ reply_markup=key_actions_markup(vpn_obj.id))
+ else:
+ bot.send_message(call.message.chat.id, text,
+ reply_markup=key_actions_markup(vpn_obj.id))
+ bot.set_state(call.message.chat.id, AdminPanel.delete_vpn)
+ elif "Cancel" in call.data:
+ # Возврат в меню серверов
+ bot.send_message(call.message.chat.id, "Вы вернулись в меню серверов.",
+ reply_markup=get_servers_markup())
+ app_logger.info(f"Администратор {call.from_user.full_name} вернулся в меню серверов")
+ bot.set_state(call.message.chat.id, AdminPanel.get_servers)
+ else:
+ bot.set_state(call.message.chat.id, AdminPanel.get_vpn_keys)
+
+@bot.callback_query_handler(func=None, state=AdminPanel.delete_vpn)
+def vpn_panel_handler(call):
+ """ Хендлер для удаления VPN ключа """
+ bot.answer_callback_query(callback_query_id=call.id)
+
+ if "action_" in call.data:
+ action, key_id = call.data.split("_")[1], call.data.split("_")[2]
+ vpn_key = VPNKey.get_by_id(key_id)
+
+ if action == "suspend":
+ app_logger.info(f"Администратор {call.message.from_user.full_name} запросил остановку "
+ f"VPN ключа {vpn_key.name}")
+ if suspend_key(vpn_key):
+ bot.send_message(call.message.chat.id, f"⏸ Ключ {vpn_key.name} приостановлен")
+ else:
+ bot.send_message(call.message.chat.id, "❌ Ошибка приостановки ключа!")
+
+ elif action == "resume":
+ app_logger.info(f"Администратор {call.message.from_user.full_name} запросил возобновление работы "
+ f"VPN ключа {vpn_key.name}")
+ if resume_key(vpn_key):
+ bot.send_message(call.message.chat.id, f"▶️ Ключ {vpn_key.name} возобновлен")
+ else:
+ bot.send_message(call.message.chat.id, "❌ Ошибка возобновления ключа!")
+
+ elif action == "revoke":
+ app_logger.info(f"Администратор {call.message.from_user.full_name} запросил отзыв "
+ f"VPN ключа {vpn_key.name}")
+ if revoke_key(vpn_key):
+ for user in User.select().where(User.vpn_key == vpn_key):
+ user.vpn_key = None
+ user.save()
+ bot.send_message(call.message.chat.id, f"🗑 Ключ {vpn_key.name} отозван")
+ else:
+ bot.send_message(call.message.chat.id, "❌ Ошибка отзыва ключа!")
+
+ bot.set_state(call.message.chat.id, AdminPanel.get_servers)
+ return
+
+ if "Cancel" in call.data:
+ bot.send_message(call.message.chat.id, "Вы вернулись в админку.",
+ reply_markup=admin_markup())
+ app_logger.info(f"Администратор {call.from_user.full_name} вернулся в админку")
+ bot.set_state(call.message.chat.id, AdminPanel.get_option)
+ return
+ else:
+ # Скорее всего, пользователь выбрал другой VPN ключ
+ bot.set_state(call.message.chat.id, AdminPanel.get_vpn_keys)
+ vpn_panel_handler(call)
+
+
+@bot.message_handler(commands=["message_sending"])
+def message_sending_handler(message: Message):
+ """ Хендлер рассылки сообщений юзерам """
+ if message.from_user.id in ALLOWED_USERS:
+ app_logger.info(f"Администратор {message.from_user.full_name} вызвал команду /message_sending.")
+ bot.send_message(message.from_user.id, "✉️ Введите текст сообщения для рассылки пользователям:")
+ bot.set_state(message.from_user.id, AdminPanel.send_message)
+ else:
+ bot.send_message(message.from_user.id, "❌ У вас недостаточно прав для выполнения этой команды!")
+ app_logger.warning(f"Попытка доступа к /message_sending без прав администратора {message.from_user.full_name}")
+
+
+@bot.message_handler(state=AdminPanel.send_message)
+def send_message_to_users_handler(message: Message):
+ """ Отправка сообщений пользователям """
+ # Проверка, что сообщение для рассылки - не одна из команд бота
+
+ if message.text in get_all_commands_bot():
+ bot.send_message(message.from_user.id, "Это одна из команд бота")
+ bot.set_state(message.from_user.id, None)
+ return
+
+ # Проверка, что сообщение не пустое
+ if not message.text:
+ bot.send_message(message.from_user.id, "Сообщение не может быть пустым.")
+ return
+ app_logger.info(f"Администратор {message.from_user.full_name} начал рассылку сообщений: {message.text}")
+
+ bot.send_message(message.chat.id, "Начинаю рассылку...")
+ bot.send_chat_action(message.chat.id, "typing")
+ for user_obj in User.select():
+ if int(user_obj.user_id) not in ALLOWED_USERS:
+ bot.send_message(user_obj.user_id, message.text)
+ bot.send_message(message.chat.id, "✅ Рассылка сообщений завершена!")
+ bot.set_state(message.from_user.id, None)
+
+
+@bot.message_handler(commands=["add_vpn_key"])
+def add_vpn_key_handler(message: Message):
+ """ Хендлер для ручного добавления VPN ключа """
+ if message.from_user.id in ALLOWED_USERS:
+ app_logger.info(f"Администратор {message.from_user.full_name} вызвал команду /add_vpn_key.")
+ bot.send_message(message.from_user.id, "🔑 Введите название нового VPN ключа:")
+ bot.set_state(message.from_user.id, AdminPanel.add_vpn_key_name)
+ else:
+ bot.send_message(message.from_user.id, "У вас недостаточно прав!")
+
+
+@bot.message_handler(state=AdminPanel.add_vpn_key_name)
+def add_vpn_key_name_handler(message: Message):
+ """ Обработка ввода названия VPN ключа """
+ app_logger.info(f"Администратор {message.from_user.full_name} ввел название VPN ключа: {message.text}")
+ with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
+ data["vpn_key_name"] = message.text
+ bot.send_message(message.from_user.id, "🔑 Введите VPN KEY в формате JSON или "
+ "VLESS-ссылку (например, vless://...) :")
+ bot.set_state(message.from_user.id, AdminPanel.add_vpn_key_key)
+
+
+@bot.message_handler(state=AdminPanel.add_vpn_key_key)
+def add_vpn_key_key_handler(message: Message):
+ """ Обработка ввода VPN ключа """
+
+ if message.text in get_all_commands_bot():
+ bot.send_message(message.from_user.id, "Это одна из команд бота")
+ bot.set_state(message.from_user.id, None)
+ return
+
+ app_logger.info(f"Администратор {message.from_user.full_name} ввел VPN ключ")
+ if "vless://" not in message.text:
+ vless_str = convert_amnezia_xray_json_to_vless_str(message.text)
+ if vless_str is None:
+ bot.send_message(message.from_user.id, "Не удалось преобразовать JSON в VLESS строку. "
+ "Проверьте правильность ввода.")
+ app_logger.warning("Не удалось преобразовать json конфиг в vless строку!")
+ return
+ else:
+ vless_str = message.text
+ with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
+ data["vpn_key_key"] = vless_str
+ bot.send_message(message.from_user.id, "Выберите сервер, к которому принадлежит ключ",
+ reply_markup=get_servers_markup())
+ bot.set_state(message.from_user.id, AdminPanel.save_vpn_key)
+
+
+@bot.callback_query_handler(func=None, state=AdminPanel.save_vpn_key)
+def save_vpn_handler(call):
+ """ Хендлер для сохранения в БД VPN ключа """
+ bot.answer_callback_query(callback_query_id=call.id)
+
+ if call.data == "Add":
+ bot.send_message(call.message.chat.id,
+ "📄 Введите данные сервера в следующем формате:\n"
+ "🏙 Location (например, США)\n"
+ "👤 Username (например, root)\n"
+ "🔒 Password (пароль от root)\n"
+ "🌐 IP address"
+ )
+ bot.set_state(call.message.chat.id, AdminPanel.add_server)
+ return
+
+ server_id = call.data
+ server_obj: Server = Server.get_by_id(server_id)
+ with bot.retrieve_data(call.from_user.id, call.from_user.id) as data:
+ try:
+ 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(data["vpn_key_key"])
+ 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}")
+ vpn_key = VPNKey.create(
+ name=data["vpn_key_name"],
+ key=data["vpn_key_key"],
+ qr_code=qr_code_path,
+ server=server_obj,
+ )
+ except peewee.IntegrityError:
+ bot.send_message(call.message.chat.id, "Такой ключ уже существует!")
+ bot.send_message(call.message.chat.id, "Вы вышли в главное меню")
+ app_logger.warning(f"Попытка создания дубликата VPN ключа {data['vpn_key_name']}!")
+
+ else:
+ app_logger.info(f"Администратор {call.message.from_user.full_name} добавил VPN ключ {vpn_key.name} "
+ f"к серверу {server_obj.location}")
+ bot.send_message(call.message.chat.id, f"VPN ключ {vpn_key.name} добавлен к серверу {server_obj.location}.")
+ bot.set_state(call.message.chat.id, None)
diff --git a/files/TG_bot_VPN-main/handlers/custom_heandlers/instruction_handlers.py b/files/TG_bot_VPN-main/handlers/custom_heandlers/instruction_handlers.py
new file mode 100644
index 0000000..4702212
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/custom_heandlers/instruction_handlers.py
@@ -0,0 +1,26 @@
+from telebot.types import Message
+
+from config_data.config import CHANNEL_ID
+from database.models import User
+from loader import bot, app_logger
+from keyboards.inline.app_buttons import get_apps_murkup
+
+
+@bot.message_handler(commands=["instruction"])
+def instruction_handler(message: Message):
+ """ Хендлер для выбора сервера подключения """
+ app_logger.info(f"Пользователь {message.from_user.full_name} вызвал команду /location")
+ cur_user = User.get(User.user_id == message.from_user.id)
+
+ if cur_user.is_subscribed:
+ instruction_text = (
+ "📖 Подробная инструкция по подключению доступна по [ссылке]"
+ "(https://telegra.ph/Kak-ispolzovat-VPN-servis-Guard-Tunnel-01-16).\n\n"
+ "💡 Для использования VPN, пожалуйста, скачайте приложение **Hiddify**.\n\n"
+ "Желаем вам безопасного и комфортного подключения! 🚀"
+ )
+ bot.send_message(message.chat.id, instruction_text, parse_mode="Markdown", reply_markup=get_apps_murkup())
+ else:
+ bot.send_message(message.chat.id, f"🚫 Вы не подписаны на [наш канал](https://t.me/{CHANNEL_ID[1:]})!\n"
+ f"Подпишитесь, чтобы получить доступ ко всему функционалу.",
+ parse_mode="Markdown")
diff --git a/files/TG_bot_VPN-main/handlers/custom_heandlers/location_handlers.py b/files/TG_bot_VPN-main/handlers/custom_heandlers/location_handlers.py
new file mode 100644
index 0000000..45490e4
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/custom_heandlers/location_handlers.py
@@ -0,0 +1,135 @@
+from telebot.types import Message
+
+from config_data.config import CHANNEL_ID
+from database.models import User, Server, VPNKey
+from loader import bot, app_logger, scheduler
+from keyboards.inline.servers import get_locations_markup, get_instruction_markup
+from states.states import GetVPNKey
+from utils.functions import is_subscribed
+from utils.generate_vpn_keys import generate_key
+from utils.tasks import cancel_key_revocation
+from utils.work_vpn_keys import revoke_key
+
+
+@bot.message_handler(commands=["location"])
+def location_handler(message: Message):
+ """ Хендлер для выбора сервера подключения """
+ app_logger.info(f"Пользователь {message.from_user.full_name} вызвал команду /location")
+ cur_user = User.get(User.user_id == message.from_user.id)
+
+ if is_subscribed(CHANNEL_ID, message.from_user.id):
+ cur_user.is_subscribed = True
+ bot.send_message(message.chat.id, "🌍 Пожалуйста, выберите сервер для подключения:", reply_markup=get_locations_markup())
+ bot.set_state(message.chat.id, GetVPNKey.get_server)
+ else:
+ bot.send_message(message.chat.id, f"🚫 Вы не подписаны на [наш канал](https://t.me/{CHANNEL_ID[1:]})!\n"
+ f"Подпишитесь, чтобы получить доступ ко всему функционалу.",
+ parse_mode="Markdown")
+ cur_user.is_subscribed = False
+ cur_user.save()
+
+
+@bot.callback_query_handler(func=None, state=GetVPNKey.get_server)
+def get_server_handler(call):
+ """ Хендлер для получения сервера подключения """
+ bot.answer_callback_query(callback_query_id=call.id)
+
+ server_id = call.data
+
+ cur_user: User = User.get(User.user_id == call.from_user.id)
+ cur_server: Server = Server.get_by_id(server_id)
+
+ # Проверка на то, что текущий VPN ключ пользователя удален.
+ try:
+ VPNKey.get_by_id(cur_user.vpn_key)
+ except Exception:
+ app_logger.warning(f"Внимание! У пользователя {cur_user.full_name} не нашлось ключа VPN!")
+ cur_user.vpn_key = None
+ cur_user.save()
+
+ app_logger.info(f"Пользователь {cur_user.full_name} выбрал сервер {cur_server.location}")
+
+ # Получение всех vpn ключей данного сервера и выбор одного
+ for vpn_key_obj in cur_server.keys:
+ if vpn_key_obj.is_valid:
+ if cur_user.vpn_key is not None:
+ users_vpn = VPNKey.get_by_id(cur_user.vpn_key)
+ users_vpn.is_valid = True
+ users_vpn.save()
+ app_logger.info(f"VPN ключ {users_vpn.name} теперь свободен.")
+ cur_user.vpn_key = vpn_key_obj
+ cur_user.save()
+ vpn_key_obj.is_valid = False
+ vpn_key_obj.save()
+ app_logger.info(f"Пользователь {cur_user.full_name} зарезервировал ключ {vpn_key_obj.name}")
+ with open(vpn_key_obj.qr_code, "rb") as qr_code:
+ bot.send_photo(
+ call.message.chat.id,
+ qr_code,
+ caption=(
+ "🔒 Мы не храним информацию о ваших подключениях!\n\n"
+ f"🔑 Имя ключа: *{vpn_key_obj.name}*\n"
+ f"🌍 Сервер: *{cur_server.location}*\n"
+ f"🔗 URL для подключения:\n\n`{vpn_key_obj.key}`"
+ ),
+ parse_mode="Markdown",
+ reply_markup=get_instruction_markup()
+ )
+ bot.set_state(call.message.chat.id, None)
+ return
+
+ # Если нет свободных ключей, генерируем новый
+ app_logger.warning(f"Внимание! Для сервера {cur_server.location} не нашлось свободных VPN ключей! "
+ f"Генерирую новый...")
+ bot.send_message(call.message.chat.id, "⌛ Пожалуйста, подождите... Идет генерация нового VPN ключа...")
+
+ new_key: VPNKey = generate_key(cur_server)
+ if new_key is None:
+ app_logger.error("Внимание! Не удалось сгенерировать новый ключ!")
+ bot.send_message(call.message.chat.id, "❌ Ведутся технические работы на стороне сервера, поэтому "
+ "генерация нового ключа пока невозможна!\n"
+ "Мы приносим свои извинения, попробуйте позже 😊")
+ bot.set_state(call.message.chat.id, None)
+ return
+ app_logger.info(f"Сгенерирован новый ключ {new_key.name}!")
+
+ if cur_user.vpn_key is not None:
+ users_vpn = VPNKey.get_by_id(cur_user.vpn_key)
+ revoke_key(users_vpn)
+
+ cur_user.vpn_key = new_key
+ cur_user.save()
+
+ new_key.is_valid = False
+ new_key.save()
+ app_logger.info(f"Пользователь {cur_user.full_name} зарезервировал новый ключ {new_key.name}")
+ with open(new_key.qr_code, "rb") as qr_code:
+ bot.send_photo(
+ call.message.chat.id,
+ qr_code,
+ caption=(
+ "🔒 Мы не храним информацию о ваших подключениях!\n\n"
+ f"🔑 Имя ключа: *{new_key.name}*\n"
+ f"🌍 Сервер: *{cur_server.location}*\n"
+ f"🔗 URL для подключения:\n\n`{new_key.key}`"
+ ),
+ parse_mode="Markdown",
+ reply_markup=get_instruction_markup()
+ )
+ bot.set_state(call.message.chat.id, None)
+
+
+@bot.callback_query_handler(func=lambda call: call.data.startswith("renew_"))
+def renew_key_handler(call):
+ bot.answer_callback_query(callback_query_id=call.id)
+ try:
+ vpn_key_id = int(call.data.split("_")[1])
+ vpn_key = VPNKey.get_by_id(vpn_key_id)
+ # Отменяем запланированную задачу отзыва
+ cancel_key_revocation(vpn_key, scheduler)
+ bot.send_message(call.message.chat.id,
+ f"✅ Ваш VPN ключ «{vpn_key.name}» успешно продлён!")
+ app_logger.info(f"VPN ключ {vpn_key.name} продлён пользователем {call.from_user.full_name}.")
+ except Exception as e:
+ app_logger.error(f"Ошибка в обработчике продления ключа: {e}")
+ bot.send_message(call.message.chat.id, "❌ Произошла ошибка при продлении ключа.")
diff --git a/files/TG_bot_VPN-main/handlers/default_heandlers/__init__.py b/files/TG_bot_VPN-main/handlers/default_heandlers/__init__.py
new file mode 100644
index 0000000..4e4b225
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/default_heandlers/__init__.py
@@ -0,0 +1,6 @@
+from . import start
+from . import help
+from . import echo
+
+
+__all__ = ['start', 'help', 'echo']
diff --git a/files/TG_bot_VPN-main/handlers/default_heandlers/echo.py b/files/TG_bot_VPN-main/handlers/default_heandlers/echo.py
new file mode 100644
index 0000000..f8f5904
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/default_heandlers/echo.py
@@ -0,0 +1,21 @@
+from telebot.types import Message
+from loader import bot
+from handlers.custom_heandlers.location_handlers import location_handler
+from handlers.default_heandlers.help import bot_help
+from handlers.custom_heandlers.instruction_handlers import instruction_handler
+
+
+# Эхо хендлер, куда летят текстовые сообщения без указанного состояния
+
+@bot.message_handler(state=None)
+def bot_echo(message: Message):
+ if message.text == "🌍 Серверы":
+ location_handler(message)
+ elif message.text == "❓ Справка":
+ bot_help(message)
+ elif message.text == "📖 Инструкция":
+ instruction_handler(message)
+ else:
+ bot.reply_to(message, f"Введите любую команду из меню, чтобы я начал работать\n"
+ f"Либо выберите одну из кнопок, которые я вам прислал 👇")
+
diff --git a/files/TG_bot_VPN-main/handlers/default_heandlers/help.py b/files/TG_bot_VPN-main/handlers/default_heandlers/help.py
new file mode 100644
index 0000000..fc77dd4
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/default_heandlers/help.py
@@ -0,0 +1,13 @@
+from telebot.types import Message
+
+from config_data.config import DEFAULT_COMMANDS, ADMIN_COMMANDS, ALLOWED_USERS
+from loader import bot
+
+
+@bot.message_handler(commands=['help'])
+def bot_help(message: Message):
+ commands = [f"/{command} - {description}" for command, description in DEFAULT_COMMANDS]
+ if message.from_user.id in ALLOWED_USERS:
+ commands.extend([f"/{command} - {description}" for command, description in ADMIN_COMMANDS])
+ bot.reply_to(message, "📋 Доступные команды:\n" + "\n".join(commands))
+ bot.send_message(message.chat.id, "🤝 Для поддержки обращайтесь: @guardtunnel_support")
diff --git a/files/TG_bot_VPN-main/handlers/default_heandlers/start.py b/files/TG_bot_VPN-main/handlers/default_heandlers/start.py
new file mode 100644
index 0000000..32beca8
--- /dev/null
+++ b/files/TG_bot_VPN-main/handlers/default_heandlers/start.py
@@ -0,0 +1,99 @@
+from telebot.types import Message
+from loader import bot, app_logger
+from config_data.config import DEFAULT_COMMANDS, ADMIN_COMMANDS, ALLOWED_USERS, CHANNEL_ID
+from database.models import User, Group
+from utils.functions import is_subscribed
+from keyboards.inline.subscribed import is_subscribed_markup
+from keyboards.reply.handlers_reply import handlers_reply
+from states.states import SubscribedState
+
+
+start_text = """*Вы не подписались на канал!*
+
+☺️ Для доступа к полному функционалу бота вам необходимо подписаться на наш новостной канал [Guard Tunnel VPN](https://t.me/{channel_id})
+
+🎁 В подарок вы получите *бесплатный доступ* на использование нашего сервиса
+
+*С Уважением, команда Guard Tunnel VPN*
+
+_После подписки на наш канал нажмите ниже на кнопку: ✅ Я подписался_"""
+
+
+@bot.message_handler(commands=['start'])
+def bot_start(message: Message):
+ if message.chat.type == "private":
+ is_sub = True if message.from_user.id in ALLOWED_USERS else False
+ if User.get_or_none(user_id=message.from_user.id) is None:
+ app_logger.info(f"Внимание! Новый юзер: {message.from_user.full_name} - {message.from_user.username}")
+ User.create(user_id=message.from_user.id,
+ full_name=message.from_user.full_name,
+ username=message.from_user.username if message.from_user.username is not None else "None",
+ is_premium=message.from_user.is_premium,
+ is_subscribed=is_sub)
+ commands = [f"/{command} - {description}" for command, description in DEFAULT_COMMANDS]
+ if message.from_user.id in ALLOWED_USERS:
+ commands.extend([f"/{command} - {description}" for command, description in ADMIN_COMMANDS])
+ bot.send_message(
+ message.from_user.id,
+ f"Здравствуйте, {message.from_user.full_name}! 👋\n"
+ f"Вы вошли как администратор. Доступны следующие команды:\n{'\n'.join(commands)}",
+ reply_markup=handlers_reply()
+ )
+ else:
+ if is_subscribed(CHANNEL_ID, message.from_user.id):
+ # Если пользователь подписан на канал, тогда ему можно пользоваться ботом.
+ bot.send_message(message.from_user.id, f"Приветствуем, {message.from_user.full_name}.\n"
+ f"Рады приветствовать вас на нашем сервисе!\n"
+ f"Что бы использовать наш VPN сервис, "
+ f"следуйте инструкциям ниже 👇\n"
+ f"{'\n'.join(commands)}",
+ reply_markup=handlers_reply())
+ cur_user = User.get(User.user_id == message.from_user.id)
+ cur_user.is_subscribed = True
+ cur_user.save()
+ else:
+ bot.send_message(message.from_user.id, start_text.format(channel_id=CHANNEL_ID[1:]),
+ reply_markup=is_subscribed_markup(), parse_mode='Markdown',
+ disable_web_page_preview=True)
+ bot.set_state(message.from_user.id, SubscribedState.subscribe)
+
+ else:
+ bot.send_message(message.chat.id, "Здравствуйте! Я - телеграм бот, модератор каналов и групп. "
+ "Чтобы получить больше информации, "
+ "обратитесь к администратору, или напишите мне в личку)")
+ if Group.get_or_none(group_id=message.chat.id) is None:
+ Group.create(group_id=message.chat.id,
+ title=message.chat.title,
+ description=message.chat.description,
+ bio=message.chat.bio,
+ invite_link=message.chat.invite_link,
+ location=message.chat.location,
+ username=message.chat.username)
+ app_logger.info(f"Внимание! Новая группа: {message.chat.title} - {message.chat.invite_link}")
+ if User.get_or_none(user_id=message.from_user.id) is None:
+ User.create(user_id=message.from_user.id,
+ full_name=message.from_user.full_name,
+ username=message.from_user.username,
+ is_premium=message.from_user.is_premium)
+ app_logger.info(f"Внимание! Новый юзер: {message.from_user.full_name} - {message.from_user.username}")
+
+@bot.callback_query_handler(func=None, state=SubscribedState.subscribe)
+def is_subscribed_handler(call):
+ if is_subscribed(CHANNEL_ID, call.from_user.id):
+ commands = [f"/{command} - {description}" for command, description in DEFAULT_COMMANDS]
+ app_logger.info(f"Пользователь {call.from_user.full_name} подписался на канал!")
+
+ cur_user = User.get(User.user_id == call.from_user.id)
+ cur_user.is_subscribed = True
+ cur_user.save()
+
+ bot.answer_callback_query(callback_query_id=call.id)
+ bot.send_message(call.message.chat.id, "Спасибо, что выбрали наш сервис, приятного использования!\n"
+ f"Вам доступны следующие команды:\n"
+ f"{'\n'.join(commands)}")
+ bot.set_state(call.message.chat.id, None)
+ else:
+ bot.answer_callback_query(callback_query_id=call.id)
+ bot.send_message(call.message.chat.id, start_text.format(channel_id=CHANNEL_ID[1:]),
+ reply_markup=is_subscribed_markup(), parse_mode='Markdown',
+ disable_web_page_preview=True)
diff --git a/files/TG_bot_VPN-main/keyboards/__init__.py b/files/TG_bot_VPN-main/keyboards/__init__.py
new file mode 100644
index 0000000..e1e9fef
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/__init__.py
@@ -0,0 +1,2 @@
+from . import inline
+from . import reply
diff --git a/files/TG_bot_VPN-main/keyboards/inline/__init__.py b/files/TG_bot_VPN-main/keyboards/inline/__init__.py
new file mode 100644
index 0000000..48c5cc6
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/inline/__init__.py
@@ -0,0 +1,3 @@
+from . import admin_buttons
+from. import servers
+from. import subscribed
diff --git a/files/TG_bot_VPN-main/keyboards/inline/admin_buttons.py b/files/TG_bot_VPN-main/keyboards/inline/admin_buttons.py
new file mode 100644
index 0000000..08fd456
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/inline/admin_buttons.py
@@ -0,0 +1,71 @@
+from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
+
+from config_data.config import ALLOWED_USERS
+from database.models import User, Server
+
+
+def admin_markup():
+ """ Inline buttons для выбора меню админа """
+ actions = InlineKeyboardMarkup(row_width=2)
+ actions.add(InlineKeyboardButton(text=f"🖥 Управление серверами", callback_data="servers"))
+ actions.add(InlineKeyboardButton(text=f"👥 Управление пользователями", callback_data="users"))
+ actions.add(InlineKeyboardButton(text=f"🚪 Выйти", callback_data="Exit"))
+ return actions
+
+
+def users_markup():
+ """ Inline buttons для выбора юзера """
+ users_obj = User.select()
+ actions = InlineKeyboardMarkup(row_width=2)
+
+ for user in users_obj:
+ if int(user.user_id) not in ALLOWED_USERS:
+ actions.add(InlineKeyboardButton(text=f"👤 {user.full_name}", callback_data=user.id))
+ actions.add(InlineKeyboardButton(text=f"🔙 Назад", callback_data="Exit"))
+ return actions
+
+
+def get_servers_markup():
+ """ Inline buttons для выдачи всех локаций серверов """
+ actions = InlineKeyboardMarkup(row_width=1)
+ servers_obj = Server.select()
+ for server in servers_obj:
+ actions.add(InlineKeyboardButton(text=f"🌍 {server.location}", callback_data=str(server.id)))
+ actions.add(InlineKeyboardButton(text=f"➕ Добавить сервер", callback_data="Add"))
+ return actions
+
+def get_vpn_markup(server_id):
+ """ Inline buttons для выдачи vpn ключей сервера """
+ cur_server: Server = Server.get_by_id(server_id)
+ actions = InlineKeyboardMarkup(row_width=2)
+ for vpn_key_obj in cur_server.keys:
+ actions.add(InlineKeyboardButton(text=f"🔑 {vpn_key_obj.name}", callback_data=f"VPN - {str(vpn_key_obj.id)}"))
+ actions.add(InlineKeyboardButton(text=f"🔄 Сгенерировать новый ключ", callback_data=f"Generate {cur_server.id}"))
+ actions.add(InlineKeyboardButton(text=f"🗑 Удалить сервер", callback_data=f"Delete {cur_server.id}"),
+ InlineKeyboardButton(text=f"🔙 Назад", callback_data="Cancel")) # Возврат в меню серверов
+ return actions
+
+def delete_vpn_markup(vpn_obj_id: int):
+ """ Inline buttons для удаления vpn ключа """
+ actions = InlineKeyboardMarkup(row_width=2)
+ actions.add(InlineKeyboardButton(text=f"🗑 Удалить", callback_data=f"Del - {vpn_obj_id}"),
+ InlineKeyboardButton(text=f"🚪 Выйти", callback_data="Cancel"))
+ return actions
+
+
+def key_actions_markup(key_id: int) -> InlineKeyboardMarkup:
+ """
+ Клавиатура для управления ключом:
+ - Приостановить/возобновить
+ - Отозвать
+ - Назад
+ """
+ markup = InlineKeyboardMarkup(row_width=2)
+
+ markup.add(
+ InlineKeyboardButton("⏸ Приостановить", callback_data=f"action_suspend_{key_id}"),
+ InlineKeyboardButton("▶️ Возобновить", callback_data=f"action_resume_{key_id}"),
+ InlineKeyboardButton("🗑 Отозвать", callback_data=f"action_revoke_{key_id}"),
+ InlineKeyboardButton("🔙 Назад", callback_data="Cancel")
+ )
+ return markup \ No newline at end of file
diff --git a/files/TG_bot_VPN-main/keyboards/inline/app_buttons.py b/files/TG_bot_VPN-main/keyboards/inline/app_buttons.py
new file mode 100644
index 0000000..b62b0eb
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/inline/app_buttons.py
@@ -0,0 +1,22 @@
+from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
+
+
+def get_apps_murkup():
+ """ Inline buttons для выдачи ссылок на скачивание приложений """
+ actions = InlineKeyboardMarkup(row_width=3)
+
+ actions.add(InlineKeyboardButton(text="💻 Windows", url="https://github.com/hiddify/hiddify-next/releases/"
+ "download/v2.0.5/Hiddify-Windows-Setup-x64.exe"),
+ InlineKeyboardButton(text="🍏 MacOS", url="https://apps.apple.com/ru/app/"
+ "hiddify-proxy-vpn/id6596777532"),
+ InlineKeyboardButton(text="🐧 Linux", url="https://github.com/hiddify/hiddify-next/releases/"
+ "download/v2.0.5/Hiddify-Debian-x64.deb")
+ )
+ actions.add(InlineKeyboardButton(text="📱 iOS", url="https://apps.apple.com/ru/app/"
+ "hiddify-proxy-vpn/id6596777532"),
+ InlineKeyboardButton(text="🤖 Android", url="https://play.google.com/store/"
+ "apps/details?id=app.hiddify.com")
+ )
+ actions.add(InlineKeyboardButton(text="🔀 Другой вариант", url="https://github.com/hiddify/"
+ "hiddify-app/releases/tag/v2.0.5"))
+ return actions
diff --git a/files/TG_bot_VPN-main/keyboards/inline/servers.py b/files/TG_bot_VPN-main/keyboards/inline/servers.py
new file mode 100644
index 0000000..1b74b56
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/inline/servers.py
@@ -0,0 +1,19 @@
+from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
+from database.models import Server
+
+
+def get_locations_markup():
+ """ Inline buttons для выдачи всех локаций серверов """
+ actions = InlineKeyboardMarkup(row_width=1)
+ servers_obj = Server.select()
+ for server in servers_obj:
+ actions.add(InlineKeyboardButton(text=f"🌍 {server.location}", callback_data=str(server.id)))
+ return actions
+
+def get_instruction_markup():
+ """ Inline buttons для выдачи ссылки на инструкцию """
+ actions = InlineKeyboardMarkup(row_width=1)
+ actions.add(InlineKeyboardButton(text="📖 Инструкция для подключения", url="https://telegra.ph/"
+ "Kak-ispolzovat-VPN-servis-"
+ "Guard-Tunnel-01-16"))
+ return actions
diff --git a/files/TG_bot_VPN-main/keyboards/inline/subscribed.py b/files/TG_bot_VPN-main/keyboards/inline/subscribed.py
new file mode 100644
index 0000000..e627494
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/inline/subscribed.py
@@ -0,0 +1,17 @@
+from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
+from config_data.config import CHANNEL_ID
+
+
+def is_subscribed_markup():
+ """ Inline buttons для проверки подписки """
+ actions = InlineKeyboardMarkup(row_width=2)
+ actions.add(InlineKeyboardButton(text=f"Guard Tunnel VPN", url=f"https://t.me/{CHANNEL_ID[1:]}",
+ callback_data="1"),
+ InlineKeyboardButton(text=f" ✅ Я подписался", callback_data="2"))
+ return actions
+
+def get_renew_markup(vpn_key_id: int):
+ """Возвращает inline-клавиатуру с кнопкой «Продлить ключ»."""
+ markup = InlineKeyboardMarkup()
+ markup.add(InlineKeyboardButton(text="🔄 Продлить ключ", callback_data=f"renew_{vpn_key_id}"))
+ return markup
diff --git a/files/TG_bot_VPN-main/keyboards/reply/__init__.py b/files/TG_bot_VPN-main/keyboards/reply/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/reply/__init__.py
diff --git a/files/TG_bot_VPN-main/keyboards/reply/handlers_reply.py b/files/TG_bot_VPN-main/keyboards/reply/handlers_reply.py
new file mode 100644
index 0000000..a6149b0
--- /dev/null
+++ b/files/TG_bot_VPN-main/keyboards/reply/handlers_reply.py
@@ -0,0 +1,9 @@
+from telebot.types import ReplyKeyboardMarkup, KeyboardButton
+
+
+def handlers_reply() -> ReplyKeyboardMarkup:
+ keyboard = ReplyKeyboardMarkup(True, False, row_width=2,
+ input_field_placeholder="Нажмите на нужную кнопку либо введите команду...")
+ keyboard.add(KeyboardButton('🌍 Серверы'), KeyboardButton('❓ Справка'))
+ keyboard.add(KeyboardButton('📖 Инструкция'))
+ return keyboard
diff --git a/files/TG_bot_VPN-main/loader.py b/files/TG_bot_VPN-main/loader.py
new file mode 100644
index 0000000..9576017
--- /dev/null
+++ b/files/TG_bot_VPN-main/loader.py
@@ -0,0 +1,30 @@
+import os.path
+
+from apscheduler.schedulers.background import BackgroundScheduler
+from telebot import TeleBot
+from telebot.storage import StateMemoryStorage
+from config_data import config
+from logging.handlers import RotatingFileHandler
+import logging
+
+from config_data.config import BASE_DIR
+
+storage = StateMemoryStorage()
+bot = TeleBot(token=config.BOT_TOKEN, state_storage=storage)
+scheduler = BackgroundScheduler()
+
+
+log_formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(name)s - %(message)s')
+my_handler = RotatingFileHandler(os.path.join(BASE_DIR, "bot.log"), mode='a', maxBytes=5 * 1024 * 1024,
+ backupCount=1, encoding="utf8", delay=0)
+my_handler.setFormatter(log_formatter)
+my_handler.setLevel(logging.DEBUG)
+
+stream_handler = logging.StreamHandler()
+stream_handler.setFormatter(log_formatter)
+stream_handler.setLevel(logging.INFO)
+
+app_logger = logging.getLogger("app_logger")
+app_logger.setLevel(logging.DEBUG)
+app_logger.addHandler(my_handler)
+app_logger.addHandler(stream_handler)
diff --git a/files/TG_bot_VPN-main/main.py b/files/TG_bot_VPN-main/main.py
new file mode 100644
index 0000000..b6ec0b1
--- /dev/null
+++ b/files/TG_bot_VPN-main/main.py
@@ -0,0 +1,31 @@
+from datetime import datetime
+
+from loader import bot, app_logger, scheduler
+import handlers # noqa
+from telebot.custom_filters import StateFilter
+from utils.set_bot_commands import set_default_commands
+from database.models import create_models
+from config_data.config import ADMIN_ID
+from utils.tasks import check_and_revoke_keys, send_renewal_notifications
+
+if __name__ == '__main__':
+ create_models()
+ app_logger.debug("Подключение к базе данных...")
+ bot.add_custom_filter(StateFilter(bot))
+ set_default_commands(bot)
+ app_logger.info("Загрузка базовых команд...")
+ # Очистка всех задач
+ scheduler.remove_all_jobs()
+
+ # Запуск проверки каждые 5 минут
+ scheduler.add_job(check_and_revoke_keys, 'interval', minutes=5, next_run_time=datetime.now())
+ scheduler.start()
+ app_logger.info("Запущен планировщик для проверки подписки пользователей и отзыва ключей.")
+
+ # Каждые 8 часов отправляем уведомления о продлении ключей
+ # scheduler.add_job(send_renewal_notifications, 'interval', hours=8, args=[scheduler], next_run_time=datetime.now())
+ # app_logger.info("Запущен планировщик для продления ключей.")
+
+ app_logger.info(f"Бот @{bot.get_me().username} запущен.")
+ bot.send_message(ADMIN_ID, "Бот запущен.")
+ bot.infinity_polling()
diff --git a/files/TG_bot_VPN-main/requirements.txt b/files/TG_bot_VPN-main/requirements.txt
new file mode 100644
index 0000000..af3951b
--- /dev/null
+++ b/files/TG_bot_VPN-main/requirements.txt
@@ -0,0 +1,9 @@
+peewee
+pyTelegramBotAPI
+python-dotenv
+requests
+paramiko
+pexpect
+cryptography
+qrcode[pil]
+APScheduler
diff --git a/files/TG_bot_VPN-main/states/__init__.py b/files/TG_bot_VPN-main/states/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/files/TG_bot_VPN-main/states/__init__.py
@@ -0,0 +1 @@
+
diff --git a/files/TG_bot_VPN-main/states/states.py b/files/TG_bot_VPN-main/states/states.py
new file mode 100644
index 0000000..8ee0f11
--- /dev/null
+++ b/files/TG_bot_VPN-main/states/states.py
@@ -0,0 +1,24 @@
+from telebot.handler_backends import State, StatesGroup
+
+
+class AdminPanel(StatesGroup):
+ get_option = State()
+ get_users = State()
+ get_servers = State()
+ add_server = State()
+ get_vpn_keys = State()
+ delete_server = State()
+ delete_vpn = State()
+ send_message = State()
+
+ add_vpn_key_name = State()
+ add_vpn_key_key = State()
+ save_vpn_key = State()
+
+
+class SubscribedState(StatesGroup):
+ subscribe = State()
+
+class GetVPNKey(StatesGroup):
+ get_server = State()
+ get_key = State()
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