Compare commits

...

10 Commits

26 changed files with 1575 additions and 356 deletions

0
.env
View File

View File

@@ -1,14 +1,22 @@
# Используем базовый Python-образ
FROM python:3.12-slim
FROM python:3.10-slim
# Устанавливаем рабочую директорию
# Установка зависимостей системы
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Рабочая директория
WORKDIR /app
# Копируем файлы проекта
COPY . .
# Устанавливаем зависимости
# Копируем requirements.txt и устанавливаем зависимости
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Указываем команду запуска бота
CMD ["python", "main.py"]
# Копируем весь код приложения в контейнер
COPY . .
# Открываем порт для приложения
EXPOSE 8000
# Команда для запуска приложения
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,6 +1,6 @@
from .payment_routes import router as payment_router
from .user_routes import router as user_router
#from .payment_routes import router as payment_router
from .user_routes import router
from .subscription_routes import router as subscription_router
from .support_routes import router as sup_router
# Экспорт всех маршрутов
__all__ = ["payment_router", "user_router", "subscription_router"]
__all__ = [ "router", "subscription_router","sup_router"]

View File

@@ -0,0 +1,65 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from yookassa import Payment
from yookassa.domain.notification import WebhookNotification
from app.services.db_manager import DatabaseManager
router = APIRouter()
class CreatePaymentRequest(BaseModel):
telegram_id: str
amount: float
class PaymentResponse(BaseModel):
payment_url: str
payment_id: str
@router.post("/payment/create", response_model=PaymentResponse)
async def create_payment(request: CreatePaymentRequest):
"""
Создаёт платёж через ЮKassa и возвращает ссылку для оплаты.
"""
try:
# Создание платежа через API ЮKassa
payment = Payment.create({
"amount": {
"value": f"{request.amount:.2f}",
"currency": "RUB"
},
"confirmation": {
"type": "redirect", # Тип подтверждения (redirect или embedded)
"return_url": "https://your-app.com/success" # URL возврата
},
"description": f"Пополнение баланса для пользователя {request.telegram_id}"
})
# Возвращаем ссылку для оплаты и ID платежа
return PaymentResponse(
payment_url=payment.confirmation.confirmation_url,
payment_id=payment.id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка создания платежа: {str(e)}")
@router.post("/payment/notification")
async def payment_notification(notification: dict, database_manager: DatabaseManager):
"""
Обрабатывает уведомления от ЮKassa.
"""
try:
# Парсим уведомление
webhook = WebhookNotification(notification)
payment_object = webhook.object
# Проверяем статус платежа
if payment_object["status"] == "succeeded":
# Обновляем баланс пользователя
telegram_id = int(payment_object["description"].split()[-1])
amount = float(payment_object["amount"]["value"])
await database_manager.update_balance(telegram_id, amount)
return {"status": "ok"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка обработки уведомления: {str(e)}")

View File

@@ -1,17 +1,20 @@
from typing import List
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from app.services.db_manager import DatabaseManager
from instance.configdb import get_database_manager
from uuid import UUID
import logging
from sqlalchemy.exc import SQLAlchemyError
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
router = APIRouter()
# DatabaseManager должен передаваться через Depends
def get_database_manager():
# Здесь должна быть логика инициализации DatabaseManager
return DatabaseManager()
# Схемы запросов и ответов
class BuySubscriptionRequest(BaseModel):
telegram_id: int
telegram_id: str
plan_id: str
class SubscriptionResponse(BaseModel):
@@ -35,29 +38,72 @@ async def buy_subscription(
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")
raise HTTPException(status_code=500, detail="ERROR")
elif result == "INSUFFICIENT_FUNDS":
raise HTTPException(status_code=400, detail="Insufficient funds")
return {"message": "Subscription purchased successfully"}
raise HTTPException(status_code=400, detail="INSUFFICIENT_FUNDS")
elif result == "TARIFF_NOT_FOUND":
raise HTTPException(status_code=400, detail="TARIFF_NOT_FOUND")
elif result == "ACTIVE_SUBSCRIPTION_EXISTS":
raise HTTPException(status_code=400, detail="ACTIVE_SUBSCRIPTION_EXISTS")
result = await database_manager.generate_uri(request_data.telegram_id)
return {"message": result}
except HTTPException as http_exc:
# Пропускаем HTTPException, чтобы FastAPI обработал его корректно
raise http_exc
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# Обрабатываем остальные исключения
raise HTTPException(status_code=500, detail=f"Unexpected error: {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)
):
@router.get("/subscription/{user_id}/last", response_model=SubscriptionResponse)
async def last_subscription(user_id: UUID, database_manager: DatabaseManager = Depends(get_database_manager)):
"""
Получение последней подписки пользователя.
Возвращает последнюю подписку пользователя.
"""
logger.info(f"Получение последней подписки для пользователя: {user_id}")
try:
subscriptions = await database_manager.last_subscription(user_id)
if subscriptions == "ERROR":
raise HTTPException(status_code=500, detail="Failed to fetch subscriptions")
subscriptions = await database_manager.get_last_subscriptions(user_id=user_id, limit=1)
if not subscriptions:
logger.warning(f"Подписки для пользователя {user_id} не найдены")
raise HTTPException(status_code=404, detail="No subscriptions found")
sub = subscriptions[0]
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(),
}
except SQLAlchemyError as e:
logger.error(f"Ошибка базы данных при получении подписки для пользователя {user_id}: {e}")
raise HTTPException(status_code=500, detail="Database error")
except HTTPException as e:
# Пропускаем HTTPException, чтобы FastAPI обработал её автоматически
raise e
except Exception as e:
logger.error(f"Неожиданная ошибка: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
@router.get("/subscriptions/{user_id}", response_model=List[SubscriptionResponse])
async def get_subscriptions(user_id: UUID, database_manager: DatabaseManager = Depends(get_database_manager)):
"""
Возвращает список подписок пользователя.
"""
logger.info(f"Получение подписок для пользователя: {user_id}")
try:
# Получаем подписки без ограничений или с указанным лимитом
subscriptions = await database_manager.last_subscriptions(user_id=str(user_id))
if not subscriptions:
logger.warning(f"Подписки для пользователя {user_id} не найдены")
raise HTTPException(status_code=404, detail="No subscriptions found")
# Формируем список подписок для ответа
return [
{
"id": sub.id,
@@ -66,7 +112,44 @@ async def last_subscription(
"expiry_date": sub.expiry_date.isoformat(),
"created_at": sub.created_at.isoformat(),
"updated_at": sub.updated_at.isoformat(),
} for sub in subscriptions
}
for sub in subscriptions
]
except SQLAlchemyError as e:
logger.error(f"Ошибка базы данных при получении подписок для пользователя {user_id}: {e}")
raise HTTPException(status_code=500, detail="Database error")
except Exception as e:
logger.error(f"Неожиданная ошибка: {e}")
raise HTTPException(status_code=500, detail=str(e))
except HTTPException as e:
# Пропускаем HTTPException, чтобы FastAPI обработал её автоматически
raise e
@router.get("/uri", response_model=dict)
async def get_uri(telegram_id: str, database_manager: DatabaseManager = Depends(get_database_manager)):
"""
Возвращает список подписок пользователя.
"""
logger.info(f"Получение подписок для пользователя: {telegram_id}")
try:
# Получаем подписки без ограничений или с указанным лимитом
uri = await database_manager.generate_uri(telegram_id)
if uri == "SUB_ERROR":
raise HTTPException(status_code=404, detail="SUB_ERROR")
if not uri:
logger.warning(f"Не удалось сгенерировать URI для пользователя с telegram_id {telegram_id}")
raise HTTPException(status_code=404, detail="URI not found")
return {"detail": uri}
except HTTPException as e:
# Пропускаем HTTPException, чтобы FastAPI обработал её автоматически
raise e
except SQLAlchemyError as e:
logger.error(f"Ошибка базы данных при получении подписок для пользователя {telegram_id}: {e}")
raise HTTPException(status_code=500, detail="Database error")
except Exception as e:
logger.error(f"Неожиданная ошибка: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,249 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.exc import SQLAlchemyError
from app.services.db_manager import DatabaseManager
from instance.configdb import get_database_manager
from uuid import UUID
from pydantic import BaseModel
from fastapi import Response, status
from enum import Enum
from typing import List, Literal, Optional
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class UpdateTicketStatusRequest(BaseModel):
new_status: str
class TicketStatus(str, Enum):
OPEN = "open"
PENDING = "pending"
CLOSED = "closed"
class CreateTicketRequest(BaseModel):
subject: str
message: str
class TicketResponse(BaseModel):
id: int
user_id: UUID
subject: str
message: str
status: TicketStatus
created_at: datetime
updated_at: datetime
class TicketMessageRequest(BaseModel):
message: str
class TicketMessageResponse(BaseModel):
ticket_id: int
sender: str
message: str
created_at: datetime
router = APIRouter()
def handle_exception(e: Exception, message: str):
logger.error(f"{message}: {e}")
raise HTTPException(status_code=500, detail=f"{message}: {str(e)}")
@router.post("/support/tickets/{ticket_id}/messages", summary="Добавить сообщение")
async def add_message(
ticket_id: int,
request: TicketMessageRequest,
sender: Literal["user", "support"], # "user" или "support"
database_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Добавляет сообщение в тикет.
Args:
ticket_id (int): ID тикета.
request (TicketMessageRequest): Данные сообщения.
sender (str): Отправитель ("user" или "support").
database_manager (DatabaseManager): Управление базой данных.
Returns:
TicketMessageResponse: Данные созданного сообщения.
"""
try:
message = await database_manager.add_message_to_ticket(ticket_id=ticket_id, sender=sender, message=request.message)
if not message:
raise HTTPException(status_code=404, detail="Тикет не найден или ошибка добавления сообщения")
if message != "OK":
raise HTTPException(status_code=500, detail="Ошибка добавления сообщения")
return Response(status_code=status.HTTP_200_OK)
except Exception as e:
handle_exception(e,"Ошибка добавления сообщения")
@router.get("/support/tickets/{ticket_id}/messages", response_model=List[TicketMessageResponse], summary="Получить сообщения")
async def get_messages(
ticket_id: int,
database_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Возвращает список сообщений в тикете.
Args:
ticket_id (int): ID тикета, для которого нужно получить сообщения.
database_manager (DatabaseManager): Менеджер базы данных.
Returns:
List[TicketMessageResponse]: Список сообщений, связанных с тикетом.
Raises:
HTTPException: 404, если сообщения для тикета не найдены.
HTTPException: 500, если произошла ошибка на сервере.
"""
try:
messages = await database_manager.get_ticket_messages(ticket_id=ticket_id)
ticket_info = await database_manager.get_ticket(ticket_id)
logger.info(messages)
result_messages = []
if messages:
for message in messages:
correct_response = TicketMessageResponse(
ticket_id=ticket_id,
sender=message.sender,
message=message.message,
created_at=message.created_at
)
result_messages.append(correct_response)
result_messages.insert(0,
TicketMessageResponse(
ticket_id=ticket_id,
sender="user",
message=ticket_info['message'],
created_at=ticket_info["created_at"]
)
)
return result_messages
except Exception as e:
handle_exception(e,"Ошибка получения сообщения")
@router.get("/support/ticket/{ticket_id}", response_model=TicketResponse, summary="Получить информацию о тикете")
async def get_ticket(
ticket_id: int,
database_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Возвращает информацию о тикете.
Args:
ticket_id (int): ID тикета, информацию котрого хочет получить пользователь.
database_manager (DatabaseManager): Менеджер базы данных.
Returns:
TicketResponse: Информация о тикете.
Raises:
HTTPException: 404, если тикет не найден.
HTTPException: 500, если произошла ошибка на сервере.
"""
try:
# Получаем данные о тикете
ticket = await database_manager.get_ticket(ticket_id=ticket_id)
if not ticket:
raise HTTPException(status_code=404, detail="Тикет не найден")
# Возвращаем данные через Pydantic-модель
return TicketResponse(**ticket)
except SQLAlchemyError as e:
handle_exception(e, "Ошибка получения тикета")
except Exception as e:
handle_exception(e, "Неизвестная ошибка при получении тикета")
@router.post("/support/tickets", response_model=TicketResponse, summary="Создать тикет")
async def create_ticket(
request: CreateTicketRequest,
user_id: UUID,
database_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Создаёт новый тикет для пользователя.
Args:
request (CreateTicketRequest): Данные для создания тикета (тема и сообщение).
user_id (UUID): Идентификатор пользователя, создающего тикет.
database_manager (DatabaseManager): Менеджер базы данных.
Returns:
TicketResponse: Данные созданного тикета.
Raises:
HTTPException: 500, если произошла ошибка при создании тикета.
"""
try:
ticket = await database_manager.create_ticket(
user_id=user_id,
subject=request.subject,
message=request.message
)
return ticket
except Exception as e:
handle_exception(e,"Ошибка содания тикета")
@router.get("/support/tickets", response_model=List[TicketResponse], summary="Получить список тикетов")
async def list_tickets(
user_id: UUID,
database_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Возвращает список тикетов пользователя.
Args:
user_id (UUID): Идентификатор пользователя, чьи тикеты нужно получить.
database_manager (DatabaseManager): Менеджер базы данных.
Returns:
List[TicketResponse]: Список тикетов пользователя.
Raises:
HTTPException: 404, если тикеты не найдены.
HTTPException: 500, если произошла ошибка на сервере.
"""
try:
tickets = await database_manager.get_active_tickets(user_id=user_id)
if not tickets:
raise HTTPException(status_code=404, detail="Тикеты не найдены")
return tickets
except Exception as e:
handle_exception(e,"Ошибка получения тикетов")
@router.patch("/support/ticket/{ticket_id}/status", summary="Обновить статус тикета")
async def update_ticket_status(
ticket_id: int,
request: UpdateTicketStatusRequest,
database_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Обновляет статус тикета.
Args:
ticket_id (int): ID тикета.
request (UpdateTicketStatusRequest): Запрос с новым статусом.
database_manager (DatabaseManager): Менеджер базы данных.
Returns:
dict: Подтверждение обновления статуса.
Raises:
HTTPException: Если тикет не найден или произошла ошибка.
"""
try:
result = await database_manager.update_ticket_status(ticket_id, request.new_status)
if result != "OK":
return "ERROR"
return "OK"
except ValueError as e:
logger.error(f"Тикет с ID {ticket_id} не найден: {e}")
raise HTTPException(status_code=404, detail="Тикет не найден.")
except Exception as e:
logger.error(f"Ошибка обновления статуса тикета {ticket_id}: {e}")
raise HTTPException(status_code=500, detail="Не удалось обновить статус тикета.")

View File

@@ -1,22 +1,39 @@
import sys
from fastapi import APIRouter, Depends, HTTPException
from fastapi.exceptions import HTTPException
from app.services.db_manager import DatabaseManager
from sqlalchemy.exc import SQLAlchemyError
from instance.configdb import get_database_manager
from pydantic import BaseModel
from typing import Optional
from uuid import UUID
import logging
logger = logging.getLogger(__name__)
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False
router = APIRouter()
# Модели запросов и ответов
class CreateUserRequest(BaseModel):
telegram_id: int
telegram_id: str
referrer_id: Optional[str] = None
class UserResponse(BaseModel):
id: str
telegram_id: int
username: str
id: UUID
telegram_id: str
username: Optional[str]
balance: float
referrer_id: Optional[str]
created_at: str
updated_at: str
class AddReferal(BaseModel):
new_user_id: str
@router.post("/user/create", response_model=UserResponse, summary="Создать пользователя")
async def create_user(
@@ -27,7 +44,7 @@ async def create_user(
Создание пользователя через Telegram ID.
"""
try:
user = await db_manager.create_user(request.telegram_id)
user = await db_manager.create_user(request.telegram_id,request.referrer_id)
if user == "ERROR":
raise HTTPException(status_code=500, detail="Failed to create user")
@@ -36,6 +53,7 @@ async def create_user(
telegram_id=user.telegram_id,
username=user.username,
balance=user.balance,
referrer_id=user.referrer_id if user.referrer_id is not None else None,
created_at=user.created_at.isoformat(),
updated_at=user.updated_at.isoformat()
)
@@ -43,26 +61,144 @@ async def create_user(
raise HTTPException(status_code=500, detail=str(e))
@router.get("/user/{telegram_id}", response_model=UserResponse, summary="Получить информацию о пользователе")
async def get_user(
telegram_id: int,
telegram_id: str,
db_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Получение информации о пользователе.
"""
try:
print(f"Получение пользователя с telegram_id: {telegram_id}")
user = await db_manager.get_user_by_telegram_id(telegram_id)
if not user:
logger.warning(f"Пользователь с telegram_id {telegram_id} не найден.")
raise HTTPException(status_code=404, detail="User not found")
return UserResponse(
print(f"Пользователь найден: ID={user.id}, Username={user.username}")
user_response = UserResponse(
id=user.id,
telegram_id=user.telegram_id,
username=user.username,
balance=user.balance,
referrer_id=user.referrer_id if user.referrer_id is not None else None,
created_at=user.created_at.isoformat(),
updated_at=user.updated_at.isoformat()
)
return user_response
except HTTPException as http_ex: # Позволяет обработать HTTPException отдельно
raise http_ex
except SQLAlchemyError as e:
logger.error(f"Ошибка базы данных при получении пользователя с telegram_id {telegram_id}: {e}")
raise HTTPException(status_code=500, detail="Database error")
except Exception as e:
logger.exception(f"Неожиданная ошибка при получении пользователя с telegram_id {telegram_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/user/{telegram_id}/balance/{amount}", summary="Обновить баланс")
async def update_balance(
telegram_id: str,
amount: float,
db_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Обновляет баланс пользователя.
"""
logger.info(f"Получен запрос на обновление баланса: telegram_id={telegram_id}, amount={amount}")
try:
result = await db_manager.update_balance(telegram_id, amount)
if result == "ERROR":
logger.error(f"Ошибка обновления баланса для пользователя {telegram_id}")
raise HTTPException(status_code=500, detail="Failed to update balance")
logger.info(f"Баланс пользователя {telegram_id} успешно обновлен на {amount}")
return {"message": "Balance updated successfully"}
except HTTPException as http_ex:
logger.warning(f"HTTP ошибка: {http_ex.detail}")
raise http_ex
except SQLAlchemyError as db_ex:
logger.error(f"Ошибка базы данных при обновлении баланса пользователя {telegram_id}: {db_ex}")
raise HTTPException(status_code=500, detail="Database error")
except Exception as e:
logger.exception(f"Неожиданная ошибка при обновлении баланса пользователя {telegram_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/user/{user_id}/transactions", summary="Последние транзакции пользователя")
async def last_transactions(
user_id: UUID,
db_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Возвращает список последних транзакций пользователя.
"""
logger.info(f"Получен запрос на транзакции для пользователя: {user_id}")
try:
logger.debug(f"Вызов метода get_transaction с user_id={user_id}")
transactions = await db_manager.get_transaction(user_id)
if transactions == "ERROR":
logger.error(f"Ошибка при получении транзакций для пользователя: {user_id}")
raise HTTPException(status_code=500, detail="Failed to fetch transactions")
logger.debug(f"Транзакции для {user_id}: {transactions}")
response = [
{
"id": tx.id,
"amount": tx.amount,
"created_at": tx.created_at.isoformat(),
"transaction_type": tx.transaction_type,
} for tx in transactions
]
logger.info(f"Формирование ответа для пользователя {user_id}: {response}")
return response
except HTTPException as http_ex:
logger.warning(f"HTTP ошибка для {user_id}: {http_ex.detail}")
raise http_ex
except SQLAlchemyError as db_ex:
logger.error(f"Ошибка базы данных для {user_id}: {db_ex}")
raise HTTPException(status_code=500, detail="Database error")
except Exception as e:
logger.exception(f"Неожиданная ошибка для {user_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/user/{referrer_id}/add_referral", summary="Обновить баланс")
async def add_referal(
referrer_id: str,
request: AddReferal,
db_manager: DatabaseManager = Depends(get_database_manager)
):
"""
Обновляет баланс пользователя.
"""
logger.info(f"Получен запрос на добавление реферала: telegram_id={referrer_id}")
try:
result = await db_manager.add_referal(referrer_id)
if result == "ERROR":
logger.error(f"Ошибка добавления реферала для {referrer_id} c айди {request.new_user_id}")
raise HTTPException(status_code=500, detail="Failed to update balance")
logger.info(f"Добавлен реферал для {referrer_id} c айди {request.new_user_id}")
return {"message": "Balance updated successfully"}
except HTTPException as http_ex:
logger.warning(f"HTTP ошибка: {http_ex.detail}")
raise http_ex
except SQLAlchemyError as db_ex:
logger.error(f"Ошибка базы данных при добавлении рефералу {referrer_id}: {db_ex}")
raise HTTPException(status_code=500, detail="Database error")
except Exception as e:
logger.exception(f"Неожиданная ошибка при добавлении рефералу {referrer_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -1,211 +1,311 @@
from instance.model import User, Subscription, Transaction, Administrators
from sqlalchemy.future import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import desc
from decimal import Decimal
import json
from instance.model import User, Subscription, Transaction, SupportTicket, TicketMessage, TicketStatus
from .xui_rep import PanelInteraction
from .postgres_rep import PostgresRepository
from .mongo_rep import MongoDBRepository
from instance.model import Transaction
from dateutil.relativedelta import relativedelta
from datetime import datetime
from .xui_rep import PanelInteraction
from .mongo_rep import MongoDBRepository
import random
import string
from typing import Optional
import logging
import asyncio
from uuid import UUID
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):
self.postgres_repo = PostgresRepository(session_generator, self.logger)
async def get_active_tickets(self, user_id: UUID):
"""
Создаёт нового пользователя, если его нет.
Получает активные подписки пользователя
"""
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"
return await self.postgres_repo.list_active_tickets(user_id)
async def create_user(self, telegram_id: str, referrer_id: Optional[str]= None):
"""
Создаёт пользователя.
"""
try:
username = self.generate_string(6)
return await self.postgres_repo.create_user(telegram_id, username, referrer_id)
except Exception as e:
self.logger.error(f"Ошибка при создании пользователя:{e}")
async def get_user_by_telegram_id(self, telegram_id: int):
async def get_user_by_telegram_id(self, telegram_id: str):
"""
Возвращает пользователя по 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
return await self.postgres_repo.get_user_by_telegram_id(telegram_id)
async def add_transaction(self, user_id: int, amount: float):
async def add_transaction(self, user_id: UUID, 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()
tran = Transaction(
user_id=user_id,
amount=Decimal(amount),
transaction_type="default"
)
return await self.postgres_repo.add_record(tran)
async def add_referal(self,referrer_id: str, new_user_id: str):
"""
Добавление рефералу пользователей
"""
return await self.postgres_repo.add_referal(referrer_id,new_user_id)
async def get_transaction(self, user_id: UUID, limit: int = 10):
"""
Возвращает транзакции.
"""
return await self.postgres_repo.get_last_transactions(user_id, limit)
async def update_balance(self, telegram_id: int, amount: float):
async def update_balance(self, telegram_id: str, 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"
self.logger.info(f"Попытка обновления баланса: telegram_id={telegram_id}, amount={amount}")
user = await self.get_user_by_telegram_id(telegram_id)
if not user:
self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.")
return "ERROR"
updated = await self.postgres_repo.update_balance(user, amount)
if not updated:
self.logger.error(f"Не удалось обновить баланс пользователя {telegram_id}")
return "ERROR"
self.logger.info(f"Баланс пользователя {telegram_id} обновлен на {amount}, добавление транзакции")
await self.add_transaction(user.id, amount)
return "OK"
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 def get_active_subscription(self, telegram_id: str):
"""
Возвращает список транзакций пользователя.
Проверяет наличие активной подписки.
"""
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"
return await self.postgres_repo.get_active_subscription(telegram_id)
async def get_last_subscriptions(self, user_id: UUID, limit: int ):
"""
Возвращает список последних подписок.
"""
return await self.postgres_repo.get_last_subscription_by_user_id(user_id, limit)
async def buy_sub(self, telegram_id: str, plan_id: str):
async for session in self.session_generator():
"""
Покупает подписку.
"""
active_subscription = await self.get_active_subscription(telegram_id)
self.logger.info(f"{active_subscription}")
if active_subscription:
self.logger.error(f"Пользователь {telegram_id} уже имеет активную подписку.")
return "ACTIVE_SUBSCRIPTION_EXISTS"
result = await self._initialize_user_and_plan(telegram_id, plan_id)
if isinstance(result, str):
return result # Возвращает "ERROR", "TARIFF_NOT_FOUND" или "INSUFFICIENT_FUNDS"
user, plan = result
await self.postgres_repo.update_balance(user,-plan['price'])
new_subscription, server = await self._create_subscription_and_add_client(user, plan)
if not new_subscription:
return "ERROR"
self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id}.")
return "OK"
async def _initialize_user_and_plan(self, telegram_id, plan_id):
"""
Инициализирует пользователя и план подписки.
"""
user = await self.get_user_by_telegram_id(telegram_id)
if not user:
self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.")
return "ERROR"
plan = await self.mongo_repo.get_subscription_plan(plan_id)
if not plan:
self.logger.error(f"Тарифный план {plan_id} не найден.")
return "TARIFF_NOT_FOUND"
cost = int(plan["price"])
if user.balance < cost:
self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.")
return "INSUFFICIENT_FUNDS"
return user, plan
async def _create_subscription_and_add_client(self, user, plan):
"""
Создаёт подписку и добавляет клиента на сервер.
"""
expiry_date = datetime.utcnow() + relativedelta(months=plan["duration_months"])
server = await self.mongo_repo.get_server_with_least_clients()
if not server:
self.logger.error("Нет доступных серверов для подписки.")
return None, None
new_subscription = Subscription(
user_id=user.id,
vpn_server_id=str(server["server"]["name"]),
plan=plan["name"],
expiry_date=expiry_date,
)
panel = PanelInteraction(
base_url=f"https://{server['server']['ip']}:{server['server']['port']}/{server['server']['secretKey']}",
login_data={"username": server["server"]["login"], "password": server["server"]["password"]},
logger=self.logger,
certificate=server["server"]["certificate"]["data"],
)
response = await panel.add_client(
inbound_id=1,
expiry_date=expiry_date.isoformat(),
email=user.username,
)
if response != "OK":
self.logger.error(f"Ошибка при добавлении клиента: {response}")
return None, None
await self.postgres_repo.add_record(new_subscription)
return new_subscription, server
async def generate_uri(self, telegram_id: str):
"""
Генерация URI для пользователя.
:param telegram_id: Telegram ID пользователя.
:return: Строка URI или None в случае ошибки.
"""
try:
# Извлечение данных
subscription = await self.postgres_repo.get_active_subscription(telegram_id)
if not subscription:
self.logger.error(f"Подписки для пользователя {telegram_id} не найдены.")
return "SUB_ERROR"
server = await self.mongo_repo.get_server(subscription.vpn_server_id)
if not server:
self.logger.error(f"Сервер с ID {subscription.vpn_server_id} не найден в MongoDB.")
return None
user = await self.postgres_repo.get_user_by_telegram_id(telegram_id)
if not user:
self.logger.error(f"Пользователь с telegram_id {telegram_id} не найден.")
return None
email = user.username # Используем email из данных пользователя
panel = PanelInteraction(
base_url=f"https://{server['server']['ip']}:{server['server']['port']}/{server['server']['secretKey']}",
login_data={"username": server["server"]["login"], "password": server["server"]["password"]},
logger=self.logger,
certificate=server["server"]["certificate"]["data"],
)
inbound_info = await panel.get_inbound_info(inbound_id=1) # Используем фиксированный ID
if not inbound_info:
self.logger.error(f"Не удалось получить информацию об инбаунде для ID {subscription.vpn_server_id}.")
return None
# Логируем полученные данные
self.logger.info(f"Inbound Info: {inbound_info}")
# Разбор JSON-строк
try:
result = await self.create_user(telegram_id)
if not result:
self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.")
return "ERROR"
stream_settings = json.loads(inbound_info["obj"]["streamSettings"])
except KeyError as e:
self.logger.error(f"Ключ 'streamSettings' отсутствует: {e}")
return None
except json.JSONDecodeError as e:
self.logger.error(f"Ошибка разбора JSON для 'streamSettings': {e}")
return None
# Получение тарифного плана из MongoDB
plan = await self.mongo_repo.get_subscription_plan(plan_id)
if not plan:
self.logger.error(f"Тарифный план {plan_id} не найден.")
return "ERROR"
settings = json.loads(inbound_info["obj"]["settings"]) # Разбираем JSON
# Проверка достаточности средств
cost = int(plan["price"])
if result.balance < cost:
self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.")
return "INSUFFICIENT_FUNDS"
# Находим клиента по email
client = next((c for c in settings["clients"] if c["email"] == email), None)
if not client:
self.logger.error(f"Клиент с email {email} не найден среди клиентов.")
return None
# Списываем средства
result.balance -= cost
server_info = server["server"]
# Создаем подписку
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)
# Преобразование данных в формат URI
uri = (
f"vless://{client['id']}@{server_info['ip']}:443?"
f"type={stream_settings['network']}&security={stream_settings['security']}"
f"&pbk={stream_settings['realitySettings']['settings']['publicKey']}"
f"&fp={stream_settings['realitySettings']['settings']['fingerprint']}"
f"&sni={stream_settings['realitySettings']['serverNames'][0]}"
f"&sid={stream_settings['realitySettings']['shortIds'][0]}"
f"&spx=%2F&flow={client['flow']}"
f"#{inbound_info['obj']['remark']}-{client['email']}"
)
# Попытка добавить пользователя на сервер
# Получаем информацию о пользователе
user = result # так как result уже содержит пользователя
if not user:
self.logger.error(f"Не удалось найти пользователя для добавления на сервер.")
await session.rollback()
return "ERROR"
self.logger.info(f"Сформирован URI для пользователя {telegram_id}: {uri}")
return uri
except Exception as e:
self.logger.error(f"Ошибка при генерации URI для пользователя {telegram_id}: {e}")
return None
# Получаем сервер из 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"
async def get_ticket(self,ticket_id: int):
"""
Ищет тикет по айди
"""
return await self.postgres_repo.get_ticket(ticket_id)
async def create_ticket(self, user_id: UUID, subject: str, message: str):
"""
Создаёт тикет
"""
ticket = SupportTicket(user_id=user_id,subject=subject,message=message)
return await self.postgres_repo.add_record(ticket)
async def add_message_to_ticket(self,ticket_id : int,sender: str,message: str):
"""
Добавляет сообщения к тикету
"""
message = TicketMessage(ticket_id=ticket_id, sender=sender, message=message)
result = await self.postgres_repo.add_record(message)
if result == None:
return "ERROR"
return "OK"
async def get_ticket_messages(self,ticket_id: int):
"""
Получает сообщения тикета
"""
return await self.postgres_repo.get_ticket_messages(ticket_id)
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'],
}
async def update_ticket_status(self, ticket_id: int, new_status: str):
"""
Обновляет статус тикета.
panel = PanelInteraction(url_base, login_data, self.logger,server_info['certificate']['data'])
expiry_date_iso = new_subscription.expiry_date.isoformat()
Args:
ticket_id (int): ID тикета, статус которого нужно обновить.
new_status (str): Новый статус тикета.
# Добавляем на сервер
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"
Returns:
dict: Словарь с ID тикета и обновлённым статусом.
Raises:
ValueError: Если тикет не найден.
"""
return await self.postgres_repo.set_new_status(ticket_id,new_status)
@staticmethod
def generate_string(length):
"""
Генерирует случайную строку заданной длины.
"""
characters = string.ascii_lowercase + string.digits
return ''.join(random.choices(characters, k=length))
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))

View File

@@ -1,7 +1,11 @@
from datetime import datetime
from uuid import UUID
from sqlalchemy.future import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import desc
from instance.model import User, Subscription, Transaction
from decimal import Decimal
from sqlalchemy import asc, desc, update
from sqlalchemy.orm import joinedload
from instance.model import TicketMessage, User, Subscription, Transaction,SupportTicket
class PostgresRepository:
@@ -9,13 +13,13 @@ class PostgresRepository:
self.session_generator = session_generator
self.logger = logger
async def create_user(self, telegram_id: int, username: str):
async def create_user(self, telegram_id: str, username: str, referrer_id: str):
"""
Создаёт нового пользователя в PostgreSQL.
"""
async for session in self.session_generator():
try:
new_user = User(telegram_id=telegram_id, username=username)
new_user = User(telegram_id=telegram_id, username=username, referrer_id=referrer_id)
session.add(new_user)
await session.commit()
return new_user
@@ -23,69 +27,66 @@ class PostgresRepository:
self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}")
await session.rollback()
return None
async def get_active_subscription(self, telegram_id: str):
"""
Проверяет наличие активной подписки у пользователя.
"""
async for session in self.session_generator():
try:
result = await session.execute(
select(Subscription)
.join(User, Subscription.user_id == User.id)
.where(User.telegram_id == telegram_id, Subscription.expiry_date > datetime.utcnow())
)
result= result.scalars().first()
self.logger.info(f"Пользователь с id {telegram_id}, проверен и имеет {result}")
return result
except SQLAlchemyError as e:
self.logger.error(f"Ошибка проверки активной подписки для пользователя {telegram_id}: {e}")
return None
async def get_user_by_telegram_id(self, telegram_id: int):
async def get_user_by_telegram_id(self, telegram_id: str):
"""
Возвращает пользователя по 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()
if result:
return result.scalars().first()
return False
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}")
return None
return False
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 def update_balance(self, user: User, amount: float):
"""
Обновляет баланс пользователя.
:param user: Объект пользователя.
:param amount: Сумма для добавления/вычитания.
:return: True, если успешно, иначе False.
"""
self.logger.info(f"Обновление баланса пользователя: id={user.id}, current_balance={user.balance}, amount={amount}")
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
user = await session.get(User, user.id) # Загружаем пользователя в той же сессии
if not user:
self.logger.warning(f"Пользователь с ID {user.id} не найден.")
return False
# Приведение amount к Decimal
user.balance += Decimal(amount)
await session.commit()
self.logger.info(f"Баланс пользователя id={user.id} успешно обновлен: new_balance={user.balance}")
return True
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при обновлении баланса: {e}")
self.logger.error(f"Ошибка при обновлении баланса пользователя id={user.id}: {e}")
await session.rollback()
return None
return False
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 def get_last_transactions(self, user_id: UUID, limit: int = 10):
"""
Возвращает последние транзакции пользователя.
"""
@@ -95,8 +96,163 @@ class PostgresRepository:
select(Transaction)
.where(Transaction.user_id == user_id)
.order_by(desc(Transaction.created_at))
.limit(limit)
)
return result.scalars().all()
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}")
self.logger.error(f"Ошибка получения транзакций пользователя {user_id}: {e}")
return None
async def get_last_subscription_by_user_id(self, user_id: UUID, limit: int = 1):
"""
Извлекает последнюю подписку пользователя на основании user_id.
:param user_id: UUID пользователя.
:return: Объект Subscription или None.
"""
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))
.limit(limit)
)
subscriptions = list(result.scalars())
self.logger.info(f"Найдены такие подписки: {subscriptions}")
return subscriptions
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении подписки для пользователя {user_id}: {e}")
return None
async def add_record(self, record):
"""
Добавляет запись в базу данных.
:param record: Объект записи.
:return: Запись или None в случае ошибки.
"""
async for session in self.session_generator():
try:
session.add(record)
await session.commit()
return record
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при добавлении записи: {record}: {e}")
await session.rollback()
return None
async def list_active_tickets(self, user_id: UUID):
async for session in self.session_generator():
try:
tickets = await session.execute(
select(SupportTicket)
.where(
SupportTicket.user_id == user_id,
SupportTicket.status.in_([status.upper() for status in ["pending", "open"]])
)
)
result = list(tickets.scalars().all())
self.logger.info(f"Получены активные тикеты: {result}")
return result
except SQLAlchemyError as e:
self.logger.error(f"Произошла ошибка при поиске активных тикетов: {e}")
return None
async def get_ticket(self, ticket_id):
async for session in self.session_generator():
try:
ticket = await session.execute(
select(SupportTicket)
.where(SupportTicket.id == ticket_id)
)
result = ticket.scalars().first()
self.logger.info(f"Получен тикет {ticket_id}.")
if result:
serialized_result = {
"id": result.id,
"user_id": result.user_id,
"subject": result.subject,
"message": result.message,
"status": result.status,
"created_at": result.created_at.isoformat(),
"updated_at": result.updated_at.isoformat(),
}
return serialized_result
except SQLAlchemyError as e:
self.logger.error(f"Произошла ошибка при поиске тикета {ticket_id}.")
return None
async def get_ticket_messages(self, ticket_id: int):
async for session in self.session_generator():
try:
# Выполняем запрос для получения сообщений, сортированных по дате
result = await session.execute(
select(TicketMessage)
.where(TicketMessage.ticket_id == ticket_id)
.order_by(asc(TicketMessage.created_at))
)
messages = result.scalars().all()
self.logger.info(f"Получены сообщения для тикета {ticket_id}, {messages}")
self.logger.info(messages)
return messages
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении сообщений для тикета {ticket_id}: {e}")
return []
async def set_new_status(self,ticket_id: int, new_status: str):
async for session in self.session_generator():
try:
# Выполняем обновление тикета
result = await session.execute(
update(SupportTicket)
.where(SupportTicket.id == ticket_id)
.values(status=new_status)
.execution_options(synchronize_session="fetch")
)
if result.rowcount == 0:
raise ValueError(f"Тикет с ID {ticket_id} не найден.")
await session.commit()
self.logger.info(f"Статус тикета {ticket_id} обновлён на '{new_status}'.")
return "OK"
except SQLAlchemyError as e:
self.logger.error(f"Ошибка обновления статуса тикета {ticket_id}: {e}")
await session.rollback()
return "ERROR"
async def add_referal(self,referrer_id: str, referral_id:str):
"""
Добавление рефералу пользователей
"""
async for session in self.session_generator():
try:
referrer = await session.execute(
select(User)
.where(User.id == referrer_id)
.options(joinedload(User.referrals)) # Загрузка связанных объектов
)
referrer = referrer.scalars().first()
if not referrer:
raise ValueError("Referrer not found")
# Проверить, существует ли уже такой реферал
existing_referrals = [ref.id for ref in referrer.referrals]
if referrer_id in existing_referrals:
raise ValueError("Referral already exists")
# Найти реферала
referral = await session.execute(
select(User).where(User.id == referral_id)
)
referral = referral.scalars().first()
if not referral:
raise ValueError("Referral user not found")
# Добавить реферала в список
referrer.referrals.append(referral)
await session.commit()
except Exception as e:
self.logger(f"Ошибка при добавлении рефералу пользователей")

View File

@@ -1,5 +1,6 @@
import aiohttp
import uuid
import json
import base64
import ssl
@@ -9,79 +10,51 @@ def generate_uuid():
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.ssl_context = self._create_ssl_context(certificate, is_encoded)
self.session_id = None
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.
"""
def _create_ssl_context(self, certificate, is_encoded):
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
self.logger.error(f"Error creating SSL context: {e}")
raise ValueError("Invalid certificate format.") 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.")
try:
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:
self.logger.error("Login failed: Unable to retrieve session ID.")
raise ValueError("Login failed: No session ID.")
except Exception as e:
self.logger.exception("Unexpected error during login.")
raise
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
login_url, data=self.login_data, ssl=self.ssl_context, timeout=10
) as response:
if response.status == 200:
session_id = response.cookies.get("3x-ui")
@@ -89,15 +62,14 @@ class PanelInteraction:
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
error_details = await response.text()
self.logger.error(f"Login failed with status {response.status}: {error_details}")
except aiohttp.ClientError as e:
self.logger.error(f"Login request failed: {e}")
return None
self.logger.exception(f"Login request failed: {e}")
raise
async def get_inbound_info(self, inbound_id):
async def get_inbound_info(self, inbound_id: int = 1):
"""
Fetch inbound information by ID.
@@ -109,8 +81,10 @@ class PanelInteraction:
async with aiohttp.ClientSession() as session:
try:
async with session.get(
url, headers=self.headers, ssl=self.cert_content, timeout=10
url, headers=self.headers, ssl=self.ssl_context, timeout=10
) as response:
response_text = await response.text() # Получаем текст ответа
self.logger.info(f"Inbound Info (raw): {response_text}") # Логируем сырой текст
if response.status == 200:
return await response.json()
else:
@@ -132,7 +106,7 @@ class PanelInteraction:
async with aiohttp.ClientSession() as session:
try:
async with session.get(
url, headers=self.headers, ssl=self.cert_content, timeout=10
url, headers=self.headers, ssl=self.ssl_context, timeout=10
) as response:
if response.status == 200:
return await response.json()
@@ -176,7 +150,7 @@ class PanelInteraction:
async with aiohttp.ClientSession() as session:
try:
async with session.post(
url, headers=self.headers, json=update_data, ssl=self.cert_content
url, headers=self.headers, json=update_data, ssl=self.ssl_context
) as response:
if response.status == 200:
self.logger.info("Client expiry updated successfully.")
@@ -197,36 +171,62 @@ class PanelInteraction:
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": ""
}
]
"id": generate_uuid(),
"flow": "xtls-rprx-vision",
"email": email,
"limitIp": 2,
"totalGB": 0,
"expiryTime": expiry_date,
"enable": True,
"tgId": "",
"subId": "",
"reset": 0
}
settings = json.dumps({"clients": [client_info]}) # Преобразуем объект в JSON-строку
payload = {
"id": inbound_id,
"settings": client_info
"id": int(inbound_id), # Преобразуем inbound_id в число
"settings": settings # Передаем settings как JSON-строку
}
async with aiohttp.ClientSession() as session:
try:
async with session.post(
url, headers=self.headers, json=payload, ssl=self.cert_content
url, headers=self.headers, json=payload, ssl=self.ssl_context
) as response:
if response.status == 200:
return await response.status
response_json = await response.json()
if response.status == 200 and response_json.get('success'):
self.logger.info(f"Клиент успешно добавлен: {response_json}")
return "OK"
else:
self.logger.error(f"Failed to add client: {response.status}")
error_msg = response_json.get('msg', 'Причина не указана')
self.logger.error(f"Не удалось добавить клиента: {error_msg}")
return None
except aiohttp.ClientError as e:
self.logger.error(f"Add client request failed: {e}")
return None
async def delete_depleted_clients(self, inbound_id=None):
"""
Удалить исчерпанных клиентов.
:param inbound_id: ID входящего соединения (inbound), если None, удаляет для всех.
:return: Ответ сервера или None в случае ошибки.
"""
await self._ensure_logged_in()
url = f"{self.base_url}/panel/api/inbounds/delDepletedClients/{inbound_id or ''}".rstrip('/')
async with aiohttp.ClientSession() as session:
try:
async with session.post(url, headers=self.headers, ssl=self.ssl_context, timeout=10) as response:
if response.status == 200:
self.logger.info(f"Depleted clients deleted successfully for inbound_id: {inbound_id}")
return await response.json()
else:
error_details = await response.text()
self.logger.error(f"Failed to delete depleted clients: {response.status} - {error_details}")
return None
except aiohttp.ClientError as e:
self.logger.error(f"Delete depleted clients request failed: {e}")
return None

View File

View File

@@ -0,0 +1,42 @@
import logging
import sys
def setup_logging():
# Очистка существующих обработчиков
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
# Настройка формата
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Обработчик для вывода в консоль
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
# Установка уровня для корневого логгера
logging.basicConfig(
level=logging.INFO,
handlers=[console_handler],
force=True # Перезаписать существующие настройки
)
# Установка уровня для конкретных логгеров
loggers = [
'app.routes',
'app.services',
'main',
'__main__'
]
for logger_name in loggers:
logger = logging.getLogger(logger_name)
logger.setLevel(logging.INFO)
# Удаляем существующие обработчики
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# Добавляем наш обработчик
logger.addHandler(console_handler)
# Запрещаем передачу родительским логгерам
logger.propagate = False

View File

@@ -1,7 +1,9 @@
from sqlalchemy import Column, String, Numeric, DateTime, Boolean, ForeignKey, Integer
from sqlalchemy import Column, String, Numeric, DateTime, Boolean, ForeignKey, Integer, Enum, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
from datetime import datetime
from enum import Enum as PyEnum
import uuid
Base = declarative_base()
@@ -9,27 +11,37 @@ Base = declarative_base()
def generate_uuid():
return str(uuid.uuid4())
class TicketStatus(PyEnum):
OPEN = "open"
PENDING = "pending"
CLOSED = "closed"
"""Пользователи"""
class User(Base):
__tablename__ = 'users'
id = Column(String, primary_key=True, default=generate_uuid)
telegram_id = Column(Integer, unique=True, nullable=False)
id = Column(UUID(as_uuid=True), primary_key=True, default=generate_uuid)
telegram_id = Column(String, unique=True, nullable=False) # telegram_id как уникальный идентификатор
username = Column(String)
balance = Column(Numeric(10, 2), default=0.0)
referrer_id = Column(String, ForeignKey('users.telegram_id'), nullable=True) # Ссылка на telegram_id
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
referrals = relationship("User", backref="referrer", remote_side=[telegram_id]) # Ссылка на telegram_id
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'))
user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'))
vpn_server_id = Column(String)
plan = Column(String)
expiry_date = Column(DateTime)
@@ -43,18 +55,40 @@ class Transaction(Base):
__tablename__ = 'transactions'
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey('users.id'))
user_id = Column(UUID(as_uuid=True), 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 SupportTicket(Base):
__tablename__ = "support_tickets"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
subject = Column(String, nullable=False)
message = Column(String, nullable=False)
status = Column(Enum(TicketStatus), default=TicketStatus.OPEN, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
"""Сообщения из тикетов"""
class TicketMessage(Base):
__tablename__ = "ticket_messages"
id = Column(Integer, primary_key=True, index=True)
ticket_id = Column(Integer, ForeignKey("support_tickets.id"), nullable=False) # ID тикета
sender = Column(String, nullable=False) # "user" или "support"
message = Column(Text, nullable=False) # Текст сообщения
created_at = Column(DateTime, default=datetime.utcnow, nullable=False) #
"""Администраторы"""
class Administrators(Base):
__tablename__ = 'admins'
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey('users.id'))
user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'))
user = relationship("User", back_populates="admins")

103
main.py
View File

@@ -1,42 +1,107 @@
import sys
from fastapi import FastAPI
from instance.config import setup_logging
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stdout)],
force=True
)
from instance.configdb import init_postgresql, init_mongodb, close_connections
from app.routes import user_router, payment_router, subscription_router
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from app.routes import router, subscription_router, sup_router
from app.services.db_manager import DatabaseManager
from instance.configdb import get_postgres_session
from app.services.mongo_rep import MongoDBRepository
from app.services.xui_rep import PanelInteraction
logger = logging.getLogger(__name__)
# Создаём приложение FastAPI
app = FastAPI()
# Инициализация менеджера базы данных
database_manager = DatabaseManager(session_generator=get_postgres_session)
mongo_repo = MongoDBRepository()
async def delete_depleted_clients_task():
"""
Удаляет исчерпанных клиентов на всех серверах из MongoDB.
"""
try:
# Получаем список серверов из MongoDB
servers = await mongo_repo.list_servers()
if not servers:
logger.warning("Список серверов пуст. Задача пропущена.")
return
# Проходим по каждому серверу и вызываем delete_depleted_clients
for server in servers:
base_url = server.get("base_url")
login_data = server.get("login_data")
certificate = server.get("certificate")
if not base_url or not login_data:
logger.error(f"Пропуск сервера из-за отсутствия данных: {server}")
continue
# Создаём экземпляр PanelInteraction
panel = PanelInteraction(
base_url=base_url,
login_data=login_data,
logger=logger,
certificate=certificate
)
# Выполняем удаление исчерпанных клиентов
response = await panel.delete_depleted_clients()
if response:
logger.info(f"Удаление клиентов завершено успешно для сервера: {base_url}")
else:
logger.warning(f"Не удалось удалить клиентов для сервера: {base_url}")
except Exception as e:
logger.error(f"Ошибка при выполнении задачи delete_depleted_clients: {e}")
scheduler = AsyncIOScheduler()
# Событие при старте приложения
@app.on_event("startup")
async def startup():
"""
Инициализация подключения к базам данных.
"""
await init_postgresql()
await init_mongodb()
try:
scheduler.add_job(delete_depleted_clients_task, CronTrigger(hour=23, minute=59))
scheduler.start()
logger.info("Инициализация PostgreSQL...")
await init_postgresql()
logger.info("PostgreSQL успешно инициализирован.")
logger.info("Инициализация MongoDB...")
await init_mongodb()
logger.info("MongoDB успешно инициализирован.")
except Exception as e:
logger.error(f"Ошибка при инициализации баз данных: {e}")
raise RuntimeError("Не удалось инициализировать базы данных")
# Событие при завершении работы приложения
@app.on_event("shutdown")
async def shutdown():
"""
Закрытие соединений с базами данных.
"""
await close_connections()
try:
logger.info("Закрытие соединений с базами данных...")
await close_connections()
logger.info("Соединения с базами данных успешно закрыты.")
except Exception as e:
logger.error(f"Ошибка при закрытии соединений: {e}")
# Подключение маршрутов
app.include_router(user_router, prefix="/api")
app.include_router(payment_router, prefix="/api")
app.include_router(router, prefix="/api")
#app.include_router(payment_router, prefix="/api")
app.include_router(subscription_router, prefix="/api")
# Пример корневого маршрута
app.include_router(sup_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}}
def read_root():
return {"message": "FastAPI приложение работает!"}

View File

@@ -3,14 +3,16 @@ aiohttp==3.11.11
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.7.0
APScheduler==3.11.0
asyncpg==0.30.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
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.4
@@ -27,5 +29,7 @@ sniffio==1.3.1
SQLAlchemy==2.0.36
starlette==0.41.3
typing_extensions==4.12.2
tzlocal==5.2
uvicorn==0.34.0
Werkzeug==3.1.3
yarl==1.18.3

0
run.py
View File

128
tests/add.py Normal file
View File

@@ -0,0 +1,128 @@
import argparse
from datetime import datetime
import json
import base64
from pymongo import MongoClient
def connect_to_mongo(uri, db_name):
"""Подключение к MongoDB."""
client = MongoClient(uri)
db = client[db_name]
return db
def load_raw_json(json_path):
"""Загружает сырые JSON-данные из файла."""
with open(json_path, "r", encoding="utf-8") as f:
return json.loads(f.read())
def encode_file(file_path):
"""Читает файл и кодирует его в Base64."""
with open(file_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def transform_data(raw_data):
"""Преобразует исходные сырые данные в целевую структуру."""
try:
settings = json.loads(raw_data["obj"]["settings"])
stream_settings = json.loads(raw_data["obj"]["streamSettings"])
sniffing_settings = json.loads(raw_data["obj"]["sniffing"])
transformed = {
"server": {
"name": raw_data["obj"].get("remark", "Unknown"),
"ip": "45.82.255.110", # Замените на актуальные данные
"port": "2053",
"secretKey": "Hd8OsqN5Jh", # Замените на актуальные данные
"login": "nc1450nP", # Замените на актуальные данные
"password": "KmajQOuf" # Замените на актуальные данные
},
"clients": [
{
"email": client["email"],
"inboundId": raw_data["obj"].get("id"),
"id": client["id"],
"flow": client.get("flow", ""),
"limits": {
"ipLimit": client.get("limitIp", 0),
"reset": client.get("reset", 0),
"totalGB": client.get("totalGB", 0)
},
"subscriptions": {
"subId": client.get("subId", ""),
"tgId": client.get("tgId", "")
}
} for client in settings["clients"]
],
"connection": {
"destination": stream_settings["realitySettings"].get("dest", ""),
"serverNames": stream_settings["realitySettings"].get("serverNames", []),
"security": stream_settings.get("security", ""),
"publicKey": stream_settings["realitySettings"]["settings"].get("publicKey", ""),
"fingerprint": stream_settings["realitySettings"]["settings"].get("fingerprint", ""),
"shortIds": stream_settings["realitySettings"].get("shortIds", []),
"tcpSettings": {
"acceptProxyProtocol": stream_settings["tcpSettings"].get("acceptProxyProtocol", False),
"headerType": stream_settings["tcpSettings"]["header"].get("type", "none")
},
"sniffing": {
"enabled": sniffing_settings.get("enabled", False),
"destOverride": sniffing_settings.get("destOverride", [])
}
}
}
return transformed
except KeyError as e:
raise ValueError(f"Ошибка преобразования данных: отсутствует ключ {e}")
def insert_certificate(data, cert_path, cert_location):
"""Добавляет сертификат в указанное место внутри структуры JSON."""
# Читаем и кодируем сертификат
certificate_data = encode_file(cert_path)
# Разбиваем путь на вложенные ключи
keys = cert_location.split(".")
target = data
for key in keys[:-1]:
if key not in target:
target[key] = {} # Создаем вложенные ключи, если их нет
target = target[key]
target[keys[-1]] = {
"data": certificate_data,
"uploaded_at": datetime.utcnow()
}
def insert_data(db, collection_name, data):
"""Вставляет данные в указанную коллекцию MongoDB."""
collection = db[collection_name]
collection.insert_one(data)
print(f"Данные успешно вставлены в коллекцию '{collection_name}'.")
def main():
parser = argparse.ArgumentParser(description="Insert raw JSON data into MongoDB with certificate")
parser.add_argument("--mongo-uri", default="mongodb://root:itOj4CE2miKR@mongodb:27017", help="MongoDB URI")
parser.add_argument("--db-name", default="MongoDBSub&Ser", help="MongoDB database name")
parser.add_argument("--collection", default="servers", help="Collection name")
parser.add_argument("--json-path", required=True, help="Path to the JSON file with raw data")
parser.add_argument("--cert-path", help="Path to the certificate file (.crt)")
parser.add_argument("--cert-location", default='server.certificate', help="Path inside JSON structure to store certificate (e.g., 'server.certificate')")
args = parser.parse_args()
# Подключение к MongoDB
db = connect_to_mongo(args.mongo_uri, args.db_name)
# Загрузка сырых данных из JSON-файла
raw_data = load_raw_json(args.json_path)
# Преобразование данных в нужную структуру
transformed_data = transform_data(raw_data)
# Вставка сертификата в структуру данных (если путь к сертификату указан)
if args.cert_path:
insert_certificate(transformed_data, args.cert_path, args.cert_location)
# Вставка данных в MongoDB
insert_data(db, args.collection, transformed_data)
if __name__ == "__main__":
main()

74
tests/add2.py Normal file
View File

@@ -0,0 +1,74 @@
import argparse
from datetime import datetime
import json
import glob
from pymongo import MongoClient
def connect_to_mongo(uri, db_name):
"""Подключение к MongoDB."""
client = MongoClient(uri)
db = client[db_name]
return db
def load_all_json_from_folder(folder_path):
"""Загружает все JSON-файлы из указанной папки."""
all_data = []
for file_path in glob.glob(f"{folder_path}/*.json"):
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
all_data.append(data)
except Exception as e:
print(f"Ошибка при чтении файла {file_path}: {e}")
return all_data
def fetch_all_documents(mongo_uri, db_name, collection_name):
"""Выводит все элементы из указанной коллекции MongoDB."""
try:
client = MongoClient(mongo_uri)
db = client[db_name]
collection = db[collection_name]
documents = collection.find()
print(f"Содержимое коллекции '{collection_name}':")
for doc in documents:
print(doc)
except Exception as e:
print(f"Ошибка при получении данных: {e}")
finally:
client.close()
def insert_data(db, collection_name, data):
"""Вставляет данные в указанную коллекцию MongoDB."""
collection = db[collection_name]
for i in data:
collection.insert_one(i)
print(f"Данные '{i}'")
print(f"Данные успешно вставлены в коллекцию '{collection_name}'.")
def main():
parser = argparse.ArgumentParser(description="Insert JSON data into MongoDB with certificate")
parser.add_argument("--mongo-uri",default="mongodb://root:itOj4CE2miKR@mongodb:27017" ,required=True, help="MongoDB URI")
parser.add_argument("--db-name",default="MongoDBSub&Ser" ,required=True, help="MongoDB database name")
parser.add_argument("--collection",default="plans", required=True, help="Collection name")
parser.add_argument("--json-path", required=True, help="Path to the JSON file with data")
args = parser.parse_args()
db = connect_to_mongo(args.mongo_uri, args.db_name)
data = load_all_json_from_folder(args.json_path)
insert_data(db, args.collection, data)
fetch_all_documents(args.mongo_uri, args.db_name,args.collection)
if __name__ == "__main__":
main()

26
tests/ca.crt Normal file
View File

@@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEYzCCA0ugAwIBAgIUEni/Go2t3/FXT7CGBMSnrlDzTKwwDQYJKoZIhvcNAQEL
BQAwgcAxCzAJBgNVBAYTAlVBMRswGQYDVQQIDBJSZXB1YmxpYyBvZiBDcmltZWEx
EzARBgNVBAcMClNpbWZlcm9wb2wxFzAVBgNVBAoMDkxhcmsgQ28gU3lzdGVtMSUw
IwYDVQQLDBxMYXJrIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRgwFgYDVQQDDA9M
YXJrIFRydXN0ZWQgQ0ExJTAjBgkqhkiG9w0BCQEWFmxhcmtjb3N5c3RlbUBwcm90
b24ubWUwHhcNMjQxMjI3MTQ1NzQ2WhcNMzQxMjI1MTQ1NzQ2WjCBwDELMAkGA1UE
BhMCVUExGzAZBgNVBAgMElJlcHVibGljIG9mIENyaW1lYTETMBEGA1UEBwwKU2lt
ZmVyb3BvbDEXMBUGA1UECgwOTGFyayBDbyBTeXN0ZW0xJTAjBgNVBAsMHExhcmsg
Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGDAWBgNVBAMMD0xhcmsgVHJ1c3RlZCBD
QTElMCMGCSqGSIb3DQEJARYWbGFya2Nvc3lzdGVtQHByb3Rvbi5tZTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAOzb2ibfe4Arrf5O3d15kObBJQkxcSGi
fzrtYj68/y0ZyNV3BTvp+gCdlmo+WqOrdgD4LCOod0585S2MLCxjvVIcuA+DIq6z
gxZvf6V1FRKjHO3s18HhUX6nl8LYe6bOveqHAiDf9TZ+8grJXYpGD2tybAofXkL5
8dmn5Jh10DTV2EBHwutET2hoBqSorop/Ro/zawYPOlMZuGXP4Txs/erUmNCzGm+b
AYw6qjBm+o9RG2AWzKVBI06/kFKA5vq7ATcEs2U5bdINy/U1u2vc1R08YuvTpPCh
2Q0uBn49T+WhiF9CpAYBoMj51Am22NqKWsc617ZFkl1OO3mWd4+mgocCAwEAAaNT
MFEwHQYDVR0OBBYEFAXCcmOWdaInuJLeY/5CRfdzb49+MB8GA1UdIwQYMBaAFAXC
cmOWdaInuJLeY/5CRfdzb49+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBADoyGv2Gdem/zrHCEy43WlXo9uFqKCX/Z/Rlr+V9OmRodZx83j2B0xIB
QdjuEP/EaxEtuGL98TIln7u8PX/FKApbZAk9zxrn0JNQ6bAVpsLBK1i+w3aw2XlN
p6qmFoc66Z8B1OUiGHrWczw0cV4rr8XoGwD5KS/jXYyuT+JTFBdsYXmUXqwqcwHY
N4qXRKh8FtqTgvjb/TpETMr7bnEHkn0vUwMwKwRe4TB1VwFIAaJeh7DPnrchy5xQ
EpS2DIQoO+ZoOaQYIkFT/8c7zpN79fy5uuVfW4XL8OS7sbZkzsl2YJDtO5zCEDNx
CJeEKQYXpCXRi+n3RvsIedshrnmqZcg=
-----END CERTIFICATE-----

1
tests/ser.json Normal file
View File

@@ -0,0 +1 @@
{"success":true,"msg":"","obj":{"id":1,"up":301130896430,"down":4057274949955,"total":0,"remark":"vlv","enable":true,"expiryTime":0,"clientStats":null,"listen":"","port":443,"protocol":"vless","settings":"{\n \"clients\": [\n {\n \"email\": \"j8oajwd3\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"xtls-rprx-vision\",\n \"id\": \"a31d71a4-6afd-4f36-96f6-860691b52873\",\n \"limitIp\": 2,\n \"reset\": 0,\n \"subId\": \"ox2awiqwduryuqnz\",\n \"tgId\": 1342351277,\n \"totalGB\": 0\n },\n {\n \"email\": \"cvvbqpm2\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"xtls-rprx-vision\",\n \"id\": \"b6882942-d69d-4d5e-be9a-168ac89b20b6\",\n \"limitIp\": 1,\n \"reset\": 0,\n \"subId\": \"jk289x00uf7vbr9x\",\n \"tgId\": 123144325,\n \"totalGB\": 0\n },\n {\n \"email\": \"k15vx82w\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"\",\n \"id\": \"3c88e5bb-53ba-443d-9d68-a09f5037030c\",\n \"limitIp\": 0,\n \"reset\": 0,\n \"subId\": \"5ffz56ofveepep1t\",\n \"tgId\": 5364765066,\n \"totalGB\": 0\n },\n {\n \"email\": \"gm5x10tr\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"xtls-rprx-vision\",\n \"id\": \"c0b9ff6c-4c48-4d75-8ca0-c13a2686fa5d\",\n \"limitIp\": 0,\n \"reset\": 0,\n \"subId\": \"132pioffqi6gwhw6\",\n \"tgId\": \"\",\n \"totalGB\": 0\n }\n ],\n \"decryption\": \"none\",\n \"fallbacks\": []\n}","streamSettings":"{\n \"network\": \"tcp\",\n \"security\": \"reality\",\n \"externalProxy\": [],\n \"realitySettings\": {\n \"show\": false,\n \"xver\": 0,\n \"dest\": \"google.com:443\",\n \"serverNames\": [\n \"google.com\",\n \"www.google.com\"\n ],\n \"privateKey\": \"gKsDFmRn0vyLMUdYdk0ZU_LVyrQh7zMl4r-9s0nNFCk\",\n \"minClient\": \"\",\n \"maxClient\": \"\",\n \"maxTimediff\": 0,\n \"shortIds\": [\n \"edfaf8ab\"\n ],\n \"settings\": {\n \"publicKey\": \"Bha0eW7nfRc69CdZyF9HlmGVvtAeOJKammhwf4WShTU\",\n \"fingerprint\": \"random\",\n \"serverName\": \"\",\n \"spiderX\": \"/\"\n }\n },\n \"tcpSettings\": {\n \"acceptProxyProtocol\": false,\n \"header\": {\n \"type\": \"none\"\n }\n }\n}","tag":"inbound-443","sniffing":"{\n \"enabled\": true,\n \"destOverride\": [\n \"http\",\n \"tls\",\n \"quic\",\n \"fakedns\"\n ],\n \"metadataOnly\": false,\n \"routeOnly\": false\n}"}}

8
tests/subs/sub1.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "Lark_Standart_1",
"normalName": "Lark Standart",
"type": "standart",
"duration_months": 1,
"ip_limit": 1,
"price": 200
}

8
tests/subs/sub2.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "Lark_Standart_6",
"normalName": "Lark Standart",
"type": "standart",
"duration_months": 6,
"ip_limit": 1,
"price": 1000
}

8
tests/subs/sub3.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "Lark_Standart_12",
"normalName": "Lark Standart",
"type": "standart",
"duration_months": 12,
"ip_limit": 1,
"price": 2000
}

8
tests/subs/sub4.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "Lark_Pro_1",
"normalName": "Lark Pro",
"type": "pro",
"duration_months": 1,
"ip_limit": 5,
"price": 600
}

8
tests/subs/sub5.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "Lark_Pro_6",
"normalName": "Lark Pro",
"type": "pro",
"duration_months": 6,
"ip_limit": 5,
"price": 3000
}

8
tests/subs/sub6.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "Lark_Pro_12",
"normalName": "Lark Pro",
"type": "pro",
"duration_months": 12,
"ip_limit": 5,
"price": 5000
}