Переделал модель БД под новую, переделал Репозиторий, переделал сервисы, убрал монгодб, изменил необходимые пакеты, марзбан я добавил, но не настроил. Весь старый бот вроде работает(только в рефералке не уверен)

This commit is contained in:
root
2025-11-24 23:43:40 +03:00
parent f0f3b96005
commit e975bf4774
18 changed files with 637 additions and 1217 deletions

4
app/services/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from .db_manager import DatabaseManager
#from .marzban import MarzbanService
__all__ = ['DatabaseManager']

View File

@@ -1,10 +1,9 @@
from decimal import Decimal
import json
from instance.model import User, Subscription, Transaction, SupportTicket, TicketMessage, TicketStatus
from .xui_rep import PanelInteraction
from instance.model import User, Subscription, Transaction
from app.services.marzban import MarzbanService
from .postgres_rep import PostgresRepository
from .mongo_rep import MongoDBRepository
from instance.model import Transaction
from instance.model import Transaction,TransactionType
from dateutil.relativedelta import relativedelta
from datetime import datetime
import random
@@ -20,52 +19,46 @@ class DatabaseManager:
Инициализация с асинхронным генератором сессий (например, get_postgres_session).
"""
self.logger = logging.getLogger(__name__)
self.mongo_repo = MongoDBRepository()
self.postgres_repo = PostgresRepository(session_generator, self.logger)
async def get_active_tickets(self, user_id: UUID):
"""
Получает активные подписки пользователя
"""
return await self.postgres_repo.list_active_tickets(user_id)
async def create_user(self, telegram_id: str, referrer_id: Optional[str]= None):
async def create_user(self, telegram_id: int, invented_by: Optional[int]= None):
"""
Создаёт пользователя.
"""
try:
username = self.generate_string(6)
return await self.postgres_repo.create_user(telegram_id, username, referrer_id)
return await self.postgres_repo.create_user(telegram_id, username, invented_by)
except Exception as e:
self.logger.error(f"Ошибка при создании пользователя:{e}")
async def get_user_by_telegram_id(self, telegram_id: str):
async def get_user_by_telegram_id(self, telegram_id: int):
"""
Возвращает пользователя по Telegram ID.
"""
return await self.postgres_repo.get_user_by_telegram_id(telegram_id)
async def add_transaction(self, user_id: UUID, amount: float):
async def add_transaction(self, telegram_id: int, amount: float):
"""
Добавляет транзакцию.
"""
tran = Transaction(
user_id=user_id,
user_id=telegram_id,
amount=Decimal(amount),
transaction_type="default"
type=TransactionType.DEPOSIT
)
return await self.postgres_repo.add_record(tran)
async def add_referal(self,referrer_id: str, new_user_id: str):
async def add_referal(self,referrer_id: int, new_user_telegram_id: int):
"""
Добавление рефералу пользователей
"""
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.add_referral(referrer_id,new_user_telegram_id)
async def get_transaction(self, telegram_id: int, limit: int = 10):
"""
Возвращает транзакции.
"""
return await self.postgres_repo.get_last_transactions(user_id, limit)
return await self.postgres_repo.get_last_transactions(telegram_id, limit)
async def update_balance(self, telegram_id: str, amount: float):
async def update_balance(self, telegram_id: int, amount: float):
"""
Обновляет баланс пользователя и добавляет транзакцию.
"""
@@ -81,227 +74,186 @@ class DatabaseManager:
return "ERROR"
self.logger.info(f"Баланс пользователя {telegram_id} обновлен на {amount}, добавление транзакции")
await self.add_transaction(user.id, amount)
await self.add_transaction(user.telegram_id, amount)
return "OK"
async def get_active_subscription(self, telegram_id: str):
async def get_active_subscription(self, telegram_id: int):
"""
Проверяет наличие активной подписки.
"""
return await self.postgres_repo.get_active_subscription(telegram_id)
async def get_last_subscriptions(self, user_id: UUID, limit: int ):
async def get_last_subscriptions(self, telegram_id: int, limit: int = 1):
"""
Возвращает список последних подписок.
"""
return await self.postgres_repo.get_last_subscription_by_user_id(user_id, limit)
return await self.postgres_repo.get_last_subscription_by_user_id(telegram_id, limit)
async def buy_sub(self, telegram_id: str, plan_id: str):
async def buy_sub(self, telegram_id: int, plan_id: str):
"""
Покупает подписку.
"""
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"
# 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"
# 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)
# 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"
# if not new_subscription:
# return "ERROR"
self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id}.")
return "OK"
# self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id}.")
# return "OK"
pass
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"
# 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"
# 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"
# cost = int(plan["price"])
# if user.balance < cost:
# self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.")
# return "INSUFFICIENT_FUNDS"
return user, plan
# return user, plan
pass
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
# 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,
)
# 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"],
)
# 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)
# 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
# return new_subscription, server
pass
async def generate_uri(self, telegram_id: str):
async def generate_uri(self, telegram_id: int):
"""
Генерация 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"
# 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
# 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
# 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 из данных пользователя
# 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"],
)
# 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
# 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}")
# # Логируем полученные данные
# self.logger.info(f"Inbound Info: {inbound_info}")
# Разбор JSON-строк
try:
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
# # Разбор JSON-строк
# try:
# 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
settings = json.loads(inbound_info["obj"]["settings"]) # Разбираем JSON
# settings = json.loads(inbound_info["obj"]["settings"]) # Разбираем JSON
# Находим клиента по 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
# # Находим клиента по 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
server_info = server["server"]
# server_info = server["server"]
# Преобразование данных в формат 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']}"
)
# # Преобразование данных в формат 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']}"
# )
self.logger.info(f"Сформирован URI для пользователя {telegram_id}: {uri}")
return uri
except Exception as e:
self.logger.error(f"Ошибка при генерации URI для пользователя {telegram_id}: {e}")
return None
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)
async def update_ticket_status(self, ticket_id: int, new_status: str):
"""
Обновляет статус тикета.
Args:
ticket_id (int): ID тикета, статус которого нужно обновить.
new_status (str): Новый статус тикета.
Returns:
dict: Словарь с ID тикета и обновлённым статусом.
Raises:
ValueError: Если тикет не найден.
"""
return await self.postgres_repo.set_new_status(ticket_id,new_status)
# self.logger.info(f"Сформирован URI для пользователя {telegram_id}: {uri}")
# return uri
# except Exception as e:
# self.logger.error(f"Ошибка при генерации URI для пользователя {telegram_id}: {e}")
# return None
pass
@staticmethod
def generate_string(length):

282
app/services/marzban.py Normal file
View File

@@ -0,0 +1,282 @@
from typing import Any, Dict, Optional, Literal
import aiohttp
import requests
from datetime import date, datetime, time, timezone
import logging
from instance import User, Subscription
class MarzbanUser:
"""Модель пользователя Marzban"""
def __init__(self, data: Dict[str, Any]):
self.username = data.get('username')
self.status = data.get('status')
self.expire = data.get('expire')
self.data_limit = data.get('data_limit')
self.data_limit_reset_strategy = data.get('data_limit_reset_strategy')
self.used_traffic = data.get('used_traffic')
self.lifetime_used_traffic = data.get('lifetime_used_traffic')
self.subscription_url = data.get('subscription_url')
self.online_at = data.get('online_at')
self.created_at = data.get('created_at')
self.proxies = data.get('proxies', {})
self.inbounds = data.get('inbounds', {})
self.note = data.get('note')
class UserStatus:
"""Статус пользователя"""
def __init__(self, data: Dict[str, Any]):
self.used_traffic = data.get('used_traffic', 0)
self.lifetime_used_traffic = data.get('lifetime_used_traffic', 0)
self.online_at = data.get('online_at')
self.status = data.get('status')
self.expire = data.get('expire')
self.data_limit = data.get('data_limit')
class MarzbanService:
def __init__(self, baseURL: str, username: str, password: str) -> None:
self.base_url = baseURL.rstrip('/')
self.token = self._get_token(username, password)
self.headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
self._session: Optional[aiohttp.ClientSession] = None
def _get_token(self, username: str, password: str) -> str:
"""Получение токена авторизации"""
try:
response = requests.post(
f"{self.base_url}/api/admin/token",
data={'username': username, 'password': password}
)
response.raise_for_status()
return response.json()['access_token']
except requests.RequestException as e:
logging.error(f"Failed to get token: {e}")
raise Exception(f"Authentication failed: {e}")
async def _get_session(self) -> aiohttp.ClientSession:
"""Ленивое создание сессии"""
if self._session is None or self._session.closed:
timeout = aiohttp.ClientTimeout(total=30)
self._session = aiohttp.ClientSession(timeout=timeout)
return self._session
async def _make_request(self, endpoint: str, method: Literal["get", "post", "put", "delete"],
data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Улучшенный метод для запросов"""
url = f"{self.base_url}{endpoint}"
try:
session = await self._get_session()
async with session.request(
method=method.upper(),
url=url,
headers=self.headers,
json=data
) as response:
response_data = await response.json() if response.content_length else {}
if response.status not in (200, 201):
raise Exception(f"HTTP {response.status}: {response_data}")
return response_data
except aiohttp.ClientError as e:
logging.error(f"Network error during {method.upper()} to {url}: {e}")
raise
except Exception as e:
logging.error(f"Unexpected error during request to {url}: {e}")
raise
async def create_user(self, user: User, subscription: Subscription) -> MarzbanUser:
"""Создает нового пользователя в Marzban"""
username = f"user_{user.telegram_id}"
if subscription.end_date:
if isinstance(subscription.end_date, datetime):
if subscription.end_date.tzinfo is None:
end_date = subscription.end_date.replace(tzinfo=timezone.utc)
else:
end_date = subscription.end_date
expire_timestamp = int(end_date.timestamp())
elif isinstance(subscription.end_date, date):
end_datetime = datetime.combine(
subscription.end_date,
time(23, 59, 59),
tzinfo=timezone.utc
)
expire_timestamp = int(end_datetime.timestamp())
else:
expire_timestamp = 0
else:
expire_timestamp = 0
data = {
"username": username,
"status": "active",
"expire": expire_timestamp,
"data_limit": 100 * 1073741824, # Конвертируем GB в bytes
"data_limit_reset_strategy": "no_reset",
"proxies": {
"trojan": {}
},
"inbounds": {
"trojan": ["TROJAN WS NOTLS"]
},
"note": f"Telegram: {user.telegram_id}",
"on_hold_timeout": None,
"on_hold_expire_duration": 0,
"next_plan": {
"add_remaining_traffic": False,
"data_limit": 0,
"expire": 0,
"fire_on_either": True
}
}
try:
response_data = await self._make_request("/api/user", "post", data)
marzban_user = MarzbanUser(response_data)
logging.info(f"User {username} created successfully")
return marzban_user
except Exception as e:
logging.error(f"Failed to create user {username}: {e}")
raise Exception(f"Failed to create user: {e}")
async def update_user(self, user: User, subscription: Subscription) -> MarzbanUser:
"""Обновляет существующего пользователя"""
username = f"user_{user.telegram_id}"
if subscription.end_date:
if isinstance(subscription.end_date, datetime):
# Если это datetime, преобразуем в timestamp
if subscription.end_date.tzinfo is None:
end_date = subscription.end_date.replace(tzinfo=timezone.utc)
else:
end_date = subscription.end_date
expire_timestamp = int(end_date.timestamp())
elif isinstance(subscription.end_date, date):
# Если это date, создаем datetime на конец дня и преобразуем в timestamp
end_datetime = datetime.combine(
subscription.end_date,
time(23, 59, 59),
tzinfo=timezone.utc
)
expire_timestamp = int(end_datetime.timestamp())
else:
expire_timestamp = 0
else:
expire_timestamp = 0
data = {
"status": "active",
"expire": expire_timestamp
}
try:
response_data = await self._make_request(f"/api/user/{username}", "put", data)
marzban_user = MarzbanUser(response_data)
logging.info(f"User {username} updated successfully")
return marzban_user
except Exception as e:
logging.error(f"Failed to update user {username}: {e}")
raise Exception(f"Failed to update user: {e}")
async def disable_user(self, user: User) -> bool:
"""Отключает пользователя"""
username = f"user_{user.telegram_id}"
data = {
"status": "disabled"
}
try:
await self._make_request(f"/api/user/{username}", "put", data)
logging.info(f"User {username} disabled successfully")
return True
except Exception as e:
logging.error(f"Failed to disable user {username}: {e}")
return False
async def enable_user(self, user: User) -> bool:
"""Включает пользователя"""
username = f"user_{user.telegram_id}"
data = {
"status": "active"
}
try:
await self._make_request(f"/api/user/{username}", "put", data)
logging.info(f"User {username} enabled successfully")
return True
except Exception as e:
logging.error(f"Failed to enable user {username}: {e}")
return False
async def delete_user(self, user: User) -> bool:
"""Полностью удаляет пользователя из Marzban"""
username = f"user_{user.telegram_id}"
try:
await self._make_request(f"/api/user/{username}", "delete")
logging.info(f"User {username} deleted successfully")
return True
except Exception as e:
logging.error(f"Failed to delete user {username}: {e}")
return False
async def get_user_status(self, user: User) -> UserStatus:
"""Получает текущий статус пользователя"""
username = f"user_{user.telegram_id}"
try:
response_data = await self._make_request(f"/api/user/{username}", "get")
return UserStatus(response_data)
except Exception as e:
logging.error(f"Failed to get status for user {username}: {e}")
raise Exception(f"Failed to get user status: {e}")
async def get_subscription_url(self, user: User) -> str:
"""Возвращает готовую subscription_url для подключения"""
username = f"user_{user.telegram_id}"
try:
response_data = await self._make_request(f"/api/user/{username}", "get")
return response_data.get('subscription_url', '')
except Exception as e:
logging.error(f"Failed to get subscription URL for user {username}: {e}")
return ""
async def get_config_links(self, user: User) -> str:
"""Возвращает конфигурации для подключения"""
username = f"user_{user.telegram_id}"
try:
response_data = await self._make_request(f"/api/user/{username}", "get")
return response_data.get('links', '')
except Exception as e:
logging.error(f"Failed to get configurations URL's for user {username}: {e}")
return ""
async def check_marzban_health(self) -> bool:
"""Проверяет доступность Marzban API"""
try:
await self._make_request("/api/admin", "get")
return True
except Exception as e:
logging.error(f"Marzban health check failed: {e}")
return False
async def close(self):
"""Закрытие сессии"""
if self._session and not self._session.closed:
await self._session.close()
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()

View File

@@ -1,158 +0,0 @@
import os
from motor.motor_asyncio import AsyncIOMotorClient
from pymongo.errors import DuplicateKeyError, NetworkTimeout
import logging
class MongoDBRepository:
def __init__(self):
# Настройки MongoDB из переменных окружения
mongo_uri = os.getenv("MONGO_URL")
database_name = os.getenv("DB_NAME")
server_collection = os.getenv("SERVER_COLLECTION", "servers")
plan_collection = os.getenv("PLAN_COLLECTION", "plans")
# Подключение к базе данных и коллекциям
self.client = AsyncIOMotorClient(mongo_uri)
self.db = self.client[database_name]
self.collection = self.db[server_collection] # Коллекция серверов
self.plans_collection = self.db[plan_collection] # Коллекция планов
self.logger = logging.getLogger(__name__)
async def add_subscription_plan(self, plan_data):
"""Добавляет новый тарифный план в коллекцию."""
try:
result = await self.plans_collection.insert_one(plan_data)
self.logger.debug(f"Тарифный план добавлен с ID: {result.inserted_id}")
return result.inserted_id
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def get_subscription_plan(self, plan_name):
"""Получает тарифный план по его имени."""
try:
plan = await self.plans_collection.find_one({"name": plan_name})
if plan:
self.logger.debug(f"Найден тарифный план: {plan}")
else:
self.logger.error(f"Тарифный план {plan_name} не найден.")
return plan
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def add_server(self, server_data):
"""Добавляет новый VPN сервер в коллекцию."""
try:
result = await self.collection.insert_one(server_data)
self.logger.debug(f"VPN сервер добавлен с ID: {result.inserted_id}")
return result.inserted_id
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def get_server(self, server_name: str):
"""Получает сервер VPN по его ID."""
try:
server = await self.collection.find_one({"server.name": server_name})
if server:
self.logger.debug(f"Найден VPN сервер: {server}")
else:
self.logger.debug(f"VPN сервер с ID {server_name} не найден.")
return server
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def get_server_with_least_clients(self):
"""Возвращает сервер с наименьшим количеством подключенных клиентов."""
try:
pipeline = [
{
"$addFields": {
"current_clients": {"$size": {"$ifNull": ["$clients", []]}}
}
},
{
"$sort": {"current_clients": 1}
},
{
"$limit": 1
}
]
result = await self.collection.aggregate(pipeline).to_list(length=1)
if result:
server = result[0]
self.logger.debug(f"Найден сервер с наименьшим количеством клиентов: {server}")
return server
else:
self.logger.debug("Не найдено серверов.")
return None
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def update_server(self, server_name, update_data):
"""Обновляет данные VPN сервера."""
try:
result = await self.collection.update_one({"server_name": server_name}, {"$set": update_data})
if result.matched_count > 0:
self.logger.debug(f"VPN сервер с ID {server_name} обновлен.")
else:
self.logger.debug(f"VPN сервер с ID {server_name} не найден.")
return result.matched_count > 0
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def delete_server(self, server_name):
"""Удаляет VPN сервер по его ID."""
try:
result = await self.collection.delete_one({"name": server_name})
if result.deleted_count > 0:
self.logger.debug(f"VPN сервер с ID {server_name} удален.")
else:
self.logger.debug(f"VPN сервер с ID {server_name} не найден.")
return result.deleted_count > 0
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def list_servers(self):
"""Возвращает список всех VPN серверов."""
try:
servers = await self.collection.find().to_list(length=1000) # Получить до 1000 серверов (можно настроить)
self.logger.debug(f"Найдено {len(servers)} VPN серверов.")
return servers
except DuplicateKeyError:
self.logger.error("Дублирующий ключ.")
except NetworkTimeout:
self.logger.error("Сетевой таймаут.")
async def __aenter__(self):
"""
Метод вызывается при входе в блок with.
"""
self.logger.debug("Контекстный менеджер: подключение открыто.")
return self
async def __aexit__(self, exc_type, exc_value, traceback):
"""
Метод вызывается при выходе из блока with.
"""
await self.close_connection()
if exc_type:
self.logger.error(f"Контекстный менеджер завершён с ошибкой: {exc_value}")
else:
self.logger.debug("Контекстный менеджер: подключение закрыто.")

View File

@@ -1,11 +1,12 @@
from datetime import datetime
from typing import Optional
from uuid import UUID
from sqlalchemy.future import select
from sqlalchemy.exc import SQLAlchemyError
from decimal import Decimal
from sqlalchemy import asc, desc, update
from sqlalchemy.orm import joinedload
from instance.model import TicketMessage, User, Subscription, Transaction,SupportTicket
from instance.model import Referral, User, Subscription, Transaction
class PostgresRepository:
@@ -13,13 +14,13 @@ class PostgresRepository:
self.session_generator = session_generator
self.logger = logger
async def create_user(self, telegram_id: str, username: str, referrer_id: str):
async def create_user(self, telegram_id: int, username: str, invited_by: Optional[int]= None):
"""
Создаёт нового пользователя в PostgreSQL.
"""
async for session in self.session_generator():
try:
new_user = User(telegram_id=telegram_id, username=username, referrer_id=referrer_id)
new_user = User(telegram_id=telegram_id, username=username, invited_by=invited_by)
session.add(new_user)
await session.commit()
return new_user
@@ -28,7 +29,7 @@ class PostgresRepository:
await session.rollback()
return None
async def get_active_subscription(self, telegram_id: str):
async def get_active_subscription(self, telegram_id: int):
"""
Проверяет наличие активной подписки у пользователя.
"""
@@ -46,7 +47,7 @@ class PostgresRepository:
self.logger.error(f"Ошибка проверки активной подписки для пользователя {telegram_id}: {e}")
return None
async def get_user_by_telegram_id(self, telegram_id: str):
async def get_user_by_telegram_id(self, telegram_id: int):
"""
Возвращает пользователя по Telegram ID.
"""
@@ -69,24 +70,24 @@ class PostgresRepository:
:param amount: Сумма для добавления/вычитания.
:return: True, если успешно, иначе False.
"""
self.logger.info(f"Обновление баланса пользователя: id={user.id}, current_balance={user.balance}, amount={amount}")
self.logger.info(f"Обновление баланса пользователя: id={user.telegram_id}, current_balance={user.balance}, amount={amount}")
async for session in self.session_generator():
try:
user = await session.get(User, user.id) # Загружаем пользователя в той же сессии
user = await session.get(User, user.telegram_id) # Загружаем пользователя в той же сессии
if not user:
self.logger.warning(f"Пользователь с ID {user.id} не найден.")
self.logger.warning(f"Пользователь с ID {user.telegram_id} не найден.")
return False
# Приведение amount к Decimal
user.balance += Decimal(amount)
await session.commit()
self.logger.info(f"Баланс пользователя id={user.id} успешно обновлен: new_balance={user.balance}")
self.logger.info(f"Баланс пользователя id={user.telegram_id} успешно обновлен: new_balance={user.balance}")
return True
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при обновлении баланса пользователя id={user.id}: {e}")
self.logger.error(f"Ошибка при обновлении баланса пользователя id={user.telegram_id}: {e}")
await session.rollback()
return False
async def get_last_transactions(self, user_id: UUID, limit: int = 10):
async def get_last_transactions(self, user_telegram_id: int, limit: int = 10):
"""
Возвращает последние транзакции пользователя.
"""
@@ -94,16 +95,16 @@ class PostgresRepository:
try:
result = await session.execute(
select(Transaction)
.where(Transaction.user_id == user_id)
.where(Transaction.user_id == user_telegram_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_telegram_id}: {e}")
return None
async def get_last_subscription_by_user_id(self, user_id: UUID, limit: int = 1):
async def get_last_subscription_by_user_id(self, user_telegram_id: int, limit: int = 1):
"""
Извлекает последнюю подписку пользователя на основании user_id.
@@ -114,15 +115,16 @@ class PostgresRepository:
try:
result = await session.execute(
select(Subscription)
.where(Subscription.user_id == user_id)
.where(Subscription.user_id == user_telegram_id)
.order_by(desc(Subscription.created_at))
.limit(limit)
)
subscriptions = list(result.scalars())
result.scalars()
self.logger.info(f"Найдены такие подписки: {subscriptions}")
return subscriptions
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении подписки для пользователя {user_id}: {e}")
self.logger.error(f"Ошибка при получении подписки для пользователя {user_telegram_id}: {e}")
return None
async def add_record(self, record):
@@ -142,117 +144,58 @@ class PostgresRepository:
await session.rollback()
return None
async def list_active_tickets(self, user_id: UUID):
async def add_referral(self, referrer_id: int, referral_id: int):
"""
Добавление реферальной связи между пользователями.
"""
async for session in self.session_generator():
try:
tickets = await session.execute(
select(SupportTicket)
# Проверить, существует ли уже такая реферальная связь
existing_referral = await session.execute(
select(Referral)
.where(
SupportTicket.user_id == user_id,
SupportTicket.status.in_([status.upper() for status in ["pending", "open"]])
)
(Referral.inviter_id == referrer_id) &
(Referral.invited_id == referral_id)
)
)
result = list(tickets.scalars().all())
self.logger.info(f"Получены активные тикеты: {result}")
return result
except SQLAlchemyError as e:
self.logger.error(f"Произошла ошибка при поиске активных тикетов: {e}")
return None
existing_referral = existing_referral.scalars().first()
if existing_referral:
raise ValueError("Referral relationship already exists")
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)) # Загрузка связанных объектов
select(User).where(User.telegram_id == referrer_id)
)
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_user = await session.execute(
select(User).where(User.telegram_id == referral_id)
)
referral = referral.scalars().first()
if not referral:
referral_user = referral_user.scalars().first()
if not referral_user:
raise ValueError("Referral user not found")
# Добавить реферала в список
referrer.referrals.append(referral)
await session.commit()
except Exception as e:
self.logger(f"Ошибка при добавлении рефералу пользователей")
# Проверить, что пользователь не приглашает сам себя
if referrer_id == referral_id:
raise ValueError("User cannot refer themselves")
# Создать новую реферальную связь
new_referral = Referral(
inviter_id=referrer_id,
invited_id=referral_id
)
session.add(new_referral)
await session.commit()
self.logger.info(f"Реферальная связь создана: {referrer_id} -> {referral_id}")
except Exception as e:
await session.rollback()
self.logger.error(f"Ошибка при добавлении реферальной связи: {str(e)}")
raise

View File

@@ -1,232 +0,0 @@
import aiohttp
import uuid
import json
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):
self.base_url = base_url
self.login_data = login_data
self.logger = logger
self.ssl_context = self._create_ssl_context(certificate, is_encoded)
self.session_id = None
self.headers = None
def _create_ssl_context(self, certificate, is_encoded):
if not certificate:
raise ValueError("Certificate is required.")
try:
ssl_context = ssl.create_default_context()
if is_encoded:
certificate = base64.b64decode(certificate).decode()
ssl_context.load_verify_locations(cadata=certificate)
return ssl_context
except Exception as e:
self.logger.error(f"Error creating SSL context: {e}")
raise ValueError("Invalid certificate format.") from e
async def _ensure_logged_in(self):
if not self.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):
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.ssl_context, 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.")
else:
error_details = await response.text()
self.logger.error(f"Login failed with status {response.status}: {error_details}")
except aiohttp.ClientError as e:
self.logger.exception(f"Login request failed: {e}")
raise
async def get_inbound_info(self, inbound_id: int = 1):
"""
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.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:
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.ssl_context, 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.ssl_context
) 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 = {
"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": 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.ssl_context
) as response:
response_json = await response.json()
if response.status == 200 and response_json.get('success'):
self.logger.info(f"Клиент успешно добавлен: {response_json}")
return "OK"
else:
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