Пиздец очередной

This commit is contained in:
Disledg
2024-12-21 18:25:52 +03:00
parent f5d2051360
commit bfe898f10a
25 changed files with 1065 additions and 574 deletions

3
.gitignore vendored
View File

@@ -1,2 +1 @@
TBot/
logs/*
*.code-workspace

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.autoImportCompletions": true
}

View File

@@ -1,36 +1,35 @@
# VPN Configuration Sales Bot (FastAPI Backend)
# VPN Configuration Sales Bot
Бот для Telegram, предназначенный для продажи VPN конфигураций. Проект создан с целью автоматизации процесса продажи VPN и управления пользователями через удобный интерфейс Telegram, с использованием баз данных PostgreSQL и MongoDB.
Проект представляет собой FastAPI сервер, интегрированный с Telegram API для автоматизации продажи VPN конфигураций и управления пользователями. Бекенд обрабатывает запросы, взаимодействует с базами данных PostgreSQL и MongoDB, и обеспечивает удобный интерфейс для пользователей и администраторов.
## 📋 Описание
Этот проект представляет собой Telegram-бота, который позволяет пользователям приобретать VPN настройки, а администраторам управлять конфигурациями и отслеживать заказы. Бот поддерживает работу с двумя базами данных для обеспечения гибкого хранения и обработки данных.
FastAPI сервер предоставляет REST API для управления функционалом Telegram-бота, включая регистрацию, авторизацию, покупку VPN конфигураций и администрирование. Использование FastAPI обеспечивает высокую производительность, читаемый код и расширяемость проекта.
## 🛠 Функционал
- Регистрация и авторизация пользователей
- Покупка VPN конфигураций
- Отсутствует сука
### В стадии разработки
- Автоматическая выдача VPN конфигураций
- Поддержка двух баз данных (PostgreSQL и MongoDB) для более гибкого и масштабируемого хранения данных
- Панель администратора для управления заказами и пользователями
- Саппорт система
- Всё в разработке
## 🚀 Технологии
- **Python**
- **Python 3.x**
- **FastAPI**
- **Telegram API**
- **PostgreSQL**
- **MongoDB**
- **SQLAlchemy** - для взаимодействия с PostgreSQL
- **PyMongo** - для работы с MongoDB
- **PostgreSQL (SQLAlchemy)**
- **MongoDB (PyMongo)**
- **JWT (JSON Web Tokens):** для аутентификации и авторизации.
- **Pydantic:** для валидации данных.
- **Alembic:** для миграций базы данных PostgreSQL.
- **Docker:** для контейнеризации и упрощения развёртывания.
## 📜 Основные API эндпоинты
Сосут эендпоинты блять, не сделаны ещё
## 📝 Лицензия
Этот проект распространяется под лицензией MIT License. Подробности в файле [LICENSE](./LICENSE).

View File

@@ -1,78 +0,0 @@
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)

View File

@@ -1,137 +0,0 @@
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)

View File

@@ -1,118 +0,0 @@
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/<plan_id>', 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/<server_name>', 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/<server_id>', 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/<server_id>', 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))

View File

@@ -1,191 +0,0 @@
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/<int:telegram_id>', 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)

View File

@@ -0,0 +1,6 @@
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"]

View File

@@ -0,0 +1,72 @@
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))

View File

@@ -0,0 +1,68 @@
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))

211
app/services/db_manager.py Normal file
View File

@@ -0,0 +1,211 @@
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))

158
app/services/mongo_rep.py Normal file
View File

@@ -0,0 +1,158 @@
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("Контекстный менеджер: подключение закрыто.")

View File

@@ -0,0 +1,102 @@
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

232
app/services/xui_rep.py Normal file
View File

@@ -0,0 +1,232 @@
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

View File

72
instance/configdb.py Normal file
View File

@@ -0,0 +1,72 @@
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)

60
instance/model.py Normal file
View File

@@ -0,0 +1,60 @@
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")

42
main.py Normal file
View File

@@ -0,0 +1,42 @@
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}}

View File

@@ -1,40 +1,31 @@
aiofiles==24.1.0
aiogram==3.15.0
aiohappyeyeballs==2.4.3
aiohttp==3.10.11
aiosignal==1.3.1
aiohappyeyeballs==2.4.4
aiohttp==3.11.11
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.6.0
asyncpg==0.30.0
attrs==24.2.0
certifi==2024.8.30
charset-normalizer==3.4.0
DateTime==5.5
anyio==4.7.0
attrs==24.3.0
blinker==1.9.0
bson==0.5.10
click==8.1.7
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
magic-filter==1.0.12
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.2
motor==3.6.0
multidict==6.1.0
propcache==0.2.0
psycopg2-binary==2.9.10
pydantic==2.9.2
pydantic_core==2.23.4
propcache==0.2.1
pydantic==2.10.4
pydantic_core==2.27.2
pymongo==4.9.2
python-dateutil==2.9.0.post0
python-telegram-bot==21.6
pytz==2024.2
requests==2.32.3
setuptools==75.1.0
six==1.16.0
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.35
telegram==0.0.1
SQLAlchemy==2.0.36
starlette==0.41.3
typing_extensions==4.12.2
urllib3==2.2.3
yarl==1.17.2
zope.interface==7.1.0
Werkzeug==3.1.3
yarl==1.18.3

0
run.py
View File