From e4be2e181160fb32278d4da2e4227292721dee39 Mon Sep 17 00:00:00 2001 From: Disledg Date: Sat, 21 Dec 2024 16:29:17 +0100 Subject: [PATCH] revert bfe898f10ac9a85114ee753ae2323f213b420d38 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revert Пиздец очередной --- app/routes/payment_routes.py => .env | 0 .gitignore | 3 +- .vscode/settings.json | 3 - README.md | 37 ++--- app/configdb1.py | 78 +++++++++ app/model1.py | 137 ++++++++++++++++ app/mongodb1.py | 118 ++++++++++++++ app/postgresql1.py | 191 ++++++++++++++++++++++ app/routes/__init__.py | 6 - app/routes/auth_routes.py | 0 app/routes/subscription_routes.py | 72 --------- app/routes/user_routes.py | 68 -------- app/services/__init__.py | 0 app/services/db_manager.py | 211 ------------------------ app/services/mongo_rep.py | 158 ------------------ app/services/payment_service.py | 0 app/services/postgres_rep.py | 102 ------------ app/services/user_service.py | 0 app/services/xui_rep.py | 232 --------------------------- config.py | 0 instance/configdb.py | 72 --------- instance/model.py | 60 ------- main.py | 42 ----- requirements.txt | 49 +++--- run.py | 0 25 files changed, 574 insertions(+), 1065 deletions(-) rename app/routes/payment_routes.py => .env (100%) delete mode 100644 .vscode/settings.json create mode 100644 app/configdb1.py create mode 100644 app/model1.py create mode 100644 app/mongodb1.py create mode 100644 app/postgresql1.py create mode 100644 app/routes/auth_routes.py delete mode 100644 app/routes/subscription_routes.py create mode 100644 app/services/__init__.py delete mode 100644 app/services/db_manager.py delete mode 100644 app/services/mongo_rep.py create mode 100644 app/services/payment_service.py delete mode 100644 app/services/postgres_rep.py create mode 100644 app/services/user_service.py delete mode 100644 app/services/xui_rep.py create mode 100644 config.py delete mode 100644 instance/configdb.py delete mode 100644 instance/model.py delete mode 100644 main.py create mode 100644 run.py diff --git a/app/routes/payment_routes.py b/.env similarity index 100% rename from app/routes/payment_routes.py rename to .env diff --git a/.gitignore b/.gitignore index 4d67604..f96f1b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.code-workspace +TBot/ +logs/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b881eff..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.analysis.autoImportCompletions": true -} \ No newline at end of file diff --git a/README.md b/README.md index fc721ad..6e28450 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,36 @@ -# VPN Configuration Sales Bot (FastAPI Backend) -Проект представляет собой FastAPI сервер, интегрированный с Telegram API для автоматизации продажи VPN конфигураций и управления пользователями. Бекенд обрабатывает запросы, взаимодействует с базами данных PostgreSQL и MongoDB, и обеспечивает удобный интерфейс для пользователей и администраторов. + +# VPN Configuration Sales Bot + +Бот для Telegram, предназначенный для продажи VPN конфигураций. Проект создан с целью автоматизации процесса продажи VPN и управления пользователями через удобный интерфейс Telegram, с использованием баз данных PostgreSQL и MongoDB. ## 📋 Описание -FastAPI сервер предоставляет REST API для управления функционалом Telegram-бота, включая регистрацию, авторизацию, покупку VPN конфигураций и администрирование. Использование FastAPI обеспечивает высокую производительность, читаемый код и расширяемость проекта. +Этот проект представляет собой Telegram-бота, который позволяет пользователям приобретать VPN настройки, а администраторам – управлять конфигурациями и отслеживать заказы. Бот поддерживает работу с двумя базами данных для обеспечения гибкого хранения и обработки данных. ## 🛠 Функционал -- Отсутствует сука +- Регистрация и авторизация пользователей +- Покупка VPN конфигураций ### В стадии разработки -- Всё в разработке +- Автоматическая выдача VPN конфигураций +- Поддержка двух баз данных (PostgreSQL и MongoDB) для более гибкого и масштабируемого хранения данных +- Панель администратора для управления заказами и пользователями +- Саппорт система ## 🚀 Технологии -- **Python 3.x** -- **FastAPI** +- **Python** - **Telegram API** -- **PostgreSQL (SQLAlchemy)** -- **MongoDB (PyMongo)** -- **JWT (JSON Web Tokens):** для аутентификации и авторизации. -- **Pydantic:** для валидации данных. -- **Alembic:** для миграций базы данных PostgreSQL. -- **Docker:** для контейнеризации и упрощения развёртывания. - -## 📜 Основные API эндпоинты - -Сосут эендпоинты блять, не сделаны ещё +- **PostgreSQL** +- **MongoDB** +- **SQLAlchemy** - для взаимодействия с PostgreSQL +- **PyMongo** - для работы с MongoDB ## 📝 Лицензия -Этот проект распространяется под лицензией MIT License. Подробности в файле [LICENSE](./LICENSE). \ No newline at end of file +Этот проект распространяется под лицензией MIT License. Подробности в файле [LICENSE](./LICENSE). + + diff --git a/app/configdb1.py b/app/configdb1.py new file mode 100644 index 0000000..842a143 --- /dev/null +++ b/app/configdb1.py @@ -0,0 +1,78 @@ +import os +from flask import Flask, g +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from motor.motor_asyncio import AsyncIOMotorClient +from databases.model import Base +import asyncio + +# Инициализация Flask +app = Flask(__name__) + +# Настройки PostgreSQL из переменных окружения +POSTGRES_DSN = os.getenv("POSTGRES_URL") + +# Создание движка для PostgreSQL +postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) +AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) + +# Настройки MongoDB из переменных окружения +MONGO_URI = os.getenv("MONGO_URL") +DATABASE_NAME = os.getenv("DB_NAME") + +# Создание клиента MongoDB +mongo_client = AsyncIOMotorClient(MONGO_URI) +mongo_db = mongo_client[DATABASE_NAME] + +@app.before_first_request +async def init_databases(): + """ + Инициализация подключений к PostgreSQL и MongoDB перед первым запросом. + """ + try: + # Инициализация PostgreSQL + async with postgres_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + print("PostgreSQL connected.") + + # Проверка подключения к MongoDB + await mongo_client.admin.command("ping") + print("MongoDB connected.") + except Exception as e: + print(f"Database initialization failed: {e}") + +@app.teardown_appcontext +def close_connections(exception=None): + """ + Закрытие соединений с базами данных после окончания работы приложения. + """ + asyncio.run(postgres_engine.dispose()) + mongo_client.close() + print("Database connections closed.") + +@app.route("/postgres_session") +async def get_postgres_session(): + """ + Пример использования сессии PostgreSQL в маршруте Flask. + """ + try: + async with AsyncSessionLocal() as session: + # Здесь можно выполнить запросы к базе данных PostgreSQL + result = await session.execute("SELECT 1") + return {"postgres_result": result.scalar()} + except Exception as e: + return {"error": str(e)}, 500 + +@app.route("/mongo_status") +async def get_mongo_status(): + """ + Пример проверки MongoDB в маршруте Flask. + """ + try: + await mongo_client.admin.command("ping") + return {"mongo_status": "connected"} + except Exception as e: + return {"error": str(e)}, 500 + +if __name__ == "__main__": + app.run(debug=True) diff --git a/app/model1.py b/app/model1.py new file mode 100644 index 0000000..cca8760 --- /dev/null +++ b/app/model1.py @@ -0,0 +1,137 @@ +import os +from flask import Flask, g +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from motor.motor_asyncio import AsyncIOMotorClient +from sqlalchemy import Column, String, Numeric, DateTime, Boolean, ForeignKey, Integer +from sqlalchemy.orm import declarative_base, relationship +from datetime import datetime +import uuid +import asyncio + +# Инициализация Flask +app = Flask(__name__) + +# Настройки PostgreSQL из переменных окружения +POSTGRES_DSN = os.getenv("POSTGRES_URL") + +# Создание движка для PostgreSQL +postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) +AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) + +# Настройки MongoDB из переменных окружения +MONGO_URI = os.getenv("MONGO_URL") +DATABASE_NAME = os.getenv("DB_NAME") + +# Создание клиента MongoDB +mongo_client = AsyncIOMotorClient(MONGO_URI) +mongo_db = mongo_client[DATABASE_NAME] + +# SQLAlchemy Base +Base = declarative_base() + +def generate_uuid(): + return str(uuid.uuid4()) + +"""Пользователи""" +class User(Base): + __tablename__ = 'users' + + id = Column(String, primary_key=True, default=generate_uuid) + telegram_id = Column(Integer, unique=True, nullable=False) + username = Column(String) + balance = Column(Numeric(10, 2), default=0.0) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + subscriptions = relationship("Subscription", back_populates="user") + transactions = relationship("Transaction", back_populates="user") + admins = relationship("Administrators", back_populates="user") + +"""Подписки""" +class Subscription(Base): + __tablename__ = 'subscriptions' + + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey('users.id')) + vpn_server_id = Column(String) + plan = Column(String) + expiry_date = Column(DateTime) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="subscriptions") + +"""Транзакции""" +class Transaction(Base): + __tablename__ = 'transactions' + + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey('users.id')) + amount = Column(Numeric(10, 2)) + transaction_type = Column(String) + created_at = Column(DateTime, default=datetime.utcnow) + + user = relationship("User", back_populates="transactions") + +"""Администраторы""" +class Administrators(Base): + __tablename__ = 'admins' + + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey('users.id')) + + user = relationship("User", back_populates="admins") + +@app.before_first_request +async def init_databases(): + """ + Инициализация подключений к PostgreSQL и MongoDB перед первым запросом. + """ + try: + # Инициализация PostgreSQL + async with postgres_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + print("PostgreSQL connected.") + + # Проверка подключения к MongoDB + await mongo_client.admin.command("ping") + print("MongoDB connected.") + except Exception as e: + print(f"Database initialization failed: {e}") + +@app.teardown_appcontext +def close_connections(exception=None): + """ + Закрытие соединений с базами данных после окончания работы приложения. + """ + asyncio.run(postgres_engine.dispose()) + mongo_client.close() + print("Database connections closed.") + +@app.route("/postgres_session") +async def get_postgres_session(): + """ + Пример использования сессии PostgreSQL в маршруте Flask. + """ + try: + async with AsyncSessionLocal() as session: + # Здесь можно выполнить запросы к базе данных PostgreSQL + result = await session.execute("SELECT 1") + return {"postgres_result": result.scalar()} + except Exception as e: + return {"error": str(e)}, 500 + +@app.route("/mongo_status") +async def get_mongo_status(): + """ + Пример проверки MongoDB в маршруте Flask. + """ + try: + await mongo_client.admin.command("ping") + return {"mongo_status": "connected"} + except Exception as e: + return {"error": str(e)}, 500 + +if __name__ == "__main__": + app.run(debug=True) diff --git a/app/mongodb1.py b/app/mongodb1.py new file mode 100644 index 0000000..121e929 --- /dev/null +++ b/app/mongodb1.py @@ -0,0 +1,118 @@ +import os +import logging +from flask import Flask, jsonify, request +from motor.motor_asyncio import AsyncIOMotorClient +from bson import ObjectId +from flask.logging import default_handler +import asyncio + +app = Flask(__name__) + +# Настройки логирования +logger = logging.getLogger("MongoDBRepository") +logger.setLevel(logging.DEBUG) +logger.addHandler(default_handler) + +# Настройки MongoDB из переменных окружения +mongo_uri = os.getenv("MONGO_URL", "mongodb://localhost:27017") +database_name = os.getenv("DB_NAME", "mydatabase") +server_collection = os.getenv("SERVER_COLLECTION", "servers") +plan_collection = os.getenv("PLAN_COLLECTION", "plans") + +# Подключение к базе данных +client = AsyncIOMotorClient(mongo_uri) +db = client[database_name] +servers = db[server_collection] +plans = db[plan_collection] + +@app.route('/plans', methods=['POST']) +async def add_subscription_plan(): + plan_data = request.json + result = await plans.insert_one(plan_data) + logger.debug(f"Тарифный план добавлен с ID: {result.inserted_id}") + return jsonify({"inserted_id": str(result.inserted_id)}), 201 + +@app.route('/plans/', methods=['GET']) +async def get_subscription_plan(plan_id): + plan = await plans.find_one({"_id": ObjectId(plan_id)}) + if plan: + logger.debug(f"Найден тарифный план: {plan}") + plan["_id"] = str(plan["_id"]) + return jsonify(plan) + else: + logger.error(f"Тарифный план {plan_id} не найден.") + return jsonify({"error": "Plan not found"}), 404 + +@app.route('/servers', methods=['POST']) +async def add_server(): + server_data = request.json + result = await servers.insert_one(server_data) + logger.debug(f"VPN сервер добавлен с ID: {result.inserted_id}") + return jsonify({"inserted_id": str(result.inserted_id)}), 201 + +@app.route('/servers/', methods=['GET']) +async def get_server(server_name): + server = await servers.find_one({"server.name": server_name}) + if server: + logger.debug(f"Найден VPN сервер: {server}") + server["_id"] = str(server["_id"]) + return jsonify(server) + else: + logger.debug(f"VPN сервер с именем {server_name} не найден.") + return jsonify({"error": "Server not found"}), 404 + +@app.route('/servers/least_clients', methods=['GET']) +async def get_server_with_least_clients(): + pipeline = [ + {"$addFields": {"current_clients": {"$size": {"$ifNull": ["$clients", []]}}}}, + {"$sort": {"current_clients": 1}}, + {"$limit": 1} + ] + result = await servers.aggregate(pipeline).to_list(length=1) + if result: + server = result[0] + server["_id"] = str(server["_id"]) + logger.debug(f"Найден сервер с наименьшим количеством клиентов: {server}") + return jsonify(server) + else: + logger.debug("Не найдено серверов.") + return jsonify({"error": "No servers found"}), 404 + +@app.route('/servers/', methods=['PUT']) +async def update_server(server_id): + update_data = request.json + result = await servers.update_one({"_id": ObjectId(server_id)}, {"$set": update_data}) + if result.matched_count > 0: + logger.debug(f"VPN сервер с ID {server_id} обновлен.") + return jsonify({"updated": True}) + else: + logger.debug(f"VPN сервер с ID {server_id} не найден.") + return jsonify({"error": "Server not found"}), 404 + +@app.route('/servers/', methods=['DELETE']) +async def delete_server(server_id): + result = await servers.delete_one({"_id": ObjectId(server_id)}) + if result.deleted_count > 0: + logger.debug(f"VPN сервер с ID {server_id} удален.") + return jsonify({"deleted": True}) + else: + logger.debug(f"VPN сервер с ID {server_id} не найден.") + return jsonify({"error": "Server not found"}), 404 + +@app.route('/servers', methods=['GET']) +async def list_servers(): + server_list = await servers.find().to_list(length=1000) + for server in server_list: + server["_id"] = str(server["_id"]) + logger.debug(f"Найдено {len(server_list)} VPN серверов.") + return jsonify(server_list) + +@app.route('/shutdown', methods=['POST']) +async def close_connection(): + client.close() + logger.debug("Подключение к MongoDB закрыто.") + return jsonify({"message": "Connection closed"}), 200 + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(app.run(debug=True)) diff --git a/app/postgresql1.py b/app/postgresql1.py new file mode 100644 index 0000000..bc851af --- /dev/null +++ b/app/postgresql1.py @@ -0,0 +1,191 @@ +from flask import Flask, request, jsonify +from databases.model import User, Subscription, Transaction +from sqlalchemy.future import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import desc +from dateutil.relativedelta import relativedelta +from datetime import datetime +from utils.panel import PanelInteraction +from databases.mongodb import MongoDBRepository +import random +import string +import logging +import asyncio + +app = Flask(__name__) + +# Настройка логирования +logger = logging.getLogger(__name__) +mongo_repo = MongoDBRepository() + +# Генератор сессий передаётся извне (например, через dependency injection) +session_generator = None + +def generate_string(length): + """ + Генерирует случайную строку заданной длины. + """ + characters = string.ascii_lowercase + string.digits + return ''.join(random.choices(characters, k=length)) + +@app.route('/create_user', methods=['POST']) +def create_user(): + telegram_id = request.json.get('telegram_id') + if not telegram_id: + return jsonify({'error': 'Telegram ID is required'}), 400 + + async def process(): + async for session in session_generator(): + try: + username = generate_string(6) + result = await session.execute(select(User).where(User.telegram_id == int(telegram_id))) + user = result.scalars().first() + if not user: + new_user = User(telegram_id=int(telegram_id), username=username) + session.add(new_user) + await session.commit() + return jsonify({'user': new_user.id, 'username': new_user.username}), 201 + return jsonify({'user': user.id, 'username': user.username}), 200 + except SQLAlchemyError as e: + logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") + await session.rollback() + return jsonify({'error': 'Internal server error'}), 500 + + return asyncio.run(process()) + +@app.route('/get_user/', methods=['GET']) +def get_user_by_telegram_id(telegram_id): + async def process(): + async for session in session_generator(): + try: + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + user = result.scalars().first() + if user: + return jsonify({'id': user.id, 'username': user.username, 'balance': user.balance}), 200 + return jsonify({'error': 'User not found'}), 404 + except SQLAlchemyError as e: + logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") + return jsonify({'error': 'Internal server error'}), 500 + + return asyncio.run(process()) + +@app.route('/update_balance', methods=['POST']) +def update_balance(): + data = request.json + telegram_id = data.get('telegram_id') + amount = data.get('amount') + + if not telegram_id or amount is None: + return jsonify({'error': 'Telegram ID and amount are required'}), 400 + + async def process(): + async for session in session_generator(): + try: + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + user = result.scalars().first() + if user: + user.balance += int(amount) + transaction = Transaction(user_id=user.id, amount=amount) + session.add(transaction) + await session.commit() + return jsonify({'balance': user.balance}), 200 + return jsonify({'error': 'User not found'}), 404 + except SQLAlchemyError as e: + logger.error(f"Ошибка при обновлении баланса: {e}") + await session.rollback() + return jsonify({'error': 'Internal server error'}), 500 + + return asyncio.run(process()) + +@app.route('/buy_subscription', methods=['POST']) +def buy_subscription(): + data = request.json + telegram_id = data.get('telegram_id') + plan_id = data.get('plan_id') + + if not telegram_id or not plan_id: + return jsonify({'error': 'Telegram ID and Plan ID are required'}), 400 + + async def process(): + async for session in session_generator(): + try: + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + user = result.scalars().first() + if not user: + return jsonify({'error': 'User not found'}), 404 + + plan = await mongo_repo.get_subscription_plan(plan_id) + if not plan: + return jsonify({'error': 'Plan not found'}), 404 + + cost = int(plan['price']) + if user.balance >= cost: + user.balance -= cost + expiry_date = datetime.utcnow() + relativedelta(months=plan['duration_months']) + server = await mongo_repo.get_server_with_least_clients() + + new_subscription = Subscription(user_id=user.id, vpn_server_id=str(server['server']['name']), + plan=plan_id, expiry_date=expiry_date) + session.add(new_subscription) + await session.commit() + return jsonify({'message': 'Subscription purchased successfully'}), 200 + return jsonify({'error': 'Insufficient funds'}), 400 + except SQLAlchemyError as e: + logger.error(f"Ошибка при покупке подписки {plan_id} для пользователя {telegram_id}: {e}") + await session.rollback() + return jsonify({'error': 'Internal server error'}), 500 + + return asyncio.run(process()) + +@app.route('/add_to_server', methods=['POST']) +def add_to_server(): + data = request.json + telegram_id = data.get('telegram_id') + + if not telegram_id: + return jsonify({'error': 'Telegram ID is required'}), 400 + + async def process(): + async for session in session_generator(): + try: + result = await session.execute(select(Subscription).join(User).where(User.telegram_id == int(telegram_id))) + user_sub = result.scalars().first() + + if not user_sub: + logger.error(f"Не удалось найти подписку для пользователя с Telegram ID {telegram_id}.") + return jsonify({'error': 'Subscription not found'}), 404 + + user_result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + user = user_result.scalars().first() + + server = await mongo_repo.get_server(user_sub.vpn_server_id) + if not server: + logger.error(f"Не удалось найти сервер с ID {user_sub.vpn_server_id}.") + return jsonify({'error': 'Server not found'}), 404 + + server_info = server['server'] + url_base = f"https://{server_info['ip']}:{server_info['port']}/{server_info['secretKey']}" + login_data = { + 'username': server_info['login'], + 'password': server_info['password'], + } + + panel = PanelInteraction(url_base, login_data, logger) + expiry_date_iso = user_sub.expiry_date.isoformat() + response = await panel.add_client(user.id, expiry_date_iso, user.username) + + if response == "OK": + logger.info(f"Клиент {telegram_id} успешно добавлен на сервер.") + return jsonify({'message': 'Client added successfully'}), 200 + else: + logger.error(f"Ошибка при добавлении клиента {telegram_id} на сервер: {response}") + return jsonify({'error': 'Failed to add client to server'}), 500 + + except Exception as e: + logger.error(f"Ошибка при установке на сервер для пользователя {telegram_id}: {e}") + return jsonify({'error': 'Internal server error'}), 500 + + return asyncio.run(process()) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 00b24f3..e69de29 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,6 +0,0 @@ -from .payment_routes import router as payment_router -from .user_routes import router as user_router -from .subscription_routes import router as subscription_router - -# Экспорт всех маршрутов -__all__ = ["payment_router", "user_router", "subscription_router"] diff --git a/app/routes/auth_routes.py b/app/routes/auth_routes.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/subscription_routes.py b/app/routes/subscription_routes.py deleted file mode 100644 index 8e9733d..0000000 --- a/app/routes/subscription_routes.py +++ /dev/null @@ -1,72 +0,0 @@ -from fastapi import APIRouter, HTTPException, Depends -from pydantic import BaseModel -from app.services.db_manager import DatabaseManager - -router = APIRouter() - -# DatabaseManager должен передаваться через Depends -def get_database_manager(): - # Здесь должна быть логика инициализации DatabaseManager - return DatabaseManager() - -# Схемы запросов и ответов -class BuySubscriptionRequest(BaseModel): - telegram_id: int - plan_id: str - -class SubscriptionResponse(BaseModel): - id: str - plan: str - vpn_server_id: str - expiry_date: str - created_at: str - updated_at: str - -# Эндпоинт для покупки подписки -@router.post("/subscription/buy", response_model=dict) -async def buy_subscription( - request_data: BuySubscriptionRequest, - database_manager: DatabaseManager = Depends(get_database_manager) -): - """ - Покупка подписки. - """ - try: - result = await database_manager.buy_sub(request_data.telegram_id, request_data.plan_id) - - if result == "ERROR": - raise HTTPException(status_code=500, detail="Failed to buy subscription") - elif result == "INSUFFICIENT_FUNDS": - raise HTTPException(status_code=400, detail="Insufficient funds") - - return {"message": "Subscription purchased successfully"} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -# Эндпоинт для получения последней подписки -@router.get("/subscription/{user_id}/last", response_model=list[SubscriptionResponse]) -async def last_subscription( - user_id: int, - database_manager: DatabaseManager = Depends(get_database_manager) -): - """ - Получение последней подписки пользователя. - """ - try: - subscriptions = await database_manager.last_subscription(user_id) - if subscriptions == "ERROR": - raise HTTPException(status_code=500, detail="Failed to fetch subscriptions") - - return [ - { - "id": sub.id, - "plan": sub.plan, - "vpn_server_id": sub.vpn_server_id, - "expiry_date": sub.expiry_date.isoformat(), - "created_at": sub.created_at.isoformat(), - "updated_at": sub.updated_at.isoformat(), - } for sub in subscriptions - ] - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/routes/user_routes.py b/app/routes/user_routes.py index fed3f91..e69de29 100644 --- a/app/routes/user_routes.py +++ b/app/routes/user_routes.py @@ -1,68 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from app.services.db_manager import DatabaseManager -from instance.configdb import get_database_manager -from pydantic import BaseModel - -router = APIRouter() - -# Модели запросов и ответов -class CreateUserRequest(BaseModel): - telegram_id: int - -class UserResponse(BaseModel): - id: str - telegram_id: int - username: str - balance: float - created_at: str - updated_at: str - - -@router.post("/user/create", response_model=UserResponse, summary="Создать пользователя") -async def create_user( - request: CreateUserRequest, - db_manager: DatabaseManager = Depends(get_database_manager) -): - """ - Создание пользователя через Telegram ID. - """ - try: - user = await db_manager.create_user(request.telegram_id) - if user == "ERROR": - raise HTTPException(status_code=500, detail="Failed to create user") - - return UserResponse( - id=user.id, - telegram_id=user.telegram_id, - username=user.username, - balance=user.balance, - created_at=user.created_at.isoformat(), - updated_at=user.updated_at.isoformat() - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@router.get("/user/{telegram_id}", response_model=UserResponse, summary="Получить информацию о пользователе") -async def get_user( - telegram_id: int, - db_manager: DatabaseManager = Depends(get_database_manager) -): - """ - Получение информации о пользователе. - """ - try: - user = await db_manager.get_user_by_telegram_id(telegram_id) - if not user: - raise HTTPException(status_code=404, detail="User not found") - - return UserResponse( - id=user.id, - telegram_id=user.telegram_id, - username=user.username, - balance=user.balance, - created_at=user.created_at.isoformat(), - updated_at=user.updated_at.isoformat() - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/db_manager.py b/app/services/db_manager.py deleted file mode 100644 index 0a6e944..0000000 --- a/app/services/db_manager.py +++ /dev/null @@ -1,211 +0,0 @@ -from instance.model import User, Subscription, Transaction, Administrators -from sqlalchemy.future import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy import desc -from dateutil.relativedelta import relativedelta -from datetime import datetime -from .xui_rep import PanelInteraction -from .mongo_rep import MongoDBRepository -import random -import string -import logging -import asyncio - -class DatabaseManager: - def __init__(self, session_generator): - """ - Инициализация с асинхронным генератором сессий (например, get_postgres_session). - """ - self.session_generator = session_generator - self.logger = logging.getLogger(__name__) - self.mongo_repo = MongoDBRepository() - - async def create_user(self, telegram_id: int): - """ - Создаёт нового пользователя, если его нет. - """ - async for session in self.session_generator(): - try: - username = self.generate_string(6) - result = await session.execute(select(User).where(User.telegram_id == int(telegram_id))) - user = result.scalars().first() - if not user: - new_user = User(telegram_id=int(telegram_id), username=username) - session.add(new_user) - await session.commit() - return new_user - return user - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") - await session.rollback() - return "ERROR" - - async def get_user_by_telegram_id(self, telegram_id: int): - """ - Возвращает пользователя по Telegram ID. - """ - async for session in self.session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - return result.scalars().first() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") - return None - - async def add_transaction(self, user_id: int, amount: float): - """ - Добавляет транзакцию для пользователя. - """ - async for session in self.session_generator(): - try: - transaction = Transaction(user_id=user_id, amount=amount) - session.add(transaction) - await session.commit() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка добавления транзакции для пользователя {user_id}: {e}") - await session.rollback() - - async def update_balance(self, telegram_id: int, amount: float): - """ - Обновляет баланс пользователя и добавляет транзакцию. - """ - async for session in self.session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = result.scalars().first() - if user: - user.balance += int(amount) - await self.add_transaction(user.id, amount) - await session.commit() - else: - self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") - return "ERROR" - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при обновлении баланса: {e}") - await session.rollback() - return "ERROR" - - async def last_subscription(self, user_id: int): - """ - Возвращает список подписок пользователя. - """ - async for session in self.session_generator(): - try: - result = await session.execute( - select(Subscription) - .where(Subscription.user_id == user_id) - .order_by(desc(Subscription.created_at)) - ) - return result.scalars().all() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}") - return "ERROR" - - async def last_transaction(self, user_id: int): - """ - Возвращает список транзакций пользователя. - """ - async for session in self.session_generator(): - try: - result = await session.execute( - select(Transaction) - .where(Transaction.user_id == user_id) - .order_by(desc(Transaction.created_at)) - ) - transactions = result.scalars().all() - return transactions - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}") - return "ERROR" - - async def buy_sub(self, telegram_id: str, plan_id: str): - async for session in self.session_generator(): - try: - result = await self.create_user(telegram_id) - if not result: - self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.") - return "ERROR" - - # Получение тарифного плана из MongoDB - plan = await self.mongo_repo.get_subscription_plan(plan_id) - if not plan: - self.logger.error(f"Тарифный план {plan_id} не найден.") - return "ERROR" - - # Проверка достаточности средств - cost = int(plan["price"]) - if result.balance < cost: - self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.") - return "INSUFFICIENT_FUNDS" - - # Списываем средства - result.balance -= cost - - # Создаем подписку - expiry_date = datetime.utcnow() + relativedelta(months=plan["duration_months"]) - server = await self.mongo_repo.get_server_with_least_clients() - self.logger.info(f"Выбран сервер для подписки: {server}") - new_subscription = Subscription( - user_id=result.id, - vpn_server_id=str(server['server']["name"]), - plan=plan_id, - expiry_date=expiry_date - ) - session.add(new_subscription) - - # Попытка добавить пользователя на сервер - # Получаем информацию о пользователе - user = result # так как result уже содержит пользователя - if not user: - self.logger.error(f"Не удалось найти пользователя для добавления на сервер.") - await session.rollback() - return "ERROR" - - # Получаем сервер из MongoDB - server_data = await self.mongo_repo.get_server(new_subscription.vpn_server_id) - if not server_data: - self.logger.error(f"Не удалось найти сервер с ID {new_subscription.vpn_server_id}.") - await session.rollback() - return "ERROR" - - server_info = server_data['server'] - url_base = f"https://{server_info['ip']}:{server_info['port']}/{server_info['secretKey']}" - login_data = { - 'username': server_info['login'], - 'password': server_info['password'], - } - - panel = PanelInteraction(url_base, login_data, self.logger,server_info['certificate']['data']) - expiry_date_iso = new_subscription.expiry_date.isoformat() - - # Добавляем на сервер - response = await panel.add_client(user.id, expiry_date_iso, user.username) - - if response != "OK": - self.logger.error(f"Ошибка при добавлении клиента {telegram_id} на сервер: {response}") - # Если не получилось добавить на сервер, откатываем транзакцию - await session.rollback() - return "ERROR" - - # Если мы здесь - значит и подписка, и добавление на сервер успешны - await session.commit() - self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id} на план {plan_id} и клиент добавлен на сервер.") - return "OK" - - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при покупке подписки {plan_id} для пользователя {telegram_id}: {e}") - await session.rollback() - return "ERROR" - except Exception as e: - self.logger.error(f"Непредвиденная ошибка: {e}") - await session.rollback() - return "ERROR" - - - @staticmethod - def generate_string(length): - """ - Генерирует случайную строку заданной длины. - """ - characters = string.ascii_lowercase + string.digits - return ''.join(random.choices(characters, k=length)) diff --git a/app/services/mongo_rep.py b/app/services/mongo_rep.py deleted file mode 100644 index 522501c..0000000 --- a/app/services/mongo_rep.py +++ /dev/null @@ -1,158 +0,0 @@ -import os -from motor.motor_asyncio import AsyncIOMotorClient -from pymongo.errors import DuplicateKeyError, NetworkTimeout -import logging - - -class MongoDBRepository: - def __init__(self): - # Настройки MongoDB из переменных окружения - mongo_uri = os.getenv("MONGO_URL") - database_name = os.getenv("DB_NAME") - server_collection = os.getenv("SERVER_COLLECTION", "servers") - plan_collection = os.getenv("PLAN_COLLECTION", "plans") - - # Подключение к базе данных и коллекциям - self.client = AsyncIOMotorClient(mongo_uri) - self.db = self.client[database_name] - self.collection = self.db[server_collection] # Коллекция серверов - self.plans_collection = self.db[plan_collection] # Коллекция планов - self.logger = logging.getLogger(__name__) - - async def add_subscription_plan(self, plan_data): - """Добавляет новый тарифный план в коллекцию.""" - try: - result = await self.plans_collection.insert_one(plan_data) - self.logger.debug(f"Тарифный план добавлен с ID: {result.inserted_id}") - return result.inserted_id - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - async def get_subscription_plan(self, plan_name): - """Получает тарифный план по его имени.""" - try: - plan = await self.plans_collection.find_one({"name": plan_name}) - if plan: - self.logger.debug(f"Найден тарифный план: {plan}") - else: - self.logger.error(f"Тарифный план {plan_name} не найден.") - return plan - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - async def add_server(self, server_data): - """Добавляет новый VPN сервер в коллекцию.""" - try: - result = await self.collection.insert_one(server_data) - self.logger.debug(f"VPN сервер добавлен с ID: {result.inserted_id}") - return result.inserted_id - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - async def get_server(self, server_name: str): - """Получает сервер VPN по его ID.""" - try: - server = await self.collection.find_one({"server.name": server_name}) - if server: - self.logger.debug(f"Найден VPN сервер: {server}") - else: - self.logger.debug(f"VPN сервер с ID {server_name} не найден.") - return server - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - async def get_server_with_least_clients(self): - """Возвращает сервер с наименьшим количеством подключенных клиентов.""" - try: - pipeline = [ - { - "$addFields": { - "current_clients": {"$size": {"$ifNull": ["$clients", []]}} - } - }, - { - "$sort": {"current_clients": 1} - }, - { - "$limit": 1 - } - ] - - result = await self.collection.aggregate(pipeline).to_list(length=1) - if result: - server = result[0] - self.logger.debug(f"Найден сервер с наименьшим количеством клиентов: {server}") - return server - else: - self.logger.debug("Не найдено серверов.") - return None - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - async def update_server(self, server_name, update_data): - """Обновляет данные VPN сервера.""" - try: - result = await self.collection.update_one({"server_name": server_name}, {"$set": update_data}) - if result.matched_count > 0: - self.logger.debug(f"VPN сервер с ID {server_name} обновлен.") - else: - self.logger.debug(f"VPN сервер с ID {server_name} не найден.") - return result.matched_count > 0 - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - async def delete_server(self, server_name): - """Удаляет VPN сервер по его ID.""" - try: - result = await self.collection.delete_one({"name": server_name}) - if result.deleted_count > 0: - self.logger.debug(f"VPN сервер с ID {server_name} удален.") - else: - self.logger.debug(f"VPN сервер с ID {server_name} не найден.") - return result.deleted_count > 0 - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - async def list_servers(self): - """Возвращает список всех VPN серверов.""" - try: - servers = await self.collection.find().to_list(length=1000) # Получить до 1000 серверов (можно настроить) - self.logger.debug(f"Найдено {len(servers)} VPN серверов.") - return servers - except DuplicateKeyError: - self.logger.error("Дублирующий ключ.") - except NetworkTimeout: - self.logger.error("Сетевой таймаут.") - - - async def __aenter__(self): - """ - Метод вызывается при входе в блок with. - """ - self.logger.debug("Контекстный менеджер: подключение открыто.") - return self - - async def __aexit__(self, exc_type, exc_value, traceback): - """ - Метод вызывается при выходе из блока with. - """ - await self.close_connection() - if exc_type: - self.logger.error(f"Контекстный менеджер завершён с ошибкой: {exc_value}") - else: - self.logger.debug("Контекстный менеджер: подключение закрыто.") - diff --git a/app/services/payment_service.py b/app/services/payment_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/postgres_rep.py b/app/services/postgres_rep.py deleted file mode 100644 index c67df1f..0000000 --- a/app/services/postgres_rep.py +++ /dev/null @@ -1,102 +0,0 @@ -from sqlalchemy.future import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy import desc -from instance.model import User, Subscription, Transaction - - -class PostgresRepository: - def __init__(self, session_generator, logger): - self.session_generator = session_generator - self.logger = logger - - async def create_user(self, telegram_id: int, username: str): - """ - Создаёт нового пользователя в PostgreSQL. - """ - async for session in self.session_generator(): - try: - new_user = User(telegram_id=telegram_id, username=username) - session.add(new_user) - await session.commit() - return new_user - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") - await session.rollback() - return None - - async def get_user_by_telegram_id(self, telegram_id: int): - """ - Возвращает пользователя по Telegram ID. - """ - async for session in self.session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - return result.scalars().first() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") - return None - - async def add_transaction(self, user_id: int, amount: float): - """ - Добавляет транзакцию для пользователя. - """ - async for session in self.session_generator(): - try: - transaction = Transaction(user_id=user_id, amount=amount) - session.add(transaction) - await session.commit() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка добавления транзакции для пользователя {user_id}: {e}") - await session.rollback() - - async def update_balance(self, telegram_id: int, amount: float): - """ - Обновляет баланс пользователя. - """ - async for session in self.session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = result.scalars().first() - if user: - user.balance += amount - await session.commit() - return user - else: - self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") - return None - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при обновлении баланса: {e}") - await session.rollback() - return None - - async def last_subscription(self, user_id: int): - """ - Возвращает последние подписки пользователя. - """ - async for session in self.session_generator(): - try: - result = await session.execute( - select(Subscription) - .where(Subscription.user_id == user_id) - .order_by(desc(Subscription.created_at)) - ) - return result.scalars().all() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}") - return None - - async def last_transaction(self, user_id: int): - """ - Возвращает последние транзакции пользователя. - """ - async for session in self.session_generator(): - try: - result = await session.execute( - select(Transaction) - .where(Transaction.user_id == user_id) - .order_by(desc(Transaction.created_at)) - ) - return result.scalars().all() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}") - return None diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/xui_rep.py b/app/services/xui_rep.py deleted file mode 100644 index 6c64f88..0000000 --- a/app/services/xui_rep.py +++ /dev/null @@ -1,232 +0,0 @@ -import aiohttp -import uuid -import base64 -import ssl - -def generate_uuid(): - return str(uuid.uuid4()) - - -class PanelInteraction: - def __init__(self, base_url, login_data, logger, certificate=None, is_encoded=True): - """ - Initialize the PanelInteraction class. - - :param base_url: Base URL for the panel. - :param login_data: Login data (username/password or token). - :param logger: Logger for debugging. - :param certificate: Certificate content (Base64-encoded or raw string). - :param is_encoded: Indicates whether the certificate is Base64-encoded. - """ - self.base_url = base_url - self.login_data = login_data - self.logger = logger - self.cert_content = self._decode_certificate(certificate, is_encoded) - self.session_id = None # Session ID will be initialized lazily - self.headers = None - - def _decode_certificate(self, certificate, is_encoded): - """ - Decode the provided certificate content. - - :param certificate: Certificate content (Base64-encoded or raw string). - :param is_encoded: Indicates whether the certificate is Base64-encoded. - :return: Decoded certificate content as bytes. - """ - - if not certificate: - self.logger.error("No certificate provided.") - raise ValueError("Certificate is required.") - try: - # Создаем SSLContext - ssl_context = ssl.create_default_context() - - # Декодируем, если нужно - if is_encoded: - certificate = base64.b64decode(certificate).decode() - - # Загружаем сертификат в SSLContext - ssl_context.load_verify_locations(cadata=certificate) - return ssl_context - - except Exception as e: - self.logger.error(f"Error while decoding certificate: {e}") - raise ValueError("Invalid certificate format or content.") from e - - - async def _ensure_logged_in(self): - """ - Ensure the session ID is available for authenticated requests. - """ - if not self.session_id: - self.session_id = await self.login() - if self.session_id: - self.headers = { - 'Accept': 'application/json', - 'Cookie': f'3x-ui={self.session_id}', - 'Content-Type': 'application/json' - } - else: - raise ValueError("Unable to log in and retrieve session ID.") - - async def login(self): - """ - Perform login to the panel. - - :return: Session ID or None. - """ - login_url = f"{self.base_url}/login" - self.logger.info(f"Attempting to login at: {login_url}") - - async with aiohttp.ClientSession() as session: - try: - async with session.post( - login_url, data=self.login_data, ssl=self.cert_content, timeout=10 - ) as response: - if response.status == 200: - session_id = response.cookies.get("3x-ui") - if session_id: - return session_id.value - else: - self.logger.error("Login failed: No session ID received.") - return None - else: - self.logger.error(f"Login failed: {response.status}") - return None - except aiohttp.ClientError as e: - self.logger.error(f"Login request failed: {e}") - return None - - async def get_inbound_info(self, inbound_id): - """ - Fetch inbound information by ID. - - :param inbound_id: ID of the inbound. - :return: JSON response or None. - """ - await self._ensure_logged_in() - url = f"{self.base_url}/panel/api/inbounds/get/{inbound_id}" - async with aiohttp.ClientSession() as session: - try: - async with session.get( - url, headers=self.headers, ssl=self.cert_content, timeout=10 - ) as response: - if response.status == 200: - return await response.json() - else: - self.logger.error(f"Failed to get inbound info: {response.status}") - return None - except aiohttp.ClientError as e: - self.logger.error(f"Get inbound info request failed: {e}") - return None - - async def get_client_traffic(self, email): - """ - Fetch traffic information for a specific client. - - :param email: Client's email. - :return: JSON response or None. - """ - await self._ensure_logged_in() - url = f"{self.base_url}/panel/api/inbounds/getClientTraffics/{email}" - async with aiohttp.ClientSession() as session: - try: - async with session.get( - url, headers=self.headers, ssl=self.cert_content, timeout=10 - ) as response: - if response.status == 200: - return await response.json() - else: - self.logger.error(f"Failed to get client traffic: {response.status}") - return None - except aiohttp.ClientError as e: - self.logger.error(f"Get client traffic request failed: {e}") - return None - - async def update_client_expiry(self, client_uuid, new_expiry_time, client_email): - """ - Update the expiry date of a specific client. - - :param client_uuid: UUID of the client. - :param new_expiry_time: New expiry date in ISO format. - :param client_email: Client's email. - :return: None. - """ - await self._ensure_logged_in() - url = f"{self.base_url}/panel/api/inbounds/updateClient" - update_data = { - "id": 1, - "settings": { - "clients": [ - { - "id": client_uuid, - "alterId": 0, - "email": client_email, - "limitIp": 2, - "totalGB": 0, - "expiryTime": new_expiry_time, - "enable": True, - "tgId": "", - "subId": "" - } - ] - } - } - - async with aiohttp.ClientSession() as session: - try: - async with session.post( - url, headers=self.headers, json=update_data, ssl=self.cert_content - ) as response: - if response.status == 200: - self.logger.info("Client expiry updated successfully.") - else: - self.logger.error(f"Failed to update client expiry: {response.status}") - except aiohttp.ClientError as e: - self.logger.error(f"Update client expiry request failed: {e}") - - async def add_client(self, inbound_id, expiry_date, email): - """ - Add a new client to an inbound. - - :param inbound_id: ID of the inbound. - :param expiry_date: Expiry date in ISO format. - :param email: Client's email. - :return: JSON response or None. - """ - await self._ensure_logged_in() - url = f"{self.base_url}/panel/api/inbounds/addClient" - client_info = { - "clients": [ - { - "id": generate_uuid(), - "alterId": 0, - "email": email, - "limitIp": 2, - "totalGB": 0, - "flow": "xtls-rprx-vision", - "expiryTime": expiry_date, - "enable": True, - "tgId": "", - "subId": "" - } - ] - } - payload = { - "id": inbound_id, - "settings": client_info - } - - async with aiohttp.ClientSession() as session: - try: - async with session.post( - url, headers=self.headers, json=payload, ssl=self.cert_content - ) as response: - if response.status == 200: - return await response.status - else: - self.logger.error(f"Failed to add client: {response.status}") - return None - except aiohttp.ClientError as e: - self.logger.error(f"Add client request failed: {e}") - return None diff --git a/config.py b/config.py new file mode 100644 index 0000000..e69de29 diff --git a/instance/configdb.py b/instance/configdb.py deleted file mode 100644 index 0f171e9..0000000 --- a/instance/configdb.py +++ /dev/null @@ -1,72 +0,0 @@ -import os -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import sessionmaker -from motor.motor_asyncio import AsyncIOMotorClient -from app.services.db_manager import DatabaseManager -from .model import Base - -# Настройки PostgreSQL из переменных окружения -POSTGRES_DSN = os.getenv("POSTGRES_URL") - -# Создание движка для PostgreSQL -postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) -AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) - -# Настройки MongoDB из переменных окружения -MONGO_URI = os.getenv("MONGO_URL") -DATABASE_NAME = os.getenv("DB_NAME") - -# Создание клиента MongoDB -mongo_client = AsyncIOMotorClient(MONGO_URI) -mongo_db = mongo_client[DATABASE_NAME] - -# Инициализация PostgreSQL -async def init_postgresql(): - """ - Инициализация подключения к PostgreSQL. - """ - try: - async with postgres_engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - print("PostgreSQL connected.") - except Exception as e: - print(f"Failed to connect to PostgreSQL: {e}") - -# Инициализация MongoDB -async def init_mongodb(): - """ - Проверка подключения к MongoDB. - """ - try: - # Проверяем подключение к MongoDB - await mongo_client.admin.command("ping") - print("MongoDB connected.") - except Exception as e: - print(f"Failed to connect to MongoDB: {e}") - -# Получение сессии PostgreSQL -async def get_postgres_session(): - """ - Асинхронный генератор сессий PostgreSQL. - """ - async with AsyncSessionLocal() as session: - yield session - -# Закрытие соединений -async def close_connections(): - """ - Закрытие всех соединений с базами данных. - """ - # Закрытие PostgreSQL - await postgres_engine.dispose() - print("PostgreSQL connection closed.") - - # Закрытие MongoDB - mongo_client.close() - print("MongoDB connection closed.") - -def get_database_manager() -> DatabaseManager: - """ - Функция-зависимость для получения экземпляра DatabaseManager. - """ - return DatabaseManager(get_postgres_session) diff --git a/instance/model.py b/instance/model.py deleted file mode 100644 index cea7a32..0000000 --- a/instance/model.py +++ /dev/null @@ -1,60 +0,0 @@ -from sqlalchemy import Column, String, Numeric, DateTime, Boolean, ForeignKey, Integer -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine -from sqlalchemy.orm import declarative_base, relationship, sessionmaker -from datetime import datetime -import uuid - -Base = declarative_base() - -def generate_uuid(): - return str(uuid.uuid4()) - -"""Пользователи""" -class User(Base): - __tablename__ = 'users' - - id = Column(String, primary_key=True, default=generate_uuid) - telegram_id = Column(Integer, unique=True, nullable=False) - username = Column(String) - balance = Column(Numeric(10, 2), default=0.0) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - subscriptions = relationship("Subscription", back_populates="user") - transactions = relationship("Transaction", back_populates="user") - admins = relationship("Administrators", back_populates="user") - -"""Подписки""" -class Subscription(Base): - __tablename__ = 'subscriptions' - - id = Column(String, primary_key=True, default=generate_uuid) - user_id = Column(String, ForeignKey('users.id')) - vpn_server_id = Column(String) - plan = Column(String) - expiry_date = Column(DateTime) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - user = relationship("User", back_populates="subscriptions") - -"""Транзакции""" -class Transaction(Base): - __tablename__ = 'transactions' - - id = Column(String, primary_key=True, default=generate_uuid) - user_id = Column(String, ForeignKey('users.id')) - amount = Column(Numeric(10, 2)) - transaction_type = Column(String) - created_at = Column(DateTime, default=datetime.utcnow) - - user = relationship("User", back_populates="transactions") - -"""Администраторы""" -class Administrators(Base): - __tablename__ = 'admins' - - id = Column(String, primary_key=True, default=generate_uuid) - user_id = Column(String, ForeignKey('users.id')) - - user = relationship("User", back_populates="admins") diff --git a/main.py b/main.py deleted file mode 100644 index b73e919..0000000 --- a/main.py +++ /dev/null @@ -1,42 +0,0 @@ -from fastapi import FastAPI -from instance.configdb import init_postgresql, init_mongodb, close_connections -from app.routes import user_router, payment_router, subscription_router -from app.services.db_manager import DatabaseManager -from instance.configdb import get_postgres_session - -# Создаём приложение FastAPI -app = FastAPI() - -# Инициализация менеджера базы данных -database_manager = DatabaseManager(session_generator=get_postgres_session) - -# Событие при старте приложения -@app.on_event("startup") -async def startup(): - """ - Инициализация подключения к базам данных. - """ - await init_postgresql() - await init_mongodb() - -# Событие при завершении работы приложения -@app.on_event("shutdown") -async def shutdown(): - """ - Закрытие соединений с базами данных. - """ - await close_connections() - -# Подключение маршрутов -app.include_router(user_router, prefix="/api") -app.include_router(payment_router, prefix="/api") -app.include_router(subscription_router, prefix="/api") - -# Пример корневого маршрута -@app.get("/") -async def root(): - """ - Пример маршрута, использующего DatabaseManager. - """ - user = await database_manager.create_user(telegram_id=12345) - return {"message": "User created", "user": {"id": user.id, "telegram_id": user.telegram_id}} diff --git a/requirements.txt b/requirements.txt index 16588e6..224c87a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,31 +1,40 @@ -aiohappyeyeballs==2.4.4 -aiohttp==3.11.11 -aiosignal==1.3.2 +aiofiles==24.1.0 +aiogram==3.15.0 +aiohappyeyeballs==2.4.3 +aiohttp==3.10.11 +aiosignal==1.3.1 annotated-types==0.7.0 -anyio==4.7.0 -attrs==24.3.0 -blinker==1.9.0 -bson==0.5.10 -click==8.1.7 +anyio==4.6.0 +asyncpg==0.30.0 +attrs==24.2.0 +certifi==2024.8.30 +charset-normalizer==3.4.0 +DateTime==5.5 dnspython==2.7.0 -fastapi==0.115.6 frozenlist==1.5.0 greenlet==3.1.1 +h11==0.14.0 +httpcore==1.0.6 +httpx==0.27.2 idna==3.10 -itsdangerous==2.2.0 -Jinja2==3.1.4 -MarkupSafe==3.0.2 +magic-filter==1.0.12 motor==3.6.0 multidict==6.1.0 -propcache==0.2.1 -pydantic==2.10.4 -pydantic_core==2.27.2 +propcache==0.2.0 +psycopg2-binary==2.9.10 +pydantic==2.9.2 +pydantic_core==2.23.4 pymongo==4.9.2 python-dateutil==2.9.0.post0 -six==1.17.0 +python-telegram-bot==21.6 +pytz==2024.2 +requests==2.32.3 +setuptools==75.1.0 +six==1.16.0 sniffio==1.3.1 -SQLAlchemy==2.0.36 -starlette==0.41.3 +SQLAlchemy==2.0.35 +telegram==0.0.1 typing_extensions==4.12.2 -Werkzeug==3.1.3 -yarl==1.18.3 +urllib3==2.2.3 +yarl==1.17.2 +zope.interface==7.1.0 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..e69de29