Update project configuration and codebase
This commit is contained in:
@@ -1 +0,0 @@
|
||||
3.13
|
||||
@@ -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")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .users import User
|
||||
from .tickets import Ticket
|
||||
from .users import User
|
||||
|
||||
__all__ = ["User", "Ticket"]
|
||||
__all__ = ["Ticket", "User"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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"<b>💸 Баланс реферала ({referal.id}) пополнен на {amount}₽</b>"
|
||||
)
|
||||
await deps.user_repository.increase_user_balance(session, referal.id, amount)
|
||||
await status_msg.edit_text(f"<b>💸 Баланс реферала ({referal.id}) пополнен на {amount}₽</b>")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
10
main.py
10
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"<code>{link or '...'}</code>"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
58
pyproject.toml
Normal file
58
pyproject.toml
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
|
||||
class SubscriptionStorage(StatesGroup):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
|
||||
class SupportStorage(StatesGroup):
|
||||
|
||||
143
texts.py
143
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)
|
||||
+ " <b>Malenia — <i>{quote}</i></b>\n\n"
|
||||
+ f"<i>{MALENIA_COINS} Личный кабинет</i>\n"
|
||||
+ f"<b>{MALENIA_LINK} Ваша ссылка на подписку: {{link}}</b>"
|
||||
+ f"<b>{MALENIA_LINK} Ключ доступа: <code>{{link}}</code></b>"
|
||||
)
|
||||
|
||||
FAQ_TEXT = "faq"
|
||||
|
||||
GENERAL_PROCESSING = "<i>⏳ Выполняю...</i>"
|
||||
|
||||
STANDARD_FALLBACK = (
|
||||
"<b>❌ Что-то пошло не так</b>, прожмите /start и повторите попытку позже."
|
||||
FAQ_TEXT = (
|
||||
"<b>FAQ: Технический минимум</b>\n\n"
|
||||
"Здесь нет магии и «инновационных прорывов». Только обфускация трафика и стабильные узлы. "
|
||||
"Если что-то не грузится — скорее всего, фильтры опять пробуют новые сигнатуры. "
|
||||
"Мы это уже чиним."
|
||||
)
|
||||
|
||||
CALLBACK_FALLBACK = "❌ Что-то пошло не так"
|
||||
GENERAL_PROCESSING = "<i>⏳ Проверяем пакеты...</i>"
|
||||
|
||||
CONTEXT_REINITIATED = (
|
||||
"🔴 Контекст данного взаимодействия сброшен. "
|
||||
"Данные могут отличаться от указанных вами."
|
||||
)
|
||||
STANDARD_FALLBACK = "<b>❌ Ошибка в логике.</b> Прожмите /start. Если не поможет — значит, мы где-то не доглядели за кодом."
|
||||
|
||||
CALLBACK_FALLBACK = "❌ Пакет потерян, попробуйте снова"
|
||||
|
||||
CONTEXT_REINITIATED = "🌑 Сессия сброшена. Данные могли обновиться."
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -115,9 +114,10 @@ CONTEXT_REINITIATED = (
|
||||
|
||||
REFERALS_MENU = (
|
||||
"<b>👥 Реферальная система</b>\n\n"
|
||||
f"<b>{MALENIA_COINS} Ваш реферальный баланс:</b> "
|
||||
"Никакого сетевого маркетинга, просто делимся ресурсами.\n"
|
||||
f"<b>{MALENIA_COINS} На вашем счету:</b> "
|
||||
"<code>{balance}₽</code>\n"
|
||||
f"<b>{MALENIA_LINK} Ваша реферальная ссылка:</b> "
|
||||
f"<b>{MALENIA_LINK} Ссылка для своих:</b> "
|
||||
"<code>{link}</code>"
|
||||
)
|
||||
|
||||
@@ -126,19 +126,15 @@ REFERALS_MENU = (
|
||||
# ============================================================
|
||||
|
||||
SUBSCRIPTION_DEVICE_SELECTOR = (
|
||||
"выбери колво устройств епта. тут ещё цена есть смотри опа: {price}"
|
||||
"Сколько девайсов нужно держать в сети одновременно?\n\nЦена вопроса: <b>{price}₽</b>"
|
||||
)
|
||||
|
||||
SUBSCRIPTION_DURATION_SELECTOR = (
|
||||
"тут короче сколько время. а ещё цена смотри ОПА {price}"
|
||||
)
|
||||
SUBSCRIPTION_DURATION_SELECTOR = "На какой срок планируем инфраструктуру?\n\nИтого: <b>{price}₽</b>"
|
||||
|
||||
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"<b>{MALENIA_SUPPORT} <i>Что-то не работает? Жалоба? Предложение?</i></b>\n"
|
||||
"— всё сюда, желательно со скриншотами. "
|
||||
"Чем детальнее описание, тем быстрее ответ.\n\n"
|
||||
"<i>❌ Для отмены — /cancel</i>"
|
||||
f"<b>{MALENIA_SUPPORT} Технический отдел</b>\n"
|
||||
"— если интернет превратился в тыкву или есть идеи по конфигам.\n"
|
||||
"Пишите по делу, скриншоты приветствуются.\n\n"
|
||||
"<i>🌑 /cancel — вернуться к делам</i>"
|
||||
)
|
||||
|
||||
SUPPORT_MSG_SENT = (
|
||||
"⏳ Сообщение отправлено. Для выхода — /cancel, "
|
||||
"вы можете вернуться сюда из главного меню в любое время."
|
||||
)
|
||||
SUPPORT_MSG_SENT = "⏳ Принято. Инженеры посмотрят, как освободятся. " "Вернуться в меню: /cancel"
|
||||
|
||||
SUPPORT_SIGNATURE = "<b>✍️ Сообщение от технической поддержки:</b>"
|
||||
SUPPORT_SIGNATURE = "<b>📡 Ответ от Malenia Team:</b>"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ШАБЛОНЫ — ОБЩИЕ БЛОКИ
|
||||
# ============================================================
|
||||
|
||||
# Используется в SUPPORT_SUBSCRIPTION и ADMIN_TICKET_WITH_RW
|
||||
_SUPPORT_USER_DESCRIPTION = (
|
||||
"👤 <b>Имя:</b> {full_name}\n"
|
||||
"🔗 <b>Username:</b> {username_display}\n"
|
||||
"🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n"
|
||||
"💰 <b>Реферальный баланс:</b> <code>{balance}</code>"
|
||||
"👤 <b>Профиль:</b> {full_name}\n"
|
||||
"🔗 <b>Alias:</b> {username_display}\n"
|
||||
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
|
||||
"💰 <b>Баланс:</b> <code>{balance}</code>"
|
||||
)
|
||||
|
||||
_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||
_DIVIDER = "────────────────────────────\n\n"
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -181,14 +173,14 @@ _DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━
|
||||
# ============================================================
|
||||
|
||||
SUPPORT_SUBSCRIPTION = (
|
||||
"<b>🎫 ЗАЯВКА НА ПОКУПКУ:</b>\n\n"
|
||||
"<b>🎫 ВХОДЯЩИЙ ЗАПРОС:</b>\n\n"
|
||||
+ _SUPPORT_USER_DESCRIPTION
|
||||
+ "\n"
|
||||
+ _DIVIDER
|
||||
+ "🗓️ Подписка на <b>{duration}</b> месяцев.\n"
|
||||
+ "📱 Количество устройств: <b>{devices}</b>\n"
|
||||
+ "🏳️ Белые списки: <b>{whitelist_description}</b>\n\n"
|
||||
+ "💰 Итого: <b>{price}₽</b>"
|
||||
+ "🗓️ Срок: <b>{duration} мес.</b>\n"
|
||||
+ "📱 Устройства: <b>{devices}</b>\n"
|
||||
+ "🏳️ Whitelists: <b>{whitelist_description}</b>\n\n"
|
||||
+ "💰 К оплате: <b>{price}₽</b>"
|
||||
)
|
||||
|
||||
ADMIN_TICKET_WITH_RW = (
|
||||
@@ -196,35 +188,24 @@ ADMIN_TICKET_WITH_RW = (
|
||||
+ _SUPPORT_USER_DESCRIPTION
|
||||
+ "\n"
|
||||
+ _DIVIDER
|
||||
+ "📋 <b>ПОДПИСКА REMNAWAVE</b>\n\n"
|
||||
+ "📊 <b>ТЕХ. ДАННЫЕ (Remnawave)</b>\n\n"
|
||||
+ "{status_icon} <b>Статус:</b> {status}\n"
|
||||
+ "📅 <b>Истекает:</b> {expire_date}\n"
|
||||
+ "⏳ <b>Осталось:</b> {days_left}\n"
|
||||
+ "📅 <b>Deadline:</b> {expire_date}\n"
|
||||
+ "⏳ <b>Остаток:</b> {days_left}\n"
|
||||
+ "📊 <b>Трафик:</b> {used_traffic}\n"
|
||||
+ "📱 <b>HWID лимит:</b> {hwid_limit}\n"
|
||||
+ "🎯 <b>Internal Squads:</b> {squads}\n\n"
|
||||
+ "📱 <b>HWID:</b> {hwid_limit}\n"
|
||||
+ "🎯 <b>Squads:</b> {squads}\n\n"
|
||||
+ _DIVIDER
|
||||
+ "🔑 <b>UUID в Remnawave:</b> <code>{remnawave_uuid}</code>"
|
||||
+ "🔑 <b>UUID:</b> <code>{remnawave_uuid}</code>"
|
||||
)
|
||||
|
||||
ADMIN_TICKET_WITHOUT_RW = (
|
||||
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
||||
"👤 <b>Имя:</b> {full_name}\n"
|
||||
"🔗 <b>Username:</b> {username_display}\n"
|
||||
"🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n"
|
||||
"🔗 <b>Alias:</b> {username_display}\n"
|
||||
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
|
||||
+ _DIVIDER
|
||||
+ "⚠️ <b>Аккаунт в Remnawave не найден</b>"
|
||||
+ "⚠️ <b>Remnawave-профиль отсутствует</b>"
|
||||
)
|
||||
|
||||
ADMIN_STANDARD_FALLBACK = (
|
||||
"<b>❌ Что-то пошло не так</b>, обратитесь к логам для подробностей."
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
||||
# ============================================================
|
||||
|
||||
|
||||
def build_main_menu_text(link: str) -> str:
|
||||
return MAIN_MENU.format(quote=fetch_quote(), link=link)
|
||||
ADMIN_STANDARD_FALLBACK = "<b>❌ Ошибка выполнения.</b> Проверь логи, что-то пошло не по чертежу."
|
||||
|
||||
Reference in New Issue
Block a user