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 = "❌ Ошибка выполнения. Проверь логи, что-то пошло не по чертежу."