Update project configuration and codebase

This commit is contained in:
2026-04-12 12:19:53 +07:00
parent 0ed0ad2b92
commit 0ac8ed8d64
25 changed files with 273 additions and 310 deletions

View File

@@ -1,7 +1,6 @@
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Optional
from datetime import UTC, datetime, timedelta
from uuid import UUID
from remnawave import RemnawaveSDK
@@ -23,9 +22,9 @@ class RWUserInfo:
expire_at: datetime
used_traffic_bytes: float
traffic_limit_bytes: int # 0 = unlimited
hwid_device_limit: Optional[int] # None = unlimited
hwid_device_limit: int | None # None = unlimited
active_squads: list[dict] # [{"uuid": "...", "name": "..."}]
description: Optional[str]
description: str | None
short_uuid: str
@@ -74,7 +73,7 @@ def _parse_user(user_dto) -> RWUserInfo:
async def get_user_by_telegram_id(
sdk: RemnawaveSDK,
telegram_id: int,
) -> Optional[RWUserInfo]:
) -> RWUserInfo | None:
"""
Получить первого пользователя Remnawave, привязанного к Telegram ID.
Возвращает None, если пользователь не найден или Remnawave недоступен.
@@ -115,7 +114,7 @@ async def add_days(
sdk: RemnawaveSDK,
user_uuid: str,
days: int,
) -> Optional[datetime]:
) -> datetime | None:
"""
Добавить N дней к expire_at пользователя.
Возвращает новую дату окончания или None при ошибке.
@@ -131,7 +130,7 @@ async def add_days(
# Обеспечиваем timezone-aware datetime
current_expire = user_dto.expire_at
if current_expire.tzinfo is None:
current_expire = current_expire.replace(tzinfo=timezone.utc)
current_expire = current_expire.replace(tzinfo=UTC)
new_expire = current_expire + timedelta(days=days)
@@ -146,7 +145,7 @@ async def add_days(
new_dt = result.expire_at
if new_dt is not None and new_dt.tzinfo is None:
new_dt = new_dt.replace(tzinfo=timezone.utc)
new_dt = new_dt.replace(tzinfo=UTC)
return new_dt
except Exception as exc:
logger.warning("Failed to add %d days for user %s: %s", days, user_uuid, exc)
@@ -157,7 +156,7 @@ async def remove_days(
sdk: RemnawaveSDK,
user_uuid: str,
days: int,
) -> Optional[datetime]:
) -> datetime | None:
"""
Вычесть N дней из expire_at пользователя.
Возвращает новую дату окончания или None при ошибке.
@@ -170,7 +169,7 @@ async def remove_days(
current_expire = user_dto.expire_at
if current_expire.tzinfo is None:
current_expire = current_expire.replace(tzinfo=timezone.utc)
current_expire = current_expire.replace(tzinfo=UTC)
new_expire = current_expire - timedelta(days=days)
@@ -185,7 +184,7 @@ async def remove_days(
new_dt = result.expire_at
if new_dt is not None and new_dt.tzinfo is None:
new_dt = new_dt.replace(tzinfo=timezone.utc)
new_dt = new_dt.replace(tzinfo=UTC)
return new_dt
except Exception as exc:
logger.warning("Failed to remove %d days for user %s: %s", days, user_uuid, exc)
@@ -203,7 +202,7 @@ async def set_hwid_limit(
Возвращает True при успехе, False при ошибке.
"""
try:
hwid_value: Optional[int] = None if limit == 0 else limit
hwid_value: int | None = None if limit == 0 else limit
await sdk.users.update_user(
UpdateUserRequestDto(
uuid=UUID(user_uuid),

View File

@@ -1,30 +1,28 @@
from typing import Optional
from aiogram import Bot
from aiogram.types import Message, ReactionTypeEmoji
from aiogram.types import User as TelegramUser
from sqlalchemy.ext.asyncio import AsyncSession
from aiogram.types import Message, ReactionTypeEmoji, User as TelegramUser
from remnawave import RemnawaveSDK
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from db.models.tickets import TicketStatus
from db.models.users import User
from misc import utils
from repositories import UserRepository
from repositories.tickets import TicketRepository
from schemas.subscriptions import SubscriptionPlan
from schemas.tickets import NewTicketDTO
from schemas.users import NewUserDTO, TicketCreator
from repositories import UserRepository
from repositories.tickets import TicketRepository
from services.rw import RWUserInfo, get_user_by_telegram_id
from texts import (
ADMIN_STANDARD_FALLBACK,
ADMIN_TICKET_WITH_RW,
ADMIN_TICKET_WITHOUT_RW,
SUPPORT_SIGNATURE,
TICKET_STATUS_EMOJIS,
ADMIN_TICKET_WITHOUT_RW,
)
from misc import utils
class UserServiceException(Exception): ...
class UserServiceError(Exception): ...
class UserService:
@@ -37,8 +35,8 @@ class UserService:
@staticmethod
def format_ticket_name(
creator: TicketCreator,
status: Optional[TicketStatus] = TicketStatus.OPEN,
ticket_id: Optional[int] = None,
status: TicketStatus | None = TicketStatus.OPEN,
ticket_id: int | None = None,
):
return (
TICKET_STATUS_EMOJIS[status.value]
@@ -49,7 +47,7 @@ class UserService:
@staticmethod
async def format_admin_support_message(
ticket_number: int, user: User, rw_user: Optional[RWUserInfo], balance: int
ticket_number: int, user: TelegramUser, rw_user: RWUserInfo | None, balance: int
):
username_display = utils.format_username(user.username)
@@ -65,9 +63,7 @@ class UserService:
status_icon, status_text = utils.format_status(rw_user.status)
expire_date = rw_user.expire_at.strftime("%d.%m.%Y")
days_left = utils.format_days_left(rw_user.expire_at)
used_traffic = utils.format_traffic(
rw_user.used_traffic_bytes, rw_user.traffic_limit_bytes
)
used_traffic = utils.format_traffic(rw_user.used_traffic_bytes, rw_user.traffic_limit_bytes)
hwid_limit = utils.format_hwid_limit(rw_user.hwid_device_limit)
squads = utils.format_squads(rw_user.active_squads)
@@ -92,7 +88,7 @@ class UserService:
session: AsyncSession,
user_id: int,
rw_sdk: RemnawaveSDK,
referal: Optional[int] = None,
referal: int | None = None,
) -> User:
user = await self.user_repository.get_user_by_id(session, user_id)
if user:
@@ -100,9 +96,7 @@ class UserService:
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
link = utils.get_subscription_link(rw_user.short_uuid) if rw_user else None
referal = (
int(referal) if str(referal).isdigit() and user_id != int(referal) else None
)
referal = int(referal) if str(referal).isdigit() and user_id != int(referal) else None
model = NewUserDTO(id=user_id, referal=referal, link=link)
return await self.user_repository.create_user(session, model)
@@ -114,9 +108,7 @@ class UserService:
subscription: SubscriptionPlan,
bot: Bot,
):
ticket = await self.ticket_repository.get_available_ticket_by_user_id(
session, user.id
)
ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id)
db_user = await self.user_repository.get_user_by_id(session, user.id)
if ticket:
@@ -143,15 +135,11 @@ class UserService:
ticket_dto = NewTicketDTO(
creator_id=user.id,
)
ticket = await self.ticket_repository.create_ticket(
session, ticket_data=ticket_dto
)
ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto)
thread = await bot.create_forum_topic(
settings.admin_group_id,
self.format_ticket_name(
creator, ticket_id=ticket.id, status=TicketStatus.PENDING
),
self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING),
)
ticket = await self.ticket_repository.update_ticket_thread_id(
session, ticket, thread.message_thread_id
@@ -166,20 +154,16 @@ class UserService:
full_name=user.full_name,
user_id=user.id,
username=user.username,
balance=db_user,
balance=db_user.balance,
),
)
async def support_forward_message(
self, session: AsyncSession, msg: Message, rw: RemnawaveSDK
):
async def support_forward_message(self, session: AsyncSession, msg: Message, rw: RemnawaveSDK):
user = await self.user_repository.get_user_by_id(session, msg.from_user.id)
if not user:
raise UserServiceException("User %d is not found.", msg.from_user.id)
raise UserServiceError("User %d is not found.", msg.from_user.id)
ticket = await self.ticket_repository.get_available_ticket_by_user_id(
session, user.id
)
ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id)
if ticket:
try:
@@ -199,15 +183,11 @@ class UserService:
ticket_dto = NewTicketDTO(
creator_id=user.id,
)
ticket = await self.ticket_repository.create_ticket(
session, ticket_data=ticket_dto
)
ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto)
thread = await msg.bot.create_forum_topic(
settings.admin_group_id,
self.format_ticket_name(
creator, ticket_id=ticket.id, status=TicketStatus.PENDING
),
self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING),
)
ticket = await self.ticket_repository.update_ticket_thread_id(
session, ticket, thread.message_thread_id
@@ -223,26 +203,18 @@ class UserService:
balance=user.balance,
),
)
await msg.copy_to(
settings.admin_group_id, message_thread_id=thread.message_thread_id
)
await msg.copy_to(settings.admin_group_id, message_thread_id=thread.message_thread_id)
async def support_answer(self, session: AsyncSession, msg: Message):
thread_id = msg.message_thread_id
ticket = await self.ticket_repository.get_ticket_by_thread_id(
session, thread_id=thread_id
)
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
if ticket.status == TicketStatus.PENDING:
await self.ticket_repository.update_ticket_status(
session, ticket, TicketStatus.OPEN
)
await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.OPEN)
await msg.bot.edit_forum_topic(
settings.admin_group_id,
ticket.thread_id,
name=self.format_ticket_name(
TicketCreator(ticket.creator_id), ticket_id=ticket.id
),
name=self.format_ticket_name(TicketCreator(ticket.creator_id), ticket_id=ticket.id),
)
try:
@@ -254,9 +226,7 @@ class UserService:
async def close_ticket(self, session: AsyncSession, msg: Message):
thread_id = msg.message_thread_id
ticket = await self.ticket_repository.get_ticket_by_thread_id(
session, thread_id=thread_id
)
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
await msg.bot.edit_forum_topic(
settings.admin_group_id,
@@ -268,16 +238,12 @@ class UserService:
),
)
await self.ticket_repository.update_ticket_status(
session, ticket, TicketStatus.CLOSED
)
await msg.bot.close_forum_topic(
settings.admin_group_id, message_thread_id=thread_id
)
await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.CLOSED)
await msg.bot.close_forum_topic(settings.admin_group_id, message_thread_id=thread_id)
async def update_user_link(
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
) -> Optional[User]:
) -> User | None:
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
link = utils.get_subscription_link(rw_user.short_uuid)
@@ -286,22 +252,12 @@ class UserService:
return user
async def fetch_user_by_thread(
self, session: AsyncSession, thread_id: int
) -> Optional[User]:
ticket = await self.ticket_repository.get_ticket_by_thread_id(
session, thread_id=thread_id
)
async def fetch_user_by_thread(self, session: AsyncSession, thread_id: int) -> User | None:
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
return await self.user_repository.get_user_by_id(session, ticket.creator_id)
async def fetch_referal_by_thread(
self, session: AsyncSession, thread_id: int
) -> Optional[User]:
ticket = await self.ticket_repository.get_ticket_by_thread_id(
session, thread_id=thread_id
)
async def fetch_referal_by_thread(self, session: AsyncSession, thread_id: int) -> User | None:
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
return await self.user_repository.get_referal_by_user_id(
session, ticket.creator_id
)
return await self.user_repository.get_referal_by_user_id(session, ticket.creator_id)