diff --git a/.python-version b/.python-version deleted file mode 100644 index 24ee5b1..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/config.py b/config.py index 58324cb..a1f1ded 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,6 @@ -from typing import Optional -from pydantic_settings import BaseSettings -from pydantic import Field from dotenv import load_dotenv +from pydantic import Field +from pydantic_settings import BaseSettings load_dotenv(override=True) @@ -9,7 +8,7 @@ load_dotenv(override=True) class Settings(BaseSettings): bot_token: str = Field(alias="BOT_TOKEN") postgres_url: str = Field(alias="POSTGRES_URL") - proxy: Optional[str] = Field(None, alias="PROXY") + proxy: str | None = Field(None, alias="PROXY") admin_group_id: int = Field(alias="ADMIN_GROUP_ID") remnawave_url: str = Field(alias="REMNAWAVE_URL") subscription_url: str = Field(alias="SUBSCRIPTION_URL") diff --git a/db/models/__init__.py b/db/models/__init__.py index 93ed393..57fbb5c 100644 --- a/db/models/__init__.py +++ b/db/models/__init__.py @@ -1,4 +1,4 @@ -from .users import User from .tickets import Ticket +from .users import User -__all__ = ["User", "Ticket"] +__all__ = ["Ticket", "User"] diff --git a/db/models/tickets.py b/db/models/tickets.py index 9490e46..a89972d 100644 --- a/db/models/tickets.py +++ b/db/models/tickets.py @@ -1,5 +1,6 @@ from enum import Enum as VanillaEnum -from sqlalchemy import INTEGER, BIGINT, Enum + +from sqlalchemy import BIGINT, INTEGER, Enum from sqlalchemy.orm import Mapped, mapped_column from db.base import Base diff --git a/db/models/users.py b/db/models/users.py index 8150668..bbc7127 100644 --- a/db/models/users.py +++ b/db/models/users.py @@ -7,9 +7,7 @@ from db.base import Base class User(Base): __tablename__ = "users" - id: Mapped[int] = mapped_column( - BIGINT, nullable=False, unique=True, primary_key=True - ) + id: Mapped[int] = mapped_column(BIGINT, nullable=False, unique=True, primary_key=True) referal_id: Mapped[int] = mapped_column(BIGINT, nullable=True) subscription_link: Mapped[str] = mapped_column(TEXT, nullable=True, unique=True) balance: Mapped[int] = mapped_column(INTEGER, nullable=False, default=0) diff --git a/db/session.py b/db/session.py index 17e273c..a1f7930 100644 --- a/db/session.py +++ b/db/session.py @@ -1,4 +1,4 @@ -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from config import settings diff --git a/handlers/__init__.py b/handlers/__init__.py index dab8038..81254d4 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -1,6 +1,6 @@ -from .referals import router as referal_router -from .menus import router as menus_router -from .support import router as support_router from .buy import router as buy_router +from .menus import router as menus_router +from .referals import router as referal_router +from .support import router as support_router routers = [referal_router, menus_router, support_router, buy_router] diff --git a/handlers/buy.py b/handlers/buy.py index dacc3fe..2b54b5a 100644 --- a/handlers/buy.py +++ b/handlers/buy.py @@ -1,15 +1,16 @@ import logging -from typing import Optional -from aiogram import Router, F -from aiogram.types import CallbackQuery + +from aiogram import F, Router from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery from sqlalchemy.ext.asyncio import AsyncSession +from config import settings from keyboards.client import ( devices_selector, duration_selector, - return_to_menu, payment_gateways, + return_to_menu, ) from misc.utils import ( calculate_price, @@ -17,7 +18,7 @@ from misc.utils import ( ) from schemas.di import DependenciesDTO from schemas.subscriptions import SubscriptionPlan -from services.user_service import UserServiceException +from services.user_service import UserServiceError from states.buy import SubscriptionStorage from texts import ( CALLBACK_FALLBACK, @@ -30,7 +31,6 @@ from texts import ( SUBSCRIPTION_DURATION_SELECTOR, SUPPORT_MSG_SENT, ) -from config import settings router = Router() logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ async def device_count_select(cb: CallbackQuery, state: FSMContext): data = await state.get_data() await state.clear() - ctx: Optional[SubscriptionPlan] = data.get("ctx", SubscriptionPlan()) + ctx: SubscriptionPlan | None = data.get("ctx", SubscriptionPlan()) cb_data = cb.data.split(":") devices = int(cb_data[1]) if cb_data[1].isdigit() else settings.min_devices @@ -75,17 +75,13 @@ async def toggle_whitelists(cb: CallbackQuery, state: FSMContext): data = await state.get_data() await state.clear() - ctx: Optional[SubscriptionPlan] = data.get("ctx", SubscriptionPlan()) - ctx.whitelists = ( - not ctx.whitelists or ctx.devices >= settings.whitelist_device_threshold - ) + ctx: SubscriptionPlan | None = data.get("ctx", SubscriptionPlan()) + ctx.whitelists = not ctx.whitelists or ctx.devices >= settings.whitelist_device_threshold try: await cb.message.edit_text( SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)), - reply_markup=devices_selector( - current=ctx.devices, whitelists=ctx.whitelists - ), + reply_markup=devices_selector(current=ctx.devices, whitelists=ctx.whitelists), ) except Exception as exc: if "message is not modified:" in str(exc): @@ -101,7 +97,7 @@ async def select_duration_init(cb: CallbackQuery, state: FSMContext): data = await state.get_data() await state.clear() - ctx: Optional[SubscriptionPlan] = data.get("ctx") + ctx: SubscriptionPlan | None = data.get("ctx") if not ctx: await cb.answer(CALLBACK_FALLBACK) return @@ -109,9 +105,7 @@ async def select_duration_init(cb: CallbackQuery, state: FSMContext): try: await cb.message.edit_text( SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)), - reply_markup=duration_selector( - current=ctx.duration, back_cb=f"devices:{ctx.devices}" - ), + reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"), ) except Exception as exc: if "message is not modified:" in str(exc): @@ -129,7 +123,7 @@ async def select_duration(cb: CallbackQuery, state: FSMContext): data = await state.get_data() await state.clear() - ctx: Optional[SubscriptionPlan] = data.get("ctx") + ctx: SubscriptionPlan | None = data.get("ctx") if not ctx: await cb.answer(CONTEXT_REINITIATED) ctx = SubscriptionPlan() @@ -140,9 +134,7 @@ async def select_duration(cb: CallbackQuery, state: FSMContext): try: await cb.message.edit_text( SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)), - reply_markup=duration_selector( - current=ctx.duration, back_cb=f"devices:{ctx.devices}" - ), + reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"), ) except Exception as exc: if "message is not modified:" in str(exc): @@ -160,7 +152,7 @@ async def proceed_to_checkout(cb: CallbackQuery, state: FSMContext): data = await state.get_data() await state.clear() - ctx: Optional[SubscriptionPlan] = data.get("ctx") + ctx: SubscriptionPlan | None = data.get("ctx") if not ctx: await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu) return @@ -181,18 +173,16 @@ async def checkout_support( data = await state.get_data() await state.clear() - ctx: Optional[SubscriptionPlan] = data.get("ctx") + ctx: SubscriptionPlan | None = data.get("ctx") if not ctx: await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu) return await cb.message.edit_text(GENERAL_PROCESSING) try: - await deps.user_service.create_subscription_ticket( - session, cb.from_user, ctx, cb.bot - ) + await deps.user_service.create_subscription_ticket(session, cb.from_user, ctx, cb.bot) await cb.message.edit_text(SUPPORT_MSG_SENT) - except UserServiceException: + except UserServiceError: await cb.message.edit_text(STANDARD_FALLBACK) raise except Exception: diff --git a/handlers/menus.py b/handlers/menus.py index 60f6309..635e290 100644 --- a/handlers/menus.py +++ b/handlers/menus.py @@ -1,13 +1,13 @@ -from aiogram import Router, F -from aiogram.filters import CommandStart, CommandObject, Command -from aiogram.types import CallbackQuery, Message +from aiogram import F, Router +from aiogram.filters import Command, CommandObject, CommandStart from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message from sqlalchemy.ext.asyncio import AsyncSession -from misc.utils import format_subscription_link -from schemas.di import DependenciesDTO from keyboards.client import main_menu, return_to_menu -from texts import GENERAL_PROCESSING, build_main_menu_text, FAQ_TEXT +from misc.utils import build_main_menu_text, format_subscription_link +from schemas.di import DependenciesDTO +from texts import FAQ_TEXT, GENERAL_PROCESSING router = Router() @@ -47,9 +47,7 @@ async def standard_start( ): await state.clear() - user = await deps.user_service.add_user( - session, user_id=msg.from_user.id, rw_sdk=deps.rw_sdk - ) + user = await deps.user_service.add_user(session, user_id=msg.from_user.id, rw_sdk=deps.rw_sdk) await msg.reply( build_main_menu_text(format_subscription_link(user.subscription_link)), @@ -77,9 +75,7 @@ async def update_link( await state.clear() await cb.message.edit_text(GENERAL_PROCESSING) - user = await deps.user_service.update_user_link( - session, cb.from_user.id, deps.rw_sdk - ) + user = await deps.user_service.update_user_link(session, cb.from_user.id, deps.rw_sdk) await cb.answer("✅") await cb.message.edit_text( build_main_menu_text(format_subscription_link(user.subscription_link)), diff --git a/handlers/referals.py b/handlers/referals.py index aa6f024..d72b7dd 100644 --- a/handlers/referals.py +++ b/handlers/referals.py @@ -1,15 +1,15 @@ -from aiogram import Router, F +from aiogram import F, Router from aiogram.filters import Command, CommandObject -from aiogram.utils.deep_linking import create_start_link -from aiogram.types import CallbackQuery, Message from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message +from aiogram.utils.deep_linking import create_start_link from sqlalchemy.ext.asyncio import AsyncSession +from config import settings from keyboards.client import referals_kb from misc.utils import format_share_deep_link from schemas.di import DependenciesDTO from texts import GENERAL_PROCESSING, REFERALS_MENU -from config import settings router = Router() @@ -57,7 +57,5 @@ async def refpay( return amount = int(amount) - await deps.user_repository.increase_user_balance(session, user.id, amount) - await status_msg.edit_text( - f"💸 Баланс реферала ({referal.id}) пополнен на {amount}₽" - ) + await deps.user_repository.increase_user_balance(session, referal.id, amount) + await status_msg.edit_text(f"💸 Баланс реферала ({referal.id}) пополнен на {amount}₽") diff --git a/handlers/support.py b/handlers/support.py index ad83715..c28e874 100644 --- a/handlers/support.py +++ b/handlers/support.py @@ -1,16 +1,17 @@ import logging -from aiogram import Router, F + +from aiogram import F, Router from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from sqlalchemy.ext.asyncio import AsyncSession from config import settings +from keyboards.client import return_to_menu from schemas.di import DependenciesDTO -from services.user_service import UserServiceException +from services.user_service import UserServiceError from states.support import SupportStorage from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT -from keyboards.client import return_to_menu router = Router() logger = logging.getLogger(__name__) @@ -34,7 +35,7 @@ async def received_message( try: await deps.user_service.support_forward_message(session, msg, deps.rw_sdk) - except UserServiceException: + except UserServiceError: await msg.reply(STANDARD_FALLBACK) raise except Exception: diff --git a/keyboards/client.py b/keyboards/client.py index 744841a..91e826f 100644 --- a/keyboards/client.py +++ b/keyboards/client.py @@ -24,9 +24,7 @@ _return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton( text="⬅️ Назад", callback_data="menu:main" ) -return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( - [[_return_to_menu_btn]] -).as_markup() +return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder([[_return_to_menu_btn]]).as_markup() payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder( [ @@ -36,9 +34,7 @@ payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder( ).as_markup() -def _return_btn( - data: str = "menu:main", text: str = "⬅️ Назад" -) -> InlineKeyboardButton: +def _return_btn(data: str = "menu:main", text: str = "⬅️ Назад") -> InlineKeyboardButton: return InlineKeyboardButton(text=text, callback_data=data) @@ -88,9 +84,7 @@ def duration_selector( callback_data=f"duration:{val}", ) - builder.row( - InlineKeyboardButton(text="🛒 Перейти к оплате", callback_data="confirm") - ) + builder.row(InlineKeyboardButton(text="🛒 Перейти к оплате", callback_data="confirm")) builder.row(_return_btn(back_cb)) return builder.as_markup() diff --git a/main.py b/main.py index 975ec61..e78705a 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,19 @@ -import logging import asyncio +import logging -from remnawave import RemnawaveSDK from aiogram import Bot, Dispatcher from aiogram.client.default import DefaultBotProperties from aiogram.client.session.aiohttp import AiohttpSession +from remnawave import RemnawaveSDK -from schemas.di import DependenciesDTO +from config import settings +from db.session import async_session from handlers import routers from middlewares import DIMiddleware -from config import settings from repositories.tickets import TicketRepository from repositories.users import UserRepository +from schemas.di import DependenciesDTO from services.user_service import UserService -from db.session import async_session logging.basicConfig(level=logging.DEBUG) diff --git a/middlewares/di.py b/middlewares/di.py index 89219cd..35a7474 100644 --- a/middlewares/di.py +++ b/middlewares/di.py @@ -1,4 +1,6 @@ -from typing import Callable, Awaitable, Any +from collections.abc import Awaitable, Callable +from typing import Any + from aiogram import BaseMiddleware from aiogram.types import TelegramObject from sqlalchemy.ext.asyncio import async_sessionmaker @@ -7,9 +9,7 @@ from schemas.di import DependenciesDTO class DIMiddleware(BaseMiddleware): - def __init__( - self, deps: DependenciesDTO, session_factory: async_sessionmaker - ) -> None: + def __init__(self, deps: DependenciesDTO, session_factory: async_sessionmaker) -> None: self.deps = deps self.session_factory = session_factory diff --git a/misc/utils.py b/misc/utils.py index 4b0e6f0..14d2a3f 100644 --- a/misc/utils.py +++ b/misc/utils.py @@ -1,11 +1,10 @@ import urllib.parse -from typing import Optional +from datetime import UTC, datetime, timedelta from random import choice -from datetime import datetime, timedelta, timezone +import texts from config import settings from schemas.subscriptions import SubscriptionDuration, SubscriptionPlan -import texts QUOTES: list[str] = [ "Верните себе свободный интернет.", @@ -47,12 +46,16 @@ def fetch_quote() -> str: def format_bytes(num_bytes: float) -> str: """Конвертировать байты в читаемый формат: ГБ / МБ / КБ / Б.""" - if num_bytes >= 1_073_741_824: # >= 1 ГБ - return f"{num_bytes / 1_073_741_824:.1f} ГБ" - if num_bytes >= 1_048_576: # >= 1 МБ - return f"{num_bytes / 1_048_576:.1f} МБ" - if num_bytes >= 1_024: # >= 1 КБ - return f"{num_bytes / 1_024:.1f} КБ" + gb_capacity = 1_073_741_824 + mb_capacity = 1_048_576 + kb_capacity = 1_024 + + if num_bytes >= gb_capacity: # >= 1 ГБ + return f"{num_bytes / gb_capacity:.1f} ГБ" + if num_bytes >= mb_capacity: # >= 1 МБ + return f"{num_bytes / mb_capacity:.1f} МБ" + if num_bytes >= kb_capacity: # >= 1 КБ + return f"{num_bytes / kb_capacity:.1f} КБ" return f"{num_bytes:.0f} Б" @@ -69,7 +72,7 @@ def format_traffic(used: float, limit: int) -> str: return texts.TRAFFIC_WITH_LIMIT.format(used=used_str, limit=limit_str) -def format_hwid_limit(limit: Optional[int]) -> str: +def format_hwid_limit(limit: int | None) -> str: """Форматировать лимит HWID: число или «без ограничений».""" if limit is None or limit == 0: return texts.UNLIMITED_HWID @@ -90,11 +93,11 @@ def format_days_left(expire_at: datetime) -> str: - «истекает сегодня» — если осталось 0 дней - «истекла N дн. назад» — если уже истекла """ - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) # Убедимся что expire_at timezone-aware if expire_at.tzinfo is None: - expire_at = expire_at.replace(tzinfo=timezone.utc) + expire_at = expire_at.replace(tzinfo=UTC) delta = expire_at - now days = delta.days # целое, может быть отрицательным @@ -118,12 +121,10 @@ def format_status(status: str) -> tuple[str, str]: "LIMITED": (texts.STATUS_ICON_LIMITED, texts.STATUS_LIMITED), "ON_HOLD": (texts.STATUS_ICON_UNKNOWN, texts.STATUS_UNKNOWN), } - return mapping.get( - status.upper(), (texts.STATUS_ICON_UNKNOWN, texts.STATUS_UNKNOWN) - ) + return mapping.get(status.upper(), (texts.STATUS_ICON_UNKNOWN, texts.STATUS_UNKNOWN)) -def format_username(username: Optional[str]) -> str: +def format_username(username: str | None) -> str: """Форматировать username: «@user» или «нет username».""" if username: return "@" + username @@ -151,14 +152,14 @@ def convert_int_to_duration(value: int) -> SubscriptionDuration: def convert_duration_to_timedelta(duration: SubscriptionDuration) -> timedelta: - MONTH_DELTA = timedelta(days=30) + month_delta = timedelta(days=30) mapping: dict[str, timedelta] = { - SubscriptionDuration.MONTH.value: MONTH_DELTA, - SubscriptionDuration.THREE_MONTHS.value: MONTH_DELTA * 3, - SubscriptionDuration.SIX_MONTHS.value: MONTH_DELTA * 6, - SubscriptionDuration.YEAR.value: MONTH_DELTA * 12, + SubscriptionDuration.MONTH.value: month_delta, + SubscriptionDuration.THREE_MONTHS.value: month_delta * 3, + SubscriptionDuration.SIX_MONTHS.value: month_delta * 6, + SubscriptionDuration.YEAR.value: month_delta * 12, } - return mapping.get(duration.value, MONTH_DELTA) + return mapping.get(duration.value, month_delta) def calculate_price(subscription: SubscriptionPlan): @@ -172,7 +173,7 @@ def calculate_price(subscription: SubscriptionPlan): ) * convert_duration_to_int(subscription.duration) -def format_subscription_link(link: Optional[str] = None): +def format_subscription_link(link: str | None = None): return f"{link or '...'}" @@ -185,7 +186,7 @@ def format_support_subscription( full_name: str, user_id: int, balance: int, - username: Optional[str] = None, + username: str | None = None, ) -> str: username = format_username(username) whitelist_description = "ВЫКЛ" @@ -208,3 +209,7 @@ def format_support_subscription( def get_subscription_link(short_uuid: str): return settings.subscription_url + "/" + short_uuid + + +def build_main_menu_text(link: str) -> str: + return texts.MAIN_MENU.format(quote=fetch_quote(), link=link) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7c9bd03 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[tool.black] +line-length = 100 +target-version = ['py313'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # Исключаем стандартные папки + \.git + | \.venv + | build + | dist + | alembic +)/ +''' + +[tool.ruff] +# Базовые настройки +line-length = 100 +target-version = "py313" + +# Исключаем alembic полностью +exclude = [ + ".git", + ".venv", + "build", + "dist", + "alembic", +] + +[tool.ruff.lint] +select = [ + "E", + "W", + "F", + "I", + "N", + "UP", + "B", + "SIM", + "PL", + "RUF", + "TID", + "PT", +] + +ignore = [ + "E501", + "D100", + "D104", + "G004", + "PLR0913", + "RUF001", + "RUF002", + "RUF003" +] + +[tool.ruff.lint.isort] +combine-as-imports = true \ No newline at end of file diff --git a/repositories/tickets.py b/repositories/tickets.py index 8489eff..aac9af9 100644 --- a/repositories/tickets.py +++ b/repositories/tickets.py @@ -1,4 +1,3 @@ -from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -8,7 +7,7 @@ from schemas.tickets import NewTicketDTO class TicketRepository: async def get_ticket_by_user_id( - self, session: AsyncSession, user_id: int, status: Optional[TicketStatus] = None + self, session: AsyncSession, user_id: int, status: TicketStatus | None = None ) -> list[Ticket]: stmt = select(Ticket).where(Ticket.creator_id == user_id) if status: @@ -19,7 +18,7 @@ class TicketRepository: async def get_available_ticket_by_user_id( self, session: AsyncSession, user_id: int - ) -> Optional[Ticket]: + ) -> Ticket | None: stmt = ( select(Ticket) .where(Ticket.creator_id == user_id) @@ -30,17 +29,13 @@ class TicketRepository: tickets = list(res.scalars().all()) return tickets[0] if tickets else None - async def get_ticket_by_thread_id( - self, session: AsyncSession, thread_id: int - ) -> Optional[Ticket]: + async def get_ticket_by_thread_id(self, session: AsyncSession, thread_id: int) -> Ticket | None: stmt = select(Ticket).where(Ticket.thread_id == thread_id) res = await session.execute(stmt) return res.scalar_one_or_none() - async def create_ticket( - self, session: AsyncSession, ticket_data: NewTicketDTO - ) -> Ticket: + async def create_ticket(self, session: AsyncSession, ticket_data: NewTicketDTO) -> Ticket: ticket = Ticket(creator_id=ticket_data.creator_id) session.add(ticket) await session.commit() @@ -54,9 +49,7 @@ class TicketRepository: await session.commit() return ticket - async def update_ticket_thread_id( - self, session: AsyncSession, ticket: Ticket, thread_id: int - ): + async def update_ticket_thread_id(self, session: AsyncSession, ticket: Ticket, thread_id: int): ticket.thread_id = thread_id await session.commit() return ticket diff --git a/repositories/users.py b/repositories/users.py index 446148f..b270305 100644 --- a/repositories/users.py +++ b/repositories/users.py @@ -1,4 +1,3 @@ -from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -7,9 +6,7 @@ from schemas.users import NewUserDTO class UserRepository: - async def get_user_by_id( - self, session: AsyncSession, user_id: int - ) -> Optional[User]: + async def get_user_by_id(self, session: AsyncSession, user_id: int) -> User | None: stmt = select(User).where(User.id == user_id) result = await session.execute(stmt) @@ -28,7 +25,7 @@ class UserRepository: async def update_user_topic( self, session: AsyncSession, user_id: int, thread_id: int - ) -> Optional[User]: + ) -> User | None: user = await self.get_user_by_id(session, user_id=user_id) if not user: return None @@ -37,9 +34,7 @@ class UserRepository: await session.commit() return user - async def update_user_link( - self, session: AsyncSession, user_id: int, link: str - ) -> Optional[User]: + async def update_user_link(self, session: AsyncSession, user_id: int, link: str) -> User | None: user = await self.get_user_by_id(session, user_id=user_id) if not user: return None @@ -50,7 +45,7 @@ class UserRepository: async def update_user_balance( self, session: AsyncSession, user_id: int, balance: int - ) -> Optional[User]: + ) -> User | None: user = await self.get_user_by_id(session, user_id=user_id) if not user: return None @@ -61,7 +56,7 @@ class UserRepository: async def increase_user_balance( self, session: AsyncSession, user_id: int, amount: int - ) -> Optional[User]: + ) -> User | None: user = await self.get_user_by_id(session, user_id=user_id) if not user: return None diff --git a/schemas/di.py b/schemas/di.py index 17ea5ec..21583c7 100644 --- a/schemas/di.py +++ b/schemas/di.py @@ -1,9 +1,10 @@ from dataclasses import dataclass +from remnawave import RemnawaveSDK + from repositories.tickets import TicketRepository from repositories.users import UserRepository from services.user_service import UserService -from remnawave import RemnawaveSDK @dataclass diff --git a/schemas/users.py b/schemas/users.py index 15f29af..9325034 100644 --- a/schemas/users.py +++ b/schemas/users.py @@ -1,15 +1,14 @@ from dataclasses import dataclass -from typing import Optional @dataclass class NewUserDTO: id: int - referal: Optional[int] = None - link: Optional[str] = None + referal: int | None = None + link: str | None = None @dataclass class TicketCreator: id: int - username: Optional[str] = None + username: str | None = None diff --git a/services/rw.py b/services/rw.py index 70c65b6..34c8e47 100644 --- a/services/rw.py +++ b/services/rw.py @@ -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), diff --git a/services/user_service.py b/services/user_service.py index 969dfec..0791e18 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -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) diff --git a/states/buy.py b/states/buy.py index 7ac22a7..374c4e6 100644 --- a/states/buy.py +++ b/states/buy.py @@ -1,4 +1,4 @@ -from aiogram.fsm.state import StatesGroup, State +from aiogram.fsm.state import State, StatesGroup class SubscriptionStorage(StatesGroup): diff --git a/states/support.py b/states/support.py index 4d7c46f..f95c398 100644 --- a/states/support.py +++ b/states/support.py @@ -1,4 +1,4 @@ -from aiogram.fsm.state import StatesGroup, State +from aiogram.fsm.state import State, StatesGroup class SupportStorage(StatesGroup): diff --git a/texts.py b/texts.py index 969ea18..b6ae8da 100644 --- a/texts.py +++ b/texts.py @@ -1,4 +1,3 @@ -from misc.utils import fetch_quote from db.models.tickets import TicketStatus # ============================================================ @@ -29,9 +28,9 @@ MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧") # ============================================================ TICKET_STATUS_EMOJIS = { - TicketStatus.CLOSED.value: "🔴", - TicketStatus.OPEN.value: "🟢", - TicketStatus.PENDING.value: "🟡", + TicketStatus.CLOSED.value: "🌑", # Более сдержанно + TicketStatus.OPEN.value: "📡", # В эфире + TicketStatus.PENDING.value: "⏳", } @@ -41,17 +40,17 @@ TICKET_STATUS_EMOJIS = { # — Иконки статусов STATUS_ICON_ACTIVE = "🟢" -STATUS_ICON_DISABLED = "🔴" -STATUS_ICON_EXPIRED = "🟡" +STATUS_ICON_DISABLED = "⚪" +STATUS_ICON_EXPIRED = "🔴" STATUS_ICON_LIMITED = "🟠" -STATUS_ICON_UNKNOWN = "⚪" +STATUS_ICON_UNKNOWN = "❓" # — Тексты статусов -STATUS_ACTIVE = "Активна" -STATUS_DISABLED = "Отключена" -STATUS_EXPIRED = "Истекла" -STATUS_LIMITED = "Превышен лимит трафика" -STATUS_UNKNOWN = "Неизвестен" +STATUS_ACTIVE = "работает" +STATUS_DISABLED = "отключена" +STATUS_EXPIRED = "срок вышел" +STATUS_LIMITED = "трафик на нуле" +STATUS_UNKNOWN = "выясняем" # ============================================================ @@ -61,25 +60,25 @@ STATUS_UNKNOWN = "Неизвестен" # — Дни до истечения подписки DAYS_LEFT_POSITIVE = "{days} дн." DAYS_LEFT_EXPIRED = "истекла {days} дн. назад" -DAYS_LEFT_TODAY = "истекает сегодня" +DAYS_LEFT_TODAY = "доживает последний день" # — Username -NO_USERNAME = "нет username" +NO_USERNAME = "скрыт" USERNAME_DISPLAY = "@{username}" # — Internal Squads -NO_SQUADS = "нет" +NO_SQUADS = "отсутствуют" # — HWID лимит -UNLIMITED_HWID = "без ограничений" +UNLIMITED_HWID = "без лимитов" # — Трафик -NO_TRAFFIC_LIMIT = "без ограничений" +NO_TRAFFIC_LIMIT = "не ограничен" TRAFFIC_WITH_LIMIT = "{used} / {limit}" TRAFFIC_NO_LIMIT = "{used}" # — Прочее -NOTHING_PLACEHOLDER = "🍃" +NOTHING_PLACEHOLDER = "∅" # ============================================================ @@ -90,23 +89,23 @@ MAIN_MENU = ( str(MALENIA_LOGO) + " Malenia — {quote}\n\n" + f"{MALENIA_COINS} Личный кабинет\n" - + f"{MALENIA_LINK} Ваша ссылка на подписку: {{link}}" + + f"{MALENIA_LINK} Ключ доступа: {{link}}" ) -FAQ_TEXT = "faq" - -GENERAL_PROCESSING = "⏳ Выполняю..." - -STANDARD_FALLBACK = ( - "❌ Что-то пошло не так, прожмите /start и повторите попытку позже." +FAQ_TEXT = ( + "FAQ: Технический минимум\n\n" + "Здесь нет магии и «инновационных прорывов». Только обфускация трафика и стабильные узлы. " + "Если что-то не грузится — скорее всего, фильтры опять пробуют новые сигнатуры. " + "Мы это уже чиним." ) -CALLBACK_FALLBACK = "❌ Что-то пошло не так" +GENERAL_PROCESSING = "⏳ Проверяем пакеты..." -CONTEXT_REINITIATED = ( - "🔴 Контекст данного взаимодействия сброшен. " - "Данные могут отличаться от указанных вами." -) +STANDARD_FALLBACK = "❌ Ошибка в логике. Прожмите /start. Если не поможет — значит, мы где-то не доглядели за кодом." + +CALLBACK_FALLBACK = "❌ Пакет потерян, попробуйте снова" + +CONTEXT_REINITIATED = "🌑 Сессия сброшена. Данные могли обновиться." # ============================================================ @@ -115,9 +114,10 @@ CONTEXT_REINITIATED = ( REFERALS_MENU = ( "👥 Реферальная система\n\n" - f"{MALENIA_COINS} Ваш реферальный баланс: " + "Никакого сетевого маркетинга, просто делимся ресурсами.\n" + f"{MALENIA_COINS} На вашем счету: " "{balance}₽\n" - f"{MALENIA_LINK} Ваша реферальная ссылка: " + f"{MALENIA_LINK} Ссылка для своих: " "{link}" ) @@ -126,19 +126,15 @@ REFERALS_MENU = ( # ============================================================ SUBSCRIPTION_DEVICE_SELECTOR = ( - "выбери колво устройств епта. тут ещё цена есть смотри опа: {price}" + "Сколько девайсов нужно держать в сети одновременно?\n\nЦена вопроса: {price}₽" ) -SUBSCRIPTION_DURATION_SELECTOR = ( - "тут короче сколько время. а ещё цена смотри ОПА {price}" -) +SUBSCRIPTION_DURATION_SELECTOR = "На какой срок планируем инфраструктуру?\n\nИтого: {price}₽" -WHITELISTS_ALREADY_ON = "уже включены!" - -TEMP_SUPPORT_FORM = "обратитесь в поддержку, прикрепив текст ниже:\n\n" +WHITELISTS_ALREADY_ON = "Списки доступа уже активны." CHECKOUT_PAYMENT_CHOICE = ( - "в поддержку чирканите епта там всё обьяснят\n" + TEMP_SUPPORT_FORM + "Автоплатежи — это не к нам. Стукните в поддержку, там объяснят, куда слать деньги." ) @@ -147,33 +143,29 @@ CHECKOUT_PAYMENT_CHOICE = ( # ============================================================ SUPPORT_INIT = ( - f"{MALENIA_SUPPORT} Что-то не работает? Жалоба? Предложение?\n" - "— всё сюда, желательно со скриншотами. " - "Чем детальнее описание, тем быстрее ответ.\n\n" - "❌ Для отмены — /cancel" + f"{MALENIA_SUPPORT} Технический отдел\n" + "— если интернет превратился в тыкву или есть идеи по конфигам.\n" + "Пишите по делу, скриншоты приветствуются.\n\n" + "🌑 /cancel — вернуться к делам" ) -SUPPORT_MSG_SENT = ( - "⏳ Сообщение отправлено. Для выхода — /cancel, " - "вы можете вернуться сюда из главного меню в любое время." -) +SUPPORT_MSG_SENT = "⏳ Принято. Инженеры посмотрят, как освободятся. " "Вернуться в меню: /cancel" -SUPPORT_SIGNATURE = "✍️ Сообщение от технической поддержки:" +SUPPORT_SIGNATURE = "📡 Ответ от Malenia Team:" # ============================================================ # ШАБЛОНЫ — ОБЩИЕ БЛОКИ # ============================================================ -# Используется в SUPPORT_SUBSCRIPTION и ADMIN_TICKET_WITH_RW _SUPPORT_USER_DESCRIPTION = ( - "👤 Имя: {full_name}\n" - "🔗 Username: {username_display}\n" - "🆔 Telegram ID: {telegram_id}\n\n" - "💰 Реферальный баланс: {balance}" + "👤 Профиль: {full_name}\n" + "🔗 Alias: {username_display}\n" + "🆔 UID: {telegram_id}\n\n" + "💰 Баланс: {balance}" ) -_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" +_DIVIDER = "────────────────────────────\n\n" # ============================================================ @@ -181,14 +173,14 @@ _DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━ # ============================================================ SUPPORT_SUBSCRIPTION = ( - "🎫 ЗАЯВКА НА ПОКУПКУ:\n\n" + "🎫 ВХОДЯЩИЙ ЗАПРОС:\n\n" + _SUPPORT_USER_DESCRIPTION + "\n" + _DIVIDER - + "🗓️ Подписка на {duration} месяцев.\n" - + "📱 Количество устройств: {devices}\n" - + "🏳️ Белые списки: {whitelist_description}\n\n" - + "💰 Итого: {price}₽" + + "🗓️ Срок: {duration} мес.\n" + + "📱 Устройства: {devices}\n" + + "🏳️ Whitelists: {whitelist_description}\n\n" + + "💰 К оплате: {price}₽" ) ADMIN_TICKET_WITH_RW = ( @@ -196,35 +188,24 @@ ADMIN_TICKET_WITH_RW = ( + _SUPPORT_USER_DESCRIPTION + "\n" + _DIVIDER - + "📋 ПОДПИСКА REMNAWAVE\n\n" + + "📊 ТЕХ. ДАННЫЕ (Remnawave)\n\n" + "{status_icon} Статус: {status}\n" - + "📅 Истекает: {expire_date}\n" - + "⏳ Осталось: {days_left}\n" + + "📅 Deadline: {expire_date}\n" + + "⏳ Остаток: {days_left}\n" + "📊 Трафик: {used_traffic}\n" - + "📱 HWID лимит: {hwid_limit}\n" - + "🎯 Internal Squads: {squads}\n\n" + + "📱 HWID: {hwid_limit}\n" + + "🎯 Squads: {squads}\n\n" + _DIVIDER - + "🔑 UUID в Remnawave: {remnawave_uuid}" + + "🔑 UUID: {remnawave_uuid}" ) ADMIN_TICKET_WITHOUT_RW = ( "🎫 ТИКЕТ T-{ticket_number:04d}\n\n" "👤 Имя: {full_name}\n" - "🔗 Username: {username_display}\n" - "🆔 Telegram ID: {telegram_id}\n\n" + "🔗 Alias: {username_display}\n" + "🆔 UID: {telegram_id}\n\n" + _DIVIDER - + "⚠️ Аккаунт в Remnawave не найден" + + "⚠️ Remnawave-профиль отсутствует" ) -ADMIN_STANDARD_FALLBACK = ( - "❌ Что-то пошло не так, обратитесь к логам для подробностей." -) - - -# ============================================================ -# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ -# ============================================================ - - -def build_main_menu_text(link: str) -> str: - return MAIN_MENU.format(quote=fetch_quote(), link=link) +ADMIN_STANDARD_FALLBACK = "❌ Ошибка выполнения. Проверь логи, что-то пошло не по чертежу."