diff options
Diffstat (limited to 'files')
140 files changed, 2120 insertions, 0 deletions
diff --git a/files/15d72c288938426db50c249effd75b07.png b/files/15d72c288938426db50c249effd75b07.png Binary files differnew file mode 100644 index 0000000..9b2ed6d --- /dev/null +++ b/files/15d72c288938426db50c249effd75b07.png diff --git a/files/1Gb_cab.png b/files/1Gb_cab.png Binary files differnew file mode 100644 index 0000000..ab1a340 --- /dev/null +++ b/files/1Gb_cab.png diff --git a/files/1fd1edd152259cb558abab7409faa120.png b/files/1fd1edd152259cb558abab7409faa120.png Binary files differnew file mode 100644 index 0000000..ed85155 --- /dev/null +++ b/files/1fd1edd152259cb558abab7409faa120.png diff --git a/files/2016-07-21 21Aygo-глушитель.jpg b/files/2016-07-21 21Aygo-глушитель.jpg Binary files differnew file mode 100644 index 0000000..a4cadae --- /dev/null +++ b/files/2016-07-21 21Aygo-глушитель.jpg diff --git a/files/2025-02-20 04.18.59.jpg b/files/2025-02-20 04.18.59.jpg Binary files differnew file mode 100644 index 0000000..049429e --- /dev/null +++ b/files/2025-02-20 04.18.59.jpg diff --git a/files/2025-06-20_22-34-47.png b/files/2025-06-20_22-34-47.png Binary files differnew file mode 100644 index 0000000..44bdade --- /dev/null +++ b/files/2025-06-20_22-34-47.png diff --git a/files/264fc3d5aab6c1a767f7659c207e1618.png b/files/264fc3d5aab6c1a767f7659c207e1618.png Binary files differnew file mode 100644 index 0000000..33b08db --- /dev/null +++ b/files/264fc3d5aab6c1a767f7659c207e1618.png diff --git a/files/2e66d41ed2450e40ffd3fa20ba1036d7.png b/files/2e66d41ed2450e40ffd3fa20ba1036d7.png Binary files differnew file mode 100644 index 0000000..dcd03c8 --- /dev/null +++ b/files/2e66d41ed2450e40ffd3fa20ba1036d7.png diff --git a/files/32de918111f7fed3e8183b4b9148286b.png b/files/32de918111f7fed3e8183b4b9148286b.png Binary files differnew file mode 100644 index 0000000..db48dbc --- /dev/null +++ b/files/32de918111f7fed3e8183b4b9148286b.png diff --git a/files/370b55d2c9ca15ef5153581f2af2b9b0.png b/files/370b55d2c9ca15ef5153581f2af2b9b0.png Binary files differnew file mode 100644 index 0000000..abfc078 --- /dev/null +++ b/files/370b55d2c9ca15ef5153581f2af2b9b0.png diff --git a/files/4a2b1a7f1e1bee9a26d7885b1d5e0f8c.png b/files/4a2b1a7f1e1bee9a26d7885b1d5e0f8c.png Binary files differnew file mode 100644 index 0000000..503f0fa --- /dev/null +++ b/files/4a2b1a7f1e1bee9a26d7885b1d5e0f8c.png diff --git a/files/6cda81091bcc9f99ad224bd5c5426f8e.png b/files/6cda81091bcc9f99ad224bd5c5426f8e.png Binary files differnew file mode 100644 index 0000000..6f4745e --- /dev/null +++ b/files/6cda81091bcc9f99ad224bd5c5426f8e.png diff --git a/files/73ce6d03c95dd19ac4263224cc1acac4.png b/files/73ce6d03c95dd19ac4263224cc1acac4.png Binary files differnew file mode 100644 index 0000000..b871de0 --- /dev/null +++ b/files/73ce6d03c95dd19ac4263224cc1acac4.png diff --git a/files/7c6829decd5925af5d5e0dcc27128438.png b/files/7c6829decd5925af5d5e0dcc27128438.png Binary files differnew file mode 100644 index 0000000..61e52e0 --- /dev/null +++ b/files/7c6829decd5925af5d5e0dcc27128438.png diff --git a/files/9132596e2802a7ecd3a47842f6475f60.png b/files/9132596e2802a7ecd3a47842f6475f60.png Binary files differnew file mode 100644 index 0000000..672295d --- /dev/null +++ b/files/9132596e2802a7ecd3a47842f6475f60.png diff --git a/files/Aygo-зад_полы_и_панель.jpg b/files/Aygo-зад_полы_и_панель.jpg Binary files differnew file mode 100644 index 0000000..c1a3289 --- /dev/null +++ b/files/Aygo-зад_полы_и_панель.jpg diff --git a/files/Aygo-задний_бампер.jpg b/files/Aygo-задний_бампер.jpg Binary files differnew file mode 100644 index 0000000..31434e5 --- /dev/null +++ b/files/Aygo-задний_бампер.jpg diff --git a/files/Aygo-задняя_подвеска.jpg b/files/Aygo-задняя_подвеска.jpg Binary files differnew file mode 100644 index 0000000..65b4bcd --- /dev/null +++ b/files/Aygo-задняя_подвеска.jpg diff --git a/files/Aygo-передн_подвеска.jpg b/files/Aygo-передн_подвеска.jpg Binary files differnew file mode 100644 index 0000000..cb78d6d --- /dev/null +++ b/files/Aygo-передн_подвеска.jpg diff --git a/files/Aygo-передний_бампер.jpg b/files/Aygo-передний_бампер.jpg Binary files differnew file mode 100644 index 0000000..41dc699 --- /dev/null +++ b/files/Aygo-передний_бампер.jpg diff --git a/files/Aygo-топл_бак_и_горловина.jpg b/files/Aygo-топл_бак_и_горловина.jpg Binary files differnew file mode 100644 index 0000000..b2ebd27 --- /dev/null +++ b/files/Aygo-топл_бак_и_горловина.jpg diff --git a/files/DB_metall.ods b/files/DB_metall.ods Binary files differnew file mode 100644 index 0000000..bc6621b --- /dev/null +++ b/files/DB_metall.ods diff --git a/files/Pasted image 20240312221215.png b/files/Pasted image 20240312221215.png Binary files differnew file mode 100644 index 0000000..714f40b --- /dev/null +++ b/files/Pasted image 20240312221215.png diff --git a/files/Pasted image 20250214130256.png b/files/Pasted image 20250214130256.png Binary files differnew file mode 100644 index 0000000..9447a00 --- /dev/null +++ b/files/Pasted image 20250214130256.png diff --git a/files/Pasted image 20250214130512.png b/files/Pasted image 20250214130512.png Binary files differnew file mode 100644 index 0000000..dbef1d9 --- /dev/null +++ b/files/Pasted image 20250214130512.png diff --git a/files/Pasted image 20250217114433.png b/files/Pasted image 20250217114433.png Binary files differnew file mode 100644 index 0000000..33df2a7 --- /dev/null +++ b/files/Pasted image 20250217114433.png diff --git a/files/Pasted image 20250501030617.png b/files/Pasted image 20250501030617.png Binary files differnew file mode 100644 index 0000000..d8472a8 --- /dev/null +++ b/files/Pasted image 20250501030617.png diff --git a/files/Pasted image 20250501031129.png b/files/Pasted image 20250501031129.png Binary files differnew file mode 100644 index 0000000..c85bfb7 --- /dev/null +++ b/files/Pasted image 20250501031129.png diff --git a/files/Pasted image 20250501031423.png b/files/Pasted image 20250501031423.png Binary files differnew file mode 100644 index 0000000..ba87255 --- /dev/null +++ b/files/Pasted image 20250501031423.png diff --git a/files/Pasted image 20250501031545.png b/files/Pasted image 20250501031545.png Binary files differnew file mode 100644 index 0000000..f93e5fa --- /dev/null +++ b/files/Pasted image 20250501031545.png diff --git a/files/Pasted image 20250501031558.png b/files/Pasted image 20250501031558.png Binary files differnew file mode 100644 index 0000000..6afb1c6 --- /dev/null +++ b/files/Pasted image 20250501031558.png diff --git a/files/Pasted image 20250501031753.png b/files/Pasted image 20250501031753.png Binary files differnew file mode 100644 index 0000000..0f1ca56 --- /dev/null +++ b/files/Pasted image 20250501031753.png diff --git a/files/Pasted image 20250621003213.png b/files/Pasted image 20250621003213.png Binary files differnew file mode 100644 index 0000000..aaf7cec --- /dev/null +++ b/files/Pasted image 20250621003213.png diff --git a/files/Pasted image 20250621003225.png b/files/Pasted image 20250621003225.png Binary files differnew file mode 100644 index 0000000..3cdb5f4 --- /dev/null +++ b/files/Pasted image 20250621003225.png diff --git a/files/Python_шпаргалка.png b/files/Python_шпаргалка.png Binary files differnew file mode 100644 index 0000000..9fa1e78 --- /dev/null +++ b/files/Python_шпаргалка.png diff --git a/files/TG_bot_VPN-main.zip b/files/TG_bot_VPN-main.zip Binary files differnew file mode 100644 index 0000000..5323159 --- /dev/null +++ b/files/TG_bot_VPN-main.zip 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 diff --git a/files/ae81365154ba5166845a7a9171d2248b.png b/files/ae81365154ba5166845a7a9171d2248b.png Binary files differnew file mode 100644 index 0000000..f63e4dd --- /dev/null +++ b/files/ae81365154ba5166845a7a9171d2248b.png diff --git a/files/b74e1c10b7c49ab7cc235e746af07128.png b/files/b74e1c10b7c49ab7cc235e746af07128.png Binary files differnew file mode 100644 index 0000000..4a94e09 --- /dev/null +++ b/files/b74e1c10b7c49ab7cc235e746af07128.png diff --git a/files/dbb44ce3043db021cfbb195941e52a5a.png b/files/dbb44ce3043db021cfbb195941e52a5a.png Binary files differnew file mode 100644 index 0000000..3e73252 --- /dev/null +++ b/files/dbb44ce3043db021cfbb195941e52a5a.png diff --git a/files/eafc9c46561ec70fb5744ea1d85eb1c0.png b/files/eafc9c46561ec70fb5744ea1d85eb1c0.png Binary files differnew file mode 100644 index 0000000..9293df5 --- /dev/null +++ b/files/eafc9c46561ec70fb5744ea1d85eb1c0.png diff --git a/files/telegram-cloud-photo-size-2-5220111584251211570-y.jpg b/files/telegram-cloud-photo-size-2-5220111584251211570-y.jpg Binary files differnew file mode 100644 index 0000000..fdcdbbd --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5220111584251211570-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5220111584251211572-y.jpg b/files/telegram-cloud-photo-size-2-5220111584251211572-y.jpg Binary files differnew file mode 100644 index 0000000..df18fec --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5220111584251211572-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5220111584251211573-y.jpg b/files/telegram-cloud-photo-size-2-5220111584251211573-y.jpg Binary files differnew file mode 100644 index 0000000..81b657f --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5220111584251211573-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5230966980947866123-y.jpg b/files/telegram-cloud-photo-size-2-5230966980947866123-y.jpg Binary files differnew file mode 100644 index 0000000..a2b08e7 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5230966980947866123-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5235693785730632253-y.jpg b/files/telegram-cloud-photo-size-2-5235693785730632253-y.jpg Binary files differnew file mode 100644 index 0000000..b8f7168 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5235693785730632253-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5249336126576118394-y.jpg b/files/telegram-cloud-photo-size-2-5249336126576118394-y.jpg Binary files differnew file mode 100644 index 0000000..39d0d9c --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5249336126576118394-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5255812146984502723-y.jpg b/files/telegram-cloud-photo-size-2-5255812146984502723-y.jpg Binary files differnew file mode 100644 index 0000000..7d1b522 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5255812146984502723-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5264765212471446942-y.jpg b/files/telegram-cloud-photo-size-2-5264765212471446942-y.jpg Binary files differnew file mode 100644 index 0000000..4a30a0d --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5264765212471446942-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5269638479933982570-y.jpg b/files/telegram-cloud-photo-size-2-5269638479933982570-y.jpg Binary files differnew file mode 100644 index 0000000..56fe121 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5269638479933982570-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5280728042542581828-y.jpg b/files/telegram-cloud-photo-size-2-5280728042542581828-y.jpg Binary files differnew file mode 100644 index 0000000..f2fe665 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5280728042542581828-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5310086154055444585-y.jpg b/files/telegram-cloud-photo-size-2-5310086154055444585-y.jpg Binary files differnew file mode 100644 index 0000000..d373755 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5310086154055444585-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5316919099131030050-y.jpg b/files/telegram-cloud-photo-size-2-5316919099131030050-y.jpg Binary files differnew file mode 100644 index 0000000..f3335d1 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5316919099131030050-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5337133182321027005-y.jpg b/files/telegram-cloud-photo-size-2-5337133182321027005-y.jpg Binary files differnew file mode 100644 index 0000000..d0478ac --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5337133182321027005-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5350524228924991162-y.jpg b/files/telegram-cloud-photo-size-2-5350524228924991162-y.jpg Binary files differnew file mode 100644 index 0000000..74d5997 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5350524228924991162-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5382031937209757544-y.jpg b/files/telegram-cloud-photo-size-2-5382031937209757544-y.jpg Binary files differnew file mode 100644 index 0000000..357c2cd --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5382031937209757544-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5382031937209757546-y.jpg b/files/telegram-cloud-photo-size-2-5382031937209757546-y.jpg Binary files differnew file mode 100644 index 0000000..d15ecbf --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5382031937209757546-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5386535536837126461-y.jpg b/files/telegram-cloud-photo-size-2-5386535536837126461-y.jpg Binary files differnew file mode 100644 index 0000000..05383d0 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5386535536837126461-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5399847483028401010-y.jpg b/files/telegram-cloud-photo-size-2-5399847483028401010-y.jpg Binary files differnew file mode 100644 index 0000000..3737103 --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5399847483028401010-y.jpg diff --git a/files/telegram-cloud-photo-size-2-5406976059292904279-x.jpg b/files/telegram-cloud-photo-size-2-5406976059292904279-x.jpg Binary files differnew file mode 100644 index 0000000..d0759af --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5406976059292904279-x.jpg diff --git a/files/telegram-cloud-photo-size-2-5413616564063822987-y.jpg b/files/telegram-cloud-photo-size-2-5413616564063822987-y.jpg Binary files differnew file mode 100644 index 0000000..485989a --- /dev/null +++ b/files/telegram-cloud-photo-size-2-5413616564063822987-y.jpg diff --git a/files/telegram-cloud-photo-size-4-5776315316576566340-y.jpg b/files/telegram-cloud-photo-size-4-5776315316576566340-y.jpg Binary files differnew file mode 100644 index 0000000..a370e5a --- /dev/null +++ b/files/telegram-cloud-photo-size-4-5776315316576566340-y.jpg diff --git a/files/telegram-cloud-photo-size-4-5850234924966199387-w.jpg b/files/telegram-cloud-photo-size-4-5850234924966199387-w.jpg Binary files differnew file mode 100644 index 0000000..3038b28 --- /dev/null +++ b/files/telegram-cloud-photo-size-4-5850234924966199387-w.jpg diff --git a/files/telegram-cloud-photo-size-4-5850234924966199390-w.jpg b/files/telegram-cloud-photo-size-4-5850234924966199390-w.jpg Binary files differnew file mode 100644 index 0000000..896a8e2 --- /dev/null +++ b/files/telegram-cloud-photo-size-4-5850234924966199390-w.jpg diff --git a/files/telegram-cloud-photo-size-4-5850234924966199391-w.jpg b/files/telegram-cloud-photo-size-4-5850234924966199391-w.jpg Binary files differnew file mode 100644 index 0000000..9a0ea44 --- /dev/null +++ b/files/telegram-cloud-photo-size-4-5850234924966199391-w.jpg diff --git a/files/telegram-cloud-photo-size-4-5863854399405801203-w.jpg b/files/telegram-cloud-photo-size-4-5863854399405801203-w.jpg Binary files differnew file mode 100644 index 0000000..9b1dc32 --- /dev/null +++ b/files/telegram-cloud-photo-size-4-5863854399405801203-w.jpg diff --git a/files/telegram-cloud-photo-size-4-6016931255788092228-y.jpg b/files/telegram-cloud-photo-size-4-6016931255788092228-y.jpg Binary files differnew file mode 100644 index 0000000..2a593ae --- /dev/null +++ b/files/telegram-cloud-photo-size-4-6016931255788092228-y.jpg diff --git a/files/Регулятор_оборотов_кулера.png b/files/Регулятор_оборотов_кулера.png Binary files differnew file mode 100644 index 0000000..82be635 --- /dev/null +++ b/files/Регулятор_оборотов_кулера.png diff --git a/files/СкриптыДляАдмина.jpg b/files/СкриптыДляАдмина.jpg Binary files differnew file mode 100644 index 0000000..b5f08a9 --- /dev/null +++ b/files/СкриптыДляАдмина.jpg diff --git a/files/Снимок экрана 2023-08-06 в 22.19.19.png b/files/Снимок экрана 2023-08-06 в 22.19.19.png Binary files differnew file mode 100644 index 0000000..2095b5e --- /dev/null +++ b/files/Снимок экрана 2023-08-06 в 22.19.19.png diff --git a/files/Снимок экрана 2023-10-06 в 10.40.44.png b/files/Снимок экрана 2023-10-06 в 10.40.44.png Binary files differnew file mode 100644 index 0000000..9f88ee3 --- /dev/null +++ b/files/Снимок экрана 2023-10-06 в 10.40.44.png diff --git a/files/Снимок экрана 2023-10-19 в 09.53.24.png b/files/Снимок экрана 2023-10-19 в 09.53.24.png Binary files differnew file mode 100644 index 0000000..9073ab5 --- /dev/null +++ b/files/Снимок экрана 2023-10-19 в 09.53.24.png diff --git a/files/Снимок экрана 2023-10-19 в 09.53.43.png b/files/Снимок экрана 2023-10-19 в 09.53.43.png Binary files differnew file mode 100644 index 0000000..e1067f5 --- /dev/null +++ b/files/Снимок экрана 2023-10-19 в 09.53.43.png diff --git a/files/Снимок экрана 2023-10-29 в 14.23.34.png b/files/Снимок экрана 2023-10-29 в 14.23.34.png Binary files differnew file mode 100644 index 0000000..501d850 --- /dev/null +++ b/files/Снимок экрана 2023-10-29 в 14.23.34.png diff --git a/files/Снимок экрана 2023-12-17 в 20.47.18.png b/files/Снимок экрана 2023-12-17 в 20.47.18.png Binary files differnew file mode 100644 index 0000000..a7a0f16 --- /dev/null +++ b/files/Снимок экрана 2023-12-17 в 20.47.18.png diff --git a/files/Снимок экрана 2023-12-17 в 20.47.46.png b/files/Снимок экрана 2023-12-17 в 20.47.46.png Binary files differnew file mode 100644 index 0000000..df5df20 --- /dev/null +++ b/files/Снимок экрана 2023-12-17 в 20.47.46.png diff --git a/files/Снимок экрана 2023-12-17 в 20.53.26.png b/files/Снимок экрана 2023-12-17 в 20.53.26.png Binary files differnew file mode 100644 index 0000000..0da19c5 --- /dev/null +++ b/files/Снимок экрана 2023-12-17 в 20.53.26.png diff --git a/files/Снимок экрана 2023-12-17 в 20.55.53.png b/files/Снимок экрана 2023-12-17 в 20.55.53.png Binary files differnew file mode 100644 index 0000000..9f7dc79 --- /dev/null +++ b/files/Снимок экрана 2023-12-17 в 20.55.53.png diff --git a/files/Снимок экрана 2024-03-10 в 20.55.05.png b/files/Снимок экрана 2024-03-10 в 20.55.05.png Binary files differnew file mode 100644 index 0000000..362c4e3 --- /dev/null +++ b/files/Снимок экрана 2024-03-10 в 20.55.05.png diff --git a/files/Снимок экрана 2024-03-14 в 13.21.49.png b/files/Снимок экрана 2024-03-14 в 13.21.49.png Binary files differnew file mode 100644 index 0000000..8fe9ab2 --- /dev/null +++ b/files/Снимок экрана 2024-03-14 в 13.21.49.png diff --git a/files/Снимок экрана 2024-03-14 в 13.22.17.png b/files/Снимок экрана 2024-03-14 в 13.22.17.png Binary files differnew file mode 100644 index 0000000..1d11b2d --- /dev/null +++ b/files/Снимок экрана 2024-03-14 в 13.22.17.png diff --git a/files/Снимок экрана 2024-03-14 в 13.25.59.png b/files/Снимок экрана 2024-03-14 в 13.25.59.png Binary files differnew file mode 100644 index 0000000..f514466 --- /dev/null +++ b/files/Снимок экрана 2024-03-14 в 13.25.59.png diff --git a/files/Снимок экрана 2024-03-14 в 13.27.58.png b/files/Снимок экрана 2024-03-14 в 13.27.58.png Binary files differnew file mode 100644 index 0000000..7c63518 --- /dev/null +++ b/files/Снимок экрана 2024-03-14 в 13.27.58.png diff --git a/files/Снимок экрана 2024-03-14 в 13.29.48.png b/files/Снимок экрана 2024-03-14 в 13.29.48.png Binary files differnew file mode 100644 index 0000000..bf572ae --- /dev/null +++ b/files/Снимок экрана 2024-03-14 в 13.29.48.png diff --git a/files/Снимок экрана 2024-03-14 в 13.32.01.png b/files/Снимок экрана 2024-03-14 в 13.32.01.png Binary files differnew file mode 100644 index 0000000..e40eb19 --- /dev/null +++ b/files/Снимок экрана 2024-03-14 в 13.32.01.png diff --git a/files/Снимок экрана 2024-03-25 в 22.49.58.png b/files/Снимок экрана 2024-03-25 в 22.49.58.png Binary files differnew file mode 100644 index 0000000..7ca4a5f --- /dev/null +++ b/files/Снимок экрана 2024-03-25 в 22.49.58.png diff --git a/files/Снимок экрана 2024-09-15 в 18.55.25.png b/files/Снимок экрана 2024-09-15 в 18.55.25.png Binary files differnew file mode 100644 index 0000000..a8fca78 --- /dev/null +++ b/files/Снимок экрана 2024-09-15 в 18.55.25.png diff --git a/files/Снимок экрана 2024-09-15 в 19.02.02.png b/files/Снимок экрана 2024-09-15 в 19.02.02.png Binary files differnew file mode 100644 index 0000000..a84ef47 --- /dev/null +++ b/files/Снимок экрана 2024-09-15 в 19.02.02.png diff --git a/files/Снимок экрана 2024-09-15 в 19.10.29.png b/files/Снимок экрана 2024-09-15 в 19.10.29.png Binary files differnew file mode 100644 index 0000000..b42a047 --- /dev/null +++ b/files/Снимок экрана 2024-09-15 в 19.10.29.png diff --git a/files/Снимок экрана 2024-09-16 в 21.31.06.png b/files/Снимок экрана 2024-09-16 в 21.31.06.png Binary files differnew file mode 100644 index 0000000..0010f2b --- /dev/null +++ b/files/Снимок экрана 2024-09-16 в 21.31.06.png diff --git a/files/Снимок экрана 2024-09-16 в 21.36.18.png b/files/Снимок экрана 2024-09-16 в 21.36.18.png Binary files differnew file mode 100644 index 0000000..421a0d5 --- /dev/null +++ b/files/Снимок экрана 2024-09-16 в 21.36.18.png diff --git a/files/Снимок экрана 2024-09-16 в 21.39.34.png b/files/Снимок экрана 2024-09-16 в 21.39.34.png Binary files differnew file mode 100644 index 0000000..a884d26 --- /dev/null +++ b/files/Снимок экрана 2024-09-16 в 21.39.34.png diff --git a/files/Снимок экрана 2024-09-16 в 21.45.31.png b/files/Снимок экрана 2024-09-16 в 21.45.31.png Binary files differnew file mode 100644 index 0000000..85e2116 --- /dev/null +++ b/files/Снимок экрана 2024-09-16 в 21.45.31.png diff --git a/files/Снимок экрана 2024-09-16 в 21.45.54.png b/files/Снимок экрана 2024-09-16 в 21.45.54.png Binary files differnew file mode 100644 index 0000000..98a2112 --- /dev/null +++ b/files/Снимок экрана 2024-09-16 в 21.45.54.png diff --git a/files/Снимок экрана 2024-11-01 в 01.39.59.png b/files/Снимок экрана 2024-11-01 в 01.39.59.png Binary files differnew file mode 100644 index 0000000..c8f630a --- /dev/null +++ b/files/Снимок экрана 2024-11-01 в 01.39.59.png diff --git a/files/Снимок экрана 2025-02-02 в 18.05.19.png b/files/Снимок экрана 2025-02-02 в 18.05.19.png Binary files differnew file mode 100644 index 0000000..204dd03 --- /dev/null +++ b/files/Снимок экрана 2025-02-02 в 18.05.19.png diff --git a/files/Снимок экрана 2025-02-02 в 18.06.09.png b/files/Снимок экрана 2025-02-02 в 18.06.09.png Binary files differnew file mode 100644 index 0000000..7aadd31 --- /dev/null +++ b/files/Снимок экрана 2025-02-02 в 18.06.09.png diff --git a/files/Снимок экрана 2025-02-02 в 18.13.01.png b/files/Снимок экрана 2025-02-02 в 18.13.01.png Binary files differnew file mode 100644 index 0000000..d5428a2 --- /dev/null +++ b/files/Снимок экрана 2025-02-02 в 18.13.01.png diff --git a/files/Снимок экрана 2025-02-02 в 18.29.43.png b/files/Снимок экрана 2025-02-02 в 18.29.43.png Binary files differnew file mode 100644 index 0000000..488777c --- /dev/null +++ b/files/Снимок экрана 2025-02-02 в 18.29.43.png diff --git a/files/Снимок экрана 2025-02-02 в 18.30.02.png b/files/Снимок экрана 2025-02-02 в 18.30.02.png Binary files differnew file mode 100644 index 0000000..325061e --- /dev/null +++ b/files/Снимок экрана 2025-02-02 в 18.30.02.png diff --git a/files/Снимок экрана 2025-02-11 в 01.15.10.png b/files/Снимок экрана 2025-02-11 в 01.15.10.png Binary files differnew file mode 100644 index 0000000..b9fd663 --- /dev/null +++ b/files/Снимок экрана 2025-02-11 в 01.15.10.png diff --git a/files/Снимок экрана 2025-02-11 в 01.17.25.png b/files/Снимок экрана 2025-02-11 в 01.17.25.png Binary files differnew file mode 100644 index 0000000..b9d05c4 --- /dev/null +++ b/files/Снимок экрана 2025-02-11 в 01.17.25.png diff --git a/files/Снимок экрана 2025-05-14 в 00.35.18.png b/files/Снимок экрана 2025-05-14 в 00.35.18.png Binary files differnew file mode 100644 index 0000000..c841e3d --- /dev/null +++ b/files/Снимок экрана 2025-05-14 в 00.35.18.png diff --git a/files/Снимок экрана 2025-05-14 в 00.38.05.png b/files/Снимок экрана 2025-05-14 в 00.38.05.png Binary files differnew file mode 100644 index 0000000..e7c051a --- /dev/null +++ b/files/Снимок экрана 2025-05-14 в 00.38.05.png |
