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 dotenv import load_dotenv
|
||||||
|
from pydantic import Field
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ load_dotenv(override=True)
|
|||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
bot_token: str = Field(alias="BOT_TOKEN")
|
bot_token: str = Field(alias="BOT_TOKEN")
|
||||||
postgres_url: str = Field(alias="POSTGRES_URL")
|
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")
|
admin_group_id: int = Field(alias="ADMIN_GROUP_ID")
|
||||||
remnawave_url: str = Field(alias="REMNAWAVE_URL")
|
remnawave_url: str = Field(alias="REMNAWAVE_URL")
|
||||||
subscription_url: str = Field(alias="SUBSCRIPTION_URL")
|
subscription_url: str = Field(alias="SUBSCRIPTION_URL")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .users import User
|
|
||||||
from .tickets import Ticket
|
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 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 sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from db.base import Base
|
from db.base import Base
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ from db.base import Base
|
|||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(
|
id: Mapped[int] = mapped_column(BIGINT, nullable=False, unique=True, primary_key=True)
|
||||||
BIGINT, nullable=False, unique=True, primary_key=True
|
|
||||||
)
|
|
||||||
referal_id: Mapped[int] = mapped_column(BIGINT, nullable=True)
|
referal_id: Mapped[int] = mapped_column(BIGINT, nullable=True)
|
||||||
subscription_link: Mapped[str] = mapped_column(TEXT, nullable=True, unique=True)
|
subscription_link: Mapped[str] = mapped_column(TEXT, nullable=True, unique=True)
|
||||||
balance: Mapped[int] = mapped_column(INTEGER, nullable=False, default=0)
|
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
|
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 .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]
|
routers = [referal_router, menus_router, support_router, buy_router]
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.types import CallbackQuery
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config import settings
|
||||||
from keyboards.client import (
|
from keyboards.client import (
|
||||||
devices_selector,
|
devices_selector,
|
||||||
duration_selector,
|
duration_selector,
|
||||||
return_to_menu,
|
|
||||||
payment_gateways,
|
payment_gateways,
|
||||||
|
return_to_menu,
|
||||||
)
|
)
|
||||||
from misc.utils import (
|
from misc.utils import (
|
||||||
calculate_price,
|
calculate_price,
|
||||||
@@ -17,7 +18,7 @@ from misc.utils import (
|
|||||||
)
|
)
|
||||||
from schemas.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from schemas.subscriptions import SubscriptionPlan
|
from schemas.subscriptions import SubscriptionPlan
|
||||||
from services.user_service import UserServiceException
|
from services.user_service import UserServiceError
|
||||||
from states.buy import SubscriptionStorage
|
from states.buy import SubscriptionStorage
|
||||||
from texts import (
|
from texts import (
|
||||||
CALLBACK_FALLBACK,
|
CALLBACK_FALLBACK,
|
||||||
@@ -30,7 +31,6 @@ from texts import (
|
|||||||
SUBSCRIPTION_DURATION_SELECTOR,
|
SUBSCRIPTION_DURATION_SELECTOR,
|
||||||
SUPPORT_MSG_SENT,
|
SUPPORT_MSG_SENT,
|
||||||
)
|
)
|
||||||
from config import settings
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -55,7 +55,7 @@ async def device_count_select(cb: CallbackQuery, state: FSMContext):
|
|||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
ctx: Optional[SubscriptionPlan] = data.get("ctx", SubscriptionPlan())
|
ctx: SubscriptionPlan | None = data.get("ctx", SubscriptionPlan())
|
||||||
|
|
||||||
cb_data = cb.data.split(":")
|
cb_data = cb.data.split(":")
|
||||||
devices = int(cb_data[1]) if cb_data[1].isdigit() else settings.min_devices
|
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()
|
data = await state.get_data()
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
ctx: Optional[SubscriptionPlan] = data.get("ctx", SubscriptionPlan())
|
ctx: SubscriptionPlan | None = data.get("ctx", SubscriptionPlan())
|
||||||
ctx.whitelists = (
|
ctx.whitelists = not ctx.whitelists or ctx.devices >= settings.whitelist_device_threshold
|
||||||
not ctx.whitelists or ctx.devices >= settings.whitelist_device_threshold
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
|
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
reply_markup=devices_selector(
|
reply_markup=devices_selector(current=ctx.devices, whitelists=ctx.whitelists),
|
||||||
current=ctx.devices, whitelists=ctx.whitelists
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if "message is not modified:" in str(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()
|
data = await state.get_data()
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
ctx: Optional[SubscriptionPlan] = data.get("ctx")
|
ctx: SubscriptionPlan | None = data.get("ctx")
|
||||||
if not ctx:
|
if not ctx:
|
||||||
await cb.answer(CALLBACK_FALLBACK)
|
await cb.answer(CALLBACK_FALLBACK)
|
||||||
return
|
return
|
||||||
@@ -109,9 +105,7 @@ async def select_duration_init(cb: CallbackQuery, state: FSMContext):
|
|||||||
try:
|
try:
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
|
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
reply_markup=duration_selector(
|
reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"),
|
||||||
current=ctx.duration, back_cb=f"devices:{ctx.devices}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if "message is not modified:" in str(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()
|
data = await state.get_data()
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
ctx: Optional[SubscriptionPlan] = data.get("ctx")
|
ctx: SubscriptionPlan | None = data.get("ctx")
|
||||||
if not ctx:
|
if not ctx:
|
||||||
await cb.answer(CONTEXT_REINITIATED)
|
await cb.answer(CONTEXT_REINITIATED)
|
||||||
ctx = SubscriptionPlan()
|
ctx = SubscriptionPlan()
|
||||||
@@ -140,9 +134,7 @@ async def select_duration(cb: CallbackQuery, state: FSMContext):
|
|||||||
try:
|
try:
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
|
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
reply_markup=duration_selector(
|
reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"),
|
||||||
current=ctx.duration, back_cb=f"devices:{ctx.devices}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if "message is not modified:" in str(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()
|
data = await state.get_data()
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
ctx: Optional[SubscriptionPlan] = data.get("ctx")
|
ctx: SubscriptionPlan | None = data.get("ctx")
|
||||||
if not ctx:
|
if not ctx:
|
||||||
await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu)
|
await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu)
|
||||||
return
|
return
|
||||||
@@ -181,18 +173,16 @@ async def checkout_support(
|
|||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
ctx: Optional[SubscriptionPlan] = data.get("ctx")
|
ctx: SubscriptionPlan | None = data.get("ctx")
|
||||||
if not ctx:
|
if not ctx:
|
||||||
await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu)
|
await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu)
|
||||||
return
|
return
|
||||||
|
|
||||||
await cb.message.edit_text(GENERAL_PROCESSING)
|
await cb.message.edit_text(GENERAL_PROCESSING)
|
||||||
try:
|
try:
|
||||||
await deps.user_service.create_subscription_ticket(
|
await deps.user_service.create_subscription_ticket(session, cb.from_user, ctx, cb.bot)
|
||||||
session, cb.from_user, ctx, cb.bot
|
|
||||||
)
|
|
||||||
await cb.message.edit_text(SUPPORT_MSG_SENT)
|
await cb.message.edit_text(SUPPORT_MSG_SENT)
|
||||||
except UserServiceException:
|
except UserServiceError:
|
||||||
await cb.message.edit_text(STANDARD_FALLBACK)
|
await cb.message.edit_text(STANDARD_FALLBACK)
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from aiogram import Router, F
|
from aiogram import F, Router
|
||||||
from aiogram.filters import CommandStart, CommandObject, Command
|
from aiogram.filters import Command, CommandObject, CommandStart
|
||||||
from aiogram.types import CallbackQuery, Message
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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 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()
|
router = Router()
|
||||||
|
|
||||||
@@ -47,9 +47,7 @@ async def standard_start(
|
|||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
user = await deps.user_service.add_user(
|
user = await deps.user_service.add_user(session, user_id=msg.from_user.id, rw_sdk=deps.rw_sdk)
|
||||||
session, user_id=msg.from_user.id, rw_sdk=deps.rw_sdk
|
|
||||||
)
|
|
||||||
|
|
||||||
await msg.reply(
|
await msg.reply(
|
||||||
build_main_menu_text(format_subscription_link(user.subscription_link)),
|
build_main_menu_text(format_subscription_link(user.subscription_link)),
|
||||||
@@ -77,9 +75,7 @@ async def update_link(
|
|||||||
await state.clear()
|
await state.clear()
|
||||||
await cb.message.edit_text(GENERAL_PROCESSING)
|
await cb.message.edit_text(GENERAL_PROCESSING)
|
||||||
|
|
||||||
user = await deps.user_service.update_user_link(
|
user = await deps.user_service.update_user_link(session, cb.from_user.id, deps.rw_sdk)
|
||||||
session, cb.from_user.id, deps.rw_sdk
|
|
||||||
)
|
|
||||||
await cb.answer("✅")
|
await cb.answer("✅")
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
build_main_menu_text(format_subscription_link(user.subscription_link)),
|
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.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.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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config import settings
|
||||||
from keyboards.client import referals_kb
|
from keyboards.client import referals_kb
|
||||||
from misc.utils import format_share_deep_link
|
from misc.utils import format_share_deep_link
|
||||||
from schemas.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from texts import GENERAL_PROCESSING, REFERALS_MENU
|
from texts import GENERAL_PROCESSING, REFERALS_MENU
|
||||||
from config import settings
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -57,7 +57,5 @@ async def refpay(
|
|||||||
return
|
return
|
||||||
|
|
||||||
amount = int(amount)
|
amount = int(amount)
|
||||||
await deps.user_repository.increase_user_balance(session, user.id, amount)
|
await deps.user_repository.increase_user_balance(session, referal.id, amount)
|
||||||
await status_msg.edit_text(
|
await status_msg.edit_text(f"<b>💸 Баланс реферала ({referal.id}) пополнен на {amount}₽</b>")
|
||||||
f"<b>💸 Баланс реферала ({referal.id}) пополнен на {amount}₽</b>"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
from aiogram import Router, F
|
|
||||||
|
from aiogram import F, Router
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
|
from keyboards.client import return_to_menu
|
||||||
from schemas.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from services.user_service import UserServiceException
|
from services.user_service import UserServiceError
|
||||||
from states.support import SupportStorage
|
from states.support import SupportStorage
|
||||||
from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT
|
from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT
|
||||||
from keyboards.client import return_to_menu
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -34,7 +35,7 @@ async def received_message(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await deps.user_service.support_forward_message(session, msg, deps.rw_sdk)
|
await deps.user_service.support_forward_message(session, msg, deps.rw_sdk)
|
||||||
except UserServiceException:
|
except UserServiceError:
|
||||||
await msg.reply(STANDARD_FALLBACK)
|
await msg.reply(STANDARD_FALLBACK)
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ _return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton(
|
|||||||
text="⬅️ Назад", callback_data="menu:main"
|
text="⬅️ Назад", callback_data="menu:main"
|
||||||
)
|
)
|
||||||
|
|
||||||
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder([[_return_to_menu_btn]]).as_markup()
|
||||||
[[_return_to_menu_btn]]
|
|
||||||
).as_markup()
|
|
||||||
|
|
||||||
payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
@@ -36,9 +34,7 @@ payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
|||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
def _return_btn(
|
def _return_btn(data: str = "menu:main", text: str = "⬅️ Назад") -> InlineKeyboardButton:
|
||||||
data: str = "menu:main", text: str = "⬅️ Назад"
|
|
||||||
) -> InlineKeyboardButton:
|
|
||||||
return InlineKeyboardButton(text=text, callback_data=data)
|
return InlineKeyboardButton(text=text, callback_data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,9 +84,7 @@ def duration_selector(
|
|||||||
callback_data=f"duration:{val}",
|
callback_data=f"duration:{val}",
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.row(
|
builder.row(InlineKeyboardButton(text="🛒 Перейти к оплате", callback_data="confirm"))
|
||||||
InlineKeyboardButton(text="🛒 Перейти к оплате", callback_data="confirm")
|
|
||||||
)
|
|
||||||
builder.row(_return_btn(back_cb))
|
builder.row(_return_btn(back_cb))
|
||||||
|
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|||||||
10
main.py
10
main.py
@@ -1,19 +1,19 @@
|
|||||||
import logging
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
from remnawave import RemnawaveSDK
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from aiogram.client.session.aiohttp import AiohttpSession
|
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 handlers import routers
|
||||||
from middlewares import DIMiddleware
|
from middlewares import DIMiddleware
|
||||||
from config import settings
|
|
||||||
from repositories.tickets import TicketRepository
|
from repositories.tickets import TicketRepository
|
||||||
from repositories.users import UserRepository
|
from repositories.users import UserRepository
|
||||||
|
from schemas.di import DependenciesDTO
|
||||||
from services.user_service import UserService
|
from services.user_service import UserService
|
||||||
from db.session import async_session
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
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 import BaseMiddleware
|
||||||
from aiogram.types import TelegramObject
|
from aiogram.types import TelegramObject
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
@@ -7,9 +9,7 @@ from schemas.di import DependenciesDTO
|
|||||||
|
|
||||||
|
|
||||||
class DIMiddleware(BaseMiddleware):
|
class DIMiddleware(BaseMiddleware):
|
||||||
def __init__(
|
def __init__(self, deps: DependenciesDTO, session_factory: async_sessionmaker) -> None:
|
||||||
self, deps: DependenciesDTO, session_factory: async_sessionmaker
|
|
||||||
) -> None:
|
|
||||||
self.deps = deps
|
self.deps = deps
|
||||||
self.session_factory = session_factory
|
self.session_factory = session_factory
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Optional
|
from datetime import UTC, datetime, timedelta
|
||||||
from random import choice
|
from random import choice
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
|
|
||||||
|
import texts
|
||||||
from config import settings
|
from config import settings
|
||||||
from schemas.subscriptions import SubscriptionDuration, SubscriptionPlan
|
from schemas.subscriptions import SubscriptionDuration, SubscriptionPlan
|
||||||
import texts
|
|
||||||
|
|
||||||
QUOTES: list[str] = [
|
QUOTES: list[str] = [
|
||||||
"Верните себе свободный интернет.",
|
"Верните себе свободный интернет.",
|
||||||
@@ -47,12 +46,16 @@ def fetch_quote() -> str:
|
|||||||
|
|
||||||
def format_bytes(num_bytes: float) -> str:
|
def format_bytes(num_bytes: float) -> str:
|
||||||
"""Конвертировать байты в читаемый формат: ГБ / МБ / КБ / Б."""
|
"""Конвертировать байты в читаемый формат: ГБ / МБ / КБ / Б."""
|
||||||
if num_bytes >= 1_073_741_824: # >= 1 ГБ
|
gb_capacity = 1_073_741_824
|
||||||
return f"{num_bytes / 1_073_741_824:.1f} ГБ"
|
mb_capacity = 1_048_576
|
||||||
if num_bytes >= 1_048_576: # >= 1 МБ
|
kb_capacity = 1_024
|
||||||
return f"{num_bytes / 1_048_576:.1f} МБ"
|
|
||||||
if num_bytes >= 1_024: # >= 1 КБ
|
if num_bytes >= gb_capacity: # >= 1 ГБ
|
||||||
return f"{num_bytes / 1_024:.1f} КБ"
|
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} Б"
|
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)
|
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: число или «без ограничений»."""
|
"""Форматировать лимит HWID: число или «без ограничений»."""
|
||||||
if limit is None or limit == 0:
|
if limit is None or limit == 0:
|
||||||
return texts.UNLIMITED_HWID
|
return texts.UNLIMITED_HWID
|
||||||
@@ -90,11 +93,11 @@ def format_days_left(expire_at: datetime) -> str:
|
|||||||
- «истекает сегодня» — если осталось 0 дней
|
- «истекает сегодня» — если осталось 0 дней
|
||||||
- «истекла N дн. назад» — если уже истекла
|
- «истекла N дн. назад» — если уже истекла
|
||||||
"""
|
"""
|
||||||
now = datetime.now(tz=timezone.utc)
|
now = datetime.now(tz=UTC)
|
||||||
|
|
||||||
# Убедимся что expire_at timezone-aware
|
# Убедимся что expire_at timezone-aware
|
||||||
if expire_at.tzinfo is None:
|
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
|
delta = expire_at - now
|
||||||
days = delta.days # целое, может быть отрицательным
|
days = delta.days # целое, может быть отрицательным
|
||||||
@@ -118,12 +121,10 @@ def format_status(status: str) -> tuple[str, str]:
|
|||||||
"LIMITED": (texts.STATUS_ICON_LIMITED, texts.STATUS_LIMITED),
|
"LIMITED": (texts.STATUS_ICON_LIMITED, texts.STATUS_LIMITED),
|
||||||
"ON_HOLD": (texts.STATUS_ICON_UNKNOWN, texts.STATUS_UNKNOWN),
|
"ON_HOLD": (texts.STATUS_ICON_UNKNOWN, texts.STATUS_UNKNOWN),
|
||||||
}
|
}
|
||||||
return mapping.get(
|
return mapping.get(status.upper(), (texts.STATUS_ICON_UNKNOWN, texts.STATUS_UNKNOWN))
|
||||||
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»."""
|
"""Форматировать username: «@user» или «нет username»."""
|
||||||
if username:
|
if username:
|
||||||
return "@" + username
|
return "@" + username
|
||||||
@@ -151,14 +152,14 @@ def convert_int_to_duration(value: int) -> SubscriptionDuration:
|
|||||||
|
|
||||||
|
|
||||||
def convert_duration_to_timedelta(duration: SubscriptionDuration) -> timedelta:
|
def convert_duration_to_timedelta(duration: SubscriptionDuration) -> timedelta:
|
||||||
MONTH_DELTA = timedelta(days=30)
|
month_delta = timedelta(days=30)
|
||||||
mapping: dict[str, timedelta] = {
|
mapping: dict[str, timedelta] = {
|
||||||
SubscriptionDuration.MONTH.value: MONTH_DELTA,
|
SubscriptionDuration.MONTH.value: month_delta,
|
||||||
SubscriptionDuration.THREE_MONTHS.value: MONTH_DELTA * 3,
|
SubscriptionDuration.THREE_MONTHS.value: month_delta * 3,
|
||||||
SubscriptionDuration.SIX_MONTHS.value: MONTH_DELTA * 6,
|
SubscriptionDuration.SIX_MONTHS.value: month_delta * 6,
|
||||||
SubscriptionDuration.YEAR.value: MONTH_DELTA * 12,
|
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):
|
def calculate_price(subscription: SubscriptionPlan):
|
||||||
@@ -172,7 +173,7 @@ def calculate_price(subscription: SubscriptionPlan):
|
|||||||
) * convert_duration_to_int(subscription.duration)
|
) * 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>"
|
return f"<code>{link or '...'}</code>"
|
||||||
|
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ def format_support_subscription(
|
|||||||
full_name: str,
|
full_name: str,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
balance: int,
|
balance: int,
|
||||||
username: Optional[str] = None,
|
username: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
username = format_username(username)
|
username = format_username(username)
|
||||||
whitelist_description = "ВЫКЛ"
|
whitelist_description = "ВЫКЛ"
|
||||||
@@ -208,3 +209,7 @@ def format_support_subscription(
|
|||||||
|
|
||||||
def get_subscription_link(short_uuid: str):
|
def get_subscription_link(short_uuid: str):
|
||||||
return settings.subscription_url + "/" + short_uuid
|
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 import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -8,7 +7,7 @@ from schemas.tickets import NewTicketDTO
|
|||||||
|
|
||||||
class TicketRepository:
|
class TicketRepository:
|
||||||
async def get_ticket_by_user_id(
|
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]:
|
) -> list[Ticket]:
|
||||||
stmt = select(Ticket).where(Ticket.creator_id == user_id)
|
stmt = select(Ticket).where(Ticket.creator_id == user_id)
|
||||||
if status:
|
if status:
|
||||||
@@ -19,7 +18,7 @@ class TicketRepository:
|
|||||||
|
|
||||||
async def get_available_ticket_by_user_id(
|
async def get_available_ticket_by_user_id(
|
||||||
self, session: AsyncSession, user_id: int
|
self, session: AsyncSession, user_id: int
|
||||||
) -> Optional[Ticket]:
|
) -> Ticket | None:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Ticket)
|
select(Ticket)
|
||||||
.where(Ticket.creator_id == user_id)
|
.where(Ticket.creator_id == user_id)
|
||||||
@@ -30,17 +29,13 @@ class TicketRepository:
|
|||||||
tickets = list(res.scalars().all())
|
tickets = list(res.scalars().all())
|
||||||
return tickets[0] if tickets else None
|
return tickets[0] if tickets else None
|
||||||
|
|
||||||
async def get_ticket_by_thread_id(
|
async def get_ticket_by_thread_id(self, session: AsyncSession, thread_id: int) -> Ticket | None:
|
||||||
self, session: AsyncSession, thread_id: int
|
|
||||||
) -> Optional[Ticket]:
|
|
||||||
stmt = select(Ticket).where(Ticket.thread_id == thread_id)
|
stmt = select(Ticket).where(Ticket.thread_id == thread_id)
|
||||||
res = await session.execute(stmt)
|
res = await session.execute(stmt)
|
||||||
|
|
||||||
return res.scalar_one_or_none()
|
return res.scalar_one_or_none()
|
||||||
|
|
||||||
async def create_ticket(
|
async def create_ticket(self, session: AsyncSession, ticket_data: NewTicketDTO) -> Ticket:
|
||||||
self, session: AsyncSession, ticket_data: NewTicketDTO
|
|
||||||
) -> Ticket:
|
|
||||||
ticket = Ticket(creator_id=ticket_data.creator_id)
|
ticket = Ticket(creator_id=ticket_data.creator_id)
|
||||||
session.add(ticket)
|
session.add(ticket)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -54,9 +49,7 @@ class TicketRepository:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
async def update_ticket_thread_id(
|
async def update_ticket_thread_id(self, session: AsyncSession, ticket: Ticket, thread_id: int):
|
||||||
self, session: AsyncSession, ticket: Ticket, thread_id: int
|
|
||||||
):
|
|
||||||
ticket.thread_id = thread_id
|
ticket.thread_id = thread_id
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return ticket
|
return ticket
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -7,9 +6,7 @@ from schemas.users import NewUserDTO
|
|||||||
|
|
||||||
|
|
||||||
class UserRepository:
|
class UserRepository:
|
||||||
async def get_user_by_id(
|
async def get_user_by_id(self, session: AsyncSession, user_id: int) -> User | None:
|
||||||
self, session: AsyncSession, user_id: int
|
|
||||||
) -> Optional[User]:
|
|
||||||
stmt = select(User).where(User.id == user_id)
|
stmt = select(User).where(User.id == user_id)
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
|
|
||||||
@@ -28,7 +25,7 @@ class UserRepository:
|
|||||||
|
|
||||||
async def update_user_topic(
|
async def update_user_topic(
|
||||||
self, session: AsyncSession, user_id: int, thread_id: int
|
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)
|
user = await self.get_user_by_id(session, user_id=user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
@@ -37,9 +34,7 @@ class UserRepository:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def update_user_link(
|
async def update_user_link(self, session: AsyncSession, user_id: int, link: str) -> User | None:
|
||||||
self, session: AsyncSession, user_id: int, link: str
|
|
||||||
) -> Optional[User]:
|
|
||||||
user = await self.get_user_by_id(session, user_id=user_id)
|
user = await self.get_user_by_id(session, user_id=user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
@@ -50,7 +45,7 @@ class UserRepository:
|
|||||||
|
|
||||||
async def update_user_balance(
|
async def update_user_balance(
|
||||||
self, session: AsyncSession, user_id: int, balance: int
|
self, session: AsyncSession, user_id: int, balance: int
|
||||||
) -> Optional[User]:
|
) -> User | None:
|
||||||
user = await self.get_user_by_id(session, user_id=user_id)
|
user = await self.get_user_by_id(session, user_id=user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
@@ -61,7 +56,7 @@ class UserRepository:
|
|||||||
|
|
||||||
async def increase_user_balance(
|
async def increase_user_balance(
|
||||||
self, session: AsyncSession, user_id: int, amount: int
|
self, session: AsyncSession, user_id: int, amount: int
|
||||||
) -> Optional[User]:
|
) -> User | None:
|
||||||
user = await self.get_user_by_id(session, user_id=user_id)
|
user = await self.get_user_by_id(session, user_id=user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from remnawave import RemnawaveSDK
|
||||||
|
|
||||||
from repositories.tickets import TicketRepository
|
from repositories.tickets import TicketRepository
|
||||||
from repositories.users import UserRepository
|
from repositories.users import UserRepository
|
||||||
from services.user_service import UserService
|
from services.user_service import UserService
|
||||||
from remnawave import RemnawaveSDK
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NewUserDTO:
|
class NewUserDTO:
|
||||||
id: int
|
id: int
|
||||||
referal: Optional[int] = None
|
referal: int | None = None
|
||||||
link: Optional[str] = None
|
link: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TicketCreator:
|
class TicketCreator:
|
||||||
id: int
|
id: int
|
||||||
username: Optional[str] = None
|
username: str | None = None
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from remnawave import RemnawaveSDK
|
from remnawave import RemnawaveSDK
|
||||||
@@ -23,9 +22,9 @@ class RWUserInfo:
|
|||||||
expire_at: datetime
|
expire_at: datetime
|
||||||
used_traffic_bytes: float
|
used_traffic_bytes: float
|
||||||
traffic_limit_bytes: int # 0 = unlimited
|
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": "..."}]
|
active_squads: list[dict] # [{"uuid": "...", "name": "..."}]
|
||||||
description: Optional[str]
|
description: str | None
|
||||||
short_uuid: str
|
short_uuid: str
|
||||||
|
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ def _parse_user(user_dto) -> RWUserInfo:
|
|||||||
async def get_user_by_telegram_id(
|
async def get_user_by_telegram_id(
|
||||||
sdk: RemnawaveSDK,
|
sdk: RemnawaveSDK,
|
||||||
telegram_id: int,
|
telegram_id: int,
|
||||||
) -> Optional[RWUserInfo]:
|
) -> RWUserInfo | None:
|
||||||
"""
|
"""
|
||||||
Получить первого пользователя Remnawave, привязанного к Telegram ID.
|
Получить первого пользователя Remnawave, привязанного к Telegram ID.
|
||||||
Возвращает None, если пользователь не найден или Remnawave недоступен.
|
Возвращает None, если пользователь не найден или Remnawave недоступен.
|
||||||
@@ -115,7 +114,7 @@ async def add_days(
|
|||||||
sdk: RemnawaveSDK,
|
sdk: RemnawaveSDK,
|
||||||
user_uuid: str,
|
user_uuid: str,
|
||||||
days: int,
|
days: int,
|
||||||
) -> Optional[datetime]:
|
) -> datetime | None:
|
||||||
"""
|
"""
|
||||||
Добавить N дней к expire_at пользователя.
|
Добавить N дней к expire_at пользователя.
|
||||||
Возвращает новую дату окончания или None при ошибке.
|
Возвращает новую дату окончания или None при ошибке.
|
||||||
@@ -131,7 +130,7 @@ async def add_days(
|
|||||||
# Обеспечиваем timezone-aware datetime
|
# Обеспечиваем timezone-aware datetime
|
||||||
current_expire = user_dto.expire_at
|
current_expire = user_dto.expire_at
|
||||||
if current_expire.tzinfo is None:
|
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)
|
new_expire = current_expire + timedelta(days=days)
|
||||||
|
|
||||||
@@ -146,7 +145,7 @@ async def add_days(
|
|||||||
|
|
||||||
new_dt = result.expire_at
|
new_dt = result.expire_at
|
||||||
if new_dt is not None and new_dt.tzinfo is None:
|
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
|
return new_dt
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to add %d days for user %s: %s", days, user_uuid, 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,
|
sdk: RemnawaveSDK,
|
||||||
user_uuid: str,
|
user_uuid: str,
|
||||||
days: int,
|
days: int,
|
||||||
) -> Optional[datetime]:
|
) -> datetime | None:
|
||||||
"""
|
"""
|
||||||
Вычесть N дней из expire_at пользователя.
|
Вычесть N дней из expire_at пользователя.
|
||||||
Возвращает новую дату окончания или None при ошибке.
|
Возвращает новую дату окончания или None при ошибке.
|
||||||
@@ -170,7 +169,7 @@ async def remove_days(
|
|||||||
|
|
||||||
current_expire = user_dto.expire_at
|
current_expire = user_dto.expire_at
|
||||||
if current_expire.tzinfo is None:
|
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)
|
new_expire = current_expire - timedelta(days=days)
|
||||||
|
|
||||||
@@ -185,7 +184,7 @@ async def remove_days(
|
|||||||
|
|
||||||
new_dt = result.expire_at
|
new_dt = result.expire_at
|
||||||
if new_dt is not None and new_dt.tzinfo is None:
|
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
|
return new_dt
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to remove %d days for user %s: %s", days, user_uuid, 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 при ошибке.
|
Возвращает True при успехе, False при ошибке.
|
||||||
"""
|
"""
|
||||||
try:
|
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(
|
await sdk.users.update_user(
|
||||||
UpdateUserRequestDto(
|
UpdateUserRequestDto(
|
||||||
uuid=UUID(user_uuid),
|
uuid=UUID(user_uuid),
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
from typing import Optional
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.types import Message, ReactionTypeEmoji
|
from aiogram.types import Message, ReactionTypeEmoji, User as TelegramUser
|
||||||
from aiogram.types import User as TelegramUser
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from remnawave import RemnawaveSDK
|
from remnawave import RemnawaveSDK
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
from db.models.tickets import TicketStatus
|
from db.models.tickets import TicketStatus
|
||||||
from db.models.users import User
|
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.subscriptions import SubscriptionPlan
|
||||||
from schemas.tickets import NewTicketDTO
|
from schemas.tickets import NewTicketDTO
|
||||||
from schemas.users import NewUserDTO, TicketCreator
|
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 services.rw import RWUserInfo, get_user_by_telegram_id
|
||||||
from texts import (
|
from texts import (
|
||||||
ADMIN_STANDARD_FALLBACK,
|
ADMIN_STANDARD_FALLBACK,
|
||||||
ADMIN_TICKET_WITH_RW,
|
ADMIN_TICKET_WITH_RW,
|
||||||
|
ADMIN_TICKET_WITHOUT_RW,
|
||||||
SUPPORT_SIGNATURE,
|
SUPPORT_SIGNATURE,
|
||||||
TICKET_STATUS_EMOJIS,
|
TICKET_STATUS_EMOJIS,
|
||||||
ADMIN_TICKET_WITHOUT_RW,
|
|
||||||
)
|
)
|
||||||
from misc import utils
|
|
||||||
|
|
||||||
|
|
||||||
class UserServiceException(Exception): ...
|
class UserServiceError(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
class UserService:
|
||||||
@@ -37,8 +35,8 @@ class UserService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def format_ticket_name(
|
def format_ticket_name(
|
||||||
creator: TicketCreator,
|
creator: TicketCreator,
|
||||||
status: Optional[TicketStatus] = TicketStatus.OPEN,
|
status: TicketStatus | None = TicketStatus.OPEN,
|
||||||
ticket_id: Optional[int] = None,
|
ticket_id: int | None = None,
|
||||||
):
|
):
|
||||||
return (
|
return (
|
||||||
TICKET_STATUS_EMOJIS[status.value]
|
TICKET_STATUS_EMOJIS[status.value]
|
||||||
@@ -49,7 +47,7 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def format_admin_support_message(
|
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)
|
username_display = utils.format_username(user.username)
|
||||||
|
|
||||||
@@ -65,9 +63,7 @@ class UserService:
|
|||||||
status_icon, status_text = utils.format_status(rw_user.status)
|
status_icon, status_text = utils.format_status(rw_user.status)
|
||||||
expire_date = rw_user.expire_at.strftime("%d.%m.%Y")
|
expire_date = rw_user.expire_at.strftime("%d.%m.%Y")
|
||||||
days_left = utils.format_days_left(rw_user.expire_at)
|
days_left = utils.format_days_left(rw_user.expire_at)
|
||||||
used_traffic = utils.format_traffic(
|
used_traffic = utils.format_traffic(rw_user.used_traffic_bytes, rw_user.traffic_limit_bytes)
|
||||||
rw_user.used_traffic_bytes, rw_user.traffic_limit_bytes
|
|
||||||
)
|
|
||||||
hwid_limit = utils.format_hwid_limit(rw_user.hwid_device_limit)
|
hwid_limit = utils.format_hwid_limit(rw_user.hwid_device_limit)
|
||||||
squads = utils.format_squads(rw_user.active_squads)
|
squads = utils.format_squads(rw_user.active_squads)
|
||||||
|
|
||||||
@@ -92,7 +88,7 @@ class UserService:
|
|||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
rw_sdk: RemnawaveSDK,
|
rw_sdk: RemnawaveSDK,
|
||||||
referal: Optional[int] = None,
|
referal: int | None = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
user = await self.user_repository.get_user_by_id(session, user_id)
|
user = await self.user_repository.get_user_by_id(session, user_id)
|
||||||
if user:
|
if user:
|
||||||
@@ -100,9 +96,7 @@ class UserService:
|
|||||||
|
|
||||||
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
|
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
|
link = utils.get_subscription_link(rw_user.short_uuid) if rw_user else None
|
||||||
referal = (
|
referal = int(referal) if str(referal).isdigit() and user_id != int(referal) else None
|
||||||
int(referal) if str(referal).isdigit() and user_id != int(referal) else None
|
|
||||||
)
|
|
||||||
|
|
||||||
model = NewUserDTO(id=user_id, referal=referal, link=link)
|
model = NewUserDTO(id=user_id, referal=referal, link=link)
|
||||||
return await self.user_repository.create_user(session, model)
|
return await self.user_repository.create_user(session, model)
|
||||||
@@ -114,9 +108,7 @@ class UserService:
|
|||||||
subscription: SubscriptionPlan,
|
subscription: SubscriptionPlan,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
):
|
):
|
||||||
ticket = await self.ticket_repository.get_available_ticket_by_user_id(
|
ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id)
|
||||||
session, user.id
|
|
||||||
)
|
|
||||||
db_user = await self.user_repository.get_user_by_id(session, user.id)
|
db_user = await self.user_repository.get_user_by_id(session, user.id)
|
||||||
|
|
||||||
if ticket:
|
if ticket:
|
||||||
@@ -143,15 +135,11 @@ class UserService:
|
|||||||
ticket_dto = NewTicketDTO(
|
ticket_dto = NewTicketDTO(
|
||||||
creator_id=user.id,
|
creator_id=user.id,
|
||||||
)
|
)
|
||||||
ticket = await self.ticket_repository.create_ticket(
|
ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto)
|
||||||
session, ticket_data=ticket_dto
|
|
||||||
)
|
|
||||||
|
|
||||||
thread = await bot.create_forum_topic(
|
thread = await bot.create_forum_topic(
|
||||||
settings.admin_group_id,
|
settings.admin_group_id,
|
||||||
self.format_ticket_name(
|
self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING),
|
||||||
creator, ticket_id=ticket.id, status=TicketStatus.PENDING
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
ticket = await self.ticket_repository.update_ticket_thread_id(
|
ticket = await self.ticket_repository.update_ticket_thread_id(
|
||||||
session, ticket, thread.message_thread_id
|
session, ticket, thread.message_thread_id
|
||||||
@@ -166,20 +154,16 @@ class UserService:
|
|||||||
full_name=user.full_name,
|
full_name=user.full_name,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
balance=db_user,
|
balance=db_user.balance,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def support_forward_message(
|
async def support_forward_message(self, session: AsyncSession, msg: Message, rw: RemnawaveSDK):
|
||||||
self, session: AsyncSession, msg: Message, rw: RemnawaveSDK
|
|
||||||
):
|
|
||||||
user = await self.user_repository.get_user_by_id(session, msg.from_user.id)
|
user = await self.user_repository.get_user_by_id(session, msg.from_user.id)
|
||||||
if not user:
|
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(
|
ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id)
|
||||||
session, user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if ticket:
|
if ticket:
|
||||||
try:
|
try:
|
||||||
@@ -199,15 +183,11 @@ class UserService:
|
|||||||
ticket_dto = NewTicketDTO(
|
ticket_dto = NewTicketDTO(
|
||||||
creator_id=user.id,
|
creator_id=user.id,
|
||||||
)
|
)
|
||||||
ticket = await self.ticket_repository.create_ticket(
|
ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto)
|
||||||
session, ticket_data=ticket_dto
|
|
||||||
)
|
|
||||||
|
|
||||||
thread = await msg.bot.create_forum_topic(
|
thread = await msg.bot.create_forum_topic(
|
||||||
settings.admin_group_id,
|
settings.admin_group_id,
|
||||||
self.format_ticket_name(
|
self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING),
|
||||||
creator, ticket_id=ticket.id, status=TicketStatus.PENDING
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
ticket = await self.ticket_repository.update_ticket_thread_id(
|
ticket = await self.ticket_repository.update_ticket_thread_id(
|
||||||
session, ticket, thread.message_thread_id
|
session, ticket, thread.message_thread_id
|
||||||
@@ -223,26 +203,18 @@ class UserService:
|
|||||||
balance=user.balance,
|
balance=user.balance,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await msg.copy_to(
|
await msg.copy_to(settings.admin_group_id, message_thread_id=thread.message_thread_id)
|
||||||
settings.admin_group_id, message_thread_id=thread.message_thread_id
|
|
||||||
)
|
|
||||||
|
|
||||||
async def support_answer(self, session: AsyncSession, msg: Message):
|
async def support_answer(self, session: AsyncSession, msg: Message):
|
||||||
thread_id = msg.message_thread_id
|
thread_id = msg.message_thread_id
|
||||||
ticket = await self.ticket_repository.get_ticket_by_thread_id(
|
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
|
||||||
session, thread_id=thread_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if ticket.status == TicketStatus.PENDING:
|
if ticket.status == TicketStatus.PENDING:
|
||||||
await self.ticket_repository.update_ticket_status(
|
await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.OPEN)
|
||||||
session, ticket, TicketStatus.OPEN
|
|
||||||
)
|
|
||||||
await msg.bot.edit_forum_topic(
|
await msg.bot.edit_forum_topic(
|
||||||
settings.admin_group_id,
|
settings.admin_group_id,
|
||||||
ticket.thread_id,
|
ticket.thread_id,
|
||||||
name=self.format_ticket_name(
|
name=self.format_ticket_name(TicketCreator(ticket.creator_id), ticket_id=ticket.id),
|
||||||
TicketCreator(ticket.creator_id), ticket_id=ticket.id
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -254,9 +226,7 @@ class UserService:
|
|||||||
|
|
||||||
async def close_ticket(self, session: AsyncSession, msg: Message):
|
async def close_ticket(self, session: AsyncSession, msg: Message):
|
||||||
thread_id = msg.message_thread_id
|
thread_id = msg.message_thread_id
|
||||||
ticket = await self.ticket_repository.get_ticket_by_thread_id(
|
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
|
||||||
session, thread_id=thread_id
|
|
||||||
)
|
|
||||||
|
|
||||||
await msg.bot.edit_forum_topic(
|
await msg.bot.edit_forum_topic(
|
||||||
settings.admin_group_id,
|
settings.admin_group_id,
|
||||||
@@ -268,16 +238,12 @@ class UserService:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.ticket_repository.update_ticket_status(
|
await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.CLOSED)
|
||||||
session, ticket, TicketStatus.CLOSED
|
await msg.bot.close_forum_topic(settings.admin_group_id, message_thread_id=thread_id)
|
||||||
)
|
|
||||||
await msg.bot.close_forum_topic(
|
|
||||||
settings.admin_group_id, message_thread_id=thread_id
|
|
||||||
)
|
|
||||||
|
|
||||||
async def update_user_link(
|
async def update_user_link(
|
||||||
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
|
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)
|
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
|
||||||
|
|
||||||
link = utils.get_subscription_link(rw_user.short_uuid)
|
link = utils.get_subscription_link(rw_user.short_uuid)
|
||||||
@@ -286,22 +252,12 @@ class UserService:
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def fetch_user_by_thread(
|
async def fetch_user_by_thread(self, session: AsyncSession, thread_id: int) -> User | None:
|
||||||
self, session: AsyncSession, thread_id: int
|
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
|
||||||
) -> Optional[User]:
|
|
||||||
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)
|
return await self.user_repository.get_user_by_id(session, ticket.creator_id)
|
||||||
|
|
||||||
async def fetch_referal_by_thread(
|
async def fetch_referal_by_thread(self, session: AsyncSession, thread_id: int) -> User | None:
|
||||||
self, session: AsyncSession, thread_id: int
|
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
|
||||||
) -> Optional[User]:
|
|
||||||
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(
|
return await self.user_repository.get_referal_by_user_id(session, ticket.creator_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):
|
class SubscriptionStorage(StatesGroup):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from aiogram.fsm.state import StatesGroup, State
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
|
||||||
class SupportStorage(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
|
from db.models.tickets import TicketStatus
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -29,9 +28,9 @@ MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧")
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
TICKET_STATUS_EMOJIS = {
|
TICKET_STATUS_EMOJIS = {
|
||||||
TicketStatus.CLOSED.value: "🔴",
|
TicketStatus.CLOSED.value: "🌑", # Более сдержанно
|
||||||
TicketStatus.OPEN.value: "🟢",
|
TicketStatus.OPEN.value: "📡", # В эфире
|
||||||
TicketStatus.PENDING.value: "🟡",
|
TicketStatus.PENDING.value: "⏳",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -41,17 +40,17 @@ TICKET_STATUS_EMOJIS = {
|
|||||||
|
|
||||||
# — Иконки статусов
|
# — Иконки статусов
|
||||||
STATUS_ICON_ACTIVE = "🟢"
|
STATUS_ICON_ACTIVE = "🟢"
|
||||||
STATUS_ICON_DISABLED = "🔴"
|
STATUS_ICON_DISABLED = "⚪"
|
||||||
STATUS_ICON_EXPIRED = "🟡"
|
STATUS_ICON_EXPIRED = "🔴"
|
||||||
STATUS_ICON_LIMITED = "🟠"
|
STATUS_ICON_LIMITED = "🟠"
|
||||||
STATUS_ICON_UNKNOWN = "⚪"
|
STATUS_ICON_UNKNOWN = "❓"
|
||||||
|
|
||||||
# — Тексты статусов
|
# — Тексты статусов
|
||||||
STATUS_ACTIVE = "Активна"
|
STATUS_ACTIVE = "работает"
|
||||||
STATUS_DISABLED = "Отключена"
|
STATUS_DISABLED = "отключена"
|
||||||
STATUS_EXPIRED = "Истекла"
|
STATUS_EXPIRED = "срок вышел"
|
||||||
STATUS_LIMITED = "Превышен лимит трафика"
|
STATUS_LIMITED = "трафик на нуле"
|
||||||
STATUS_UNKNOWN = "Неизвестен"
|
STATUS_UNKNOWN = "выясняем"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -61,25 +60,25 @@ STATUS_UNKNOWN = "Неизвестен"
|
|||||||
# — Дни до истечения подписки
|
# — Дни до истечения подписки
|
||||||
DAYS_LEFT_POSITIVE = "{days} дн."
|
DAYS_LEFT_POSITIVE = "{days} дн."
|
||||||
DAYS_LEFT_EXPIRED = "истекла {days} дн. назад"
|
DAYS_LEFT_EXPIRED = "истекла {days} дн. назад"
|
||||||
DAYS_LEFT_TODAY = "истекает сегодня"
|
DAYS_LEFT_TODAY = "доживает последний день"
|
||||||
|
|
||||||
# — Username
|
# — Username
|
||||||
NO_USERNAME = "нет username"
|
NO_USERNAME = "скрыт"
|
||||||
USERNAME_DISPLAY = "@{username}"
|
USERNAME_DISPLAY = "@{username}"
|
||||||
|
|
||||||
# — Internal Squads
|
# — Internal Squads
|
||||||
NO_SQUADS = "нет"
|
NO_SQUADS = "отсутствуют"
|
||||||
|
|
||||||
# — HWID лимит
|
# — HWID лимит
|
||||||
UNLIMITED_HWID = "без ограничений"
|
UNLIMITED_HWID = "без лимитов"
|
||||||
|
|
||||||
# — Трафик
|
# — Трафик
|
||||||
NO_TRAFFIC_LIMIT = "без ограничений"
|
NO_TRAFFIC_LIMIT = "не ограничен"
|
||||||
TRAFFIC_WITH_LIMIT = "{used} / {limit}"
|
TRAFFIC_WITH_LIMIT = "{used} / {limit}"
|
||||||
TRAFFIC_NO_LIMIT = "{used}"
|
TRAFFIC_NO_LIMIT = "{used}"
|
||||||
|
|
||||||
# — Прочее
|
# — Прочее
|
||||||
NOTHING_PLACEHOLDER = "🍃"
|
NOTHING_PLACEHOLDER = "∅"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -90,23 +89,23 @@ MAIN_MENU = (
|
|||||||
str(MALENIA_LOGO)
|
str(MALENIA_LOGO)
|
||||||
+ " <b>Malenia — <i>{quote}</i></b>\n\n"
|
+ " <b>Malenia — <i>{quote}</i></b>\n\n"
|
||||||
+ f"<i>{MALENIA_COINS} Личный кабинет</i>\n"
|
+ f"<i>{MALENIA_COINS} Личный кабинет</i>\n"
|
||||||
+ f"<b>{MALENIA_LINK} Ваша ссылка на подписку: {{link}}</b>"
|
+ f"<b>{MALENIA_LINK} Ключ доступа: <code>{{link}}</code></b>"
|
||||||
)
|
)
|
||||||
|
|
||||||
FAQ_TEXT = "faq"
|
FAQ_TEXT = (
|
||||||
|
"<b>FAQ: Технический минимум</b>\n\n"
|
||||||
GENERAL_PROCESSING = "<i>⏳ Выполняю...</i>"
|
"Здесь нет магии и «инновационных прорывов». Только обфускация трафика и стабильные узлы. "
|
||||||
|
"Если что-то не грузится — скорее всего, фильтры опять пробуют новые сигнатуры. "
|
||||||
STANDARD_FALLBACK = (
|
"Мы это уже чиним."
|
||||||
"<b>❌ Что-то пошло не так</b>, прожмите /start и повторите попытку позже."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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 = (
|
REFERALS_MENU = (
|
||||||
"<b>👥 Реферальная система</b>\n\n"
|
"<b>👥 Реферальная система</b>\n\n"
|
||||||
f"<b>{MALENIA_COINS} Ваш реферальный баланс:</b> "
|
"Никакого сетевого маркетинга, просто делимся ресурсами.\n"
|
||||||
|
f"<b>{MALENIA_COINS} На вашем счету:</b> "
|
||||||
"<code>{balance}₽</code>\n"
|
"<code>{balance}₽</code>\n"
|
||||||
f"<b>{MALENIA_LINK} Ваша реферальная ссылка:</b> "
|
f"<b>{MALENIA_LINK} Ссылка для своих:</b> "
|
||||||
"<code>{link}</code>"
|
"<code>{link}</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,19 +126,15 @@ REFERALS_MENU = (
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
SUBSCRIPTION_DEVICE_SELECTOR = (
|
SUBSCRIPTION_DEVICE_SELECTOR = (
|
||||||
"выбери колво устройств епта. тут ещё цена есть смотри опа: {price}"
|
"Сколько девайсов нужно держать в сети одновременно?\n\nЦена вопроса: <b>{price}₽</b>"
|
||||||
)
|
)
|
||||||
|
|
||||||
SUBSCRIPTION_DURATION_SELECTOR = (
|
SUBSCRIPTION_DURATION_SELECTOR = "На какой срок планируем инфраструктуру?\n\nИтого: <b>{price}₽</b>"
|
||||||
"тут короче сколько время. а ещё цена смотри ОПА {price}"
|
|
||||||
)
|
|
||||||
|
|
||||||
WHITELISTS_ALREADY_ON = "уже включены!"
|
WHITELISTS_ALREADY_ON = "Списки доступа уже активны."
|
||||||
|
|
||||||
TEMP_SUPPORT_FORM = "обратитесь в поддержку, прикрепив текст ниже:\n\n"
|
|
||||||
|
|
||||||
CHECKOUT_PAYMENT_CHOICE = (
|
CHECKOUT_PAYMENT_CHOICE = (
|
||||||
"в поддержку чирканите епта там всё обьяснят\n" + TEMP_SUPPORT_FORM
|
"Автоплатежи — это не к нам. Стукните в поддержку, там объяснят, куда слать деньги."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -147,33 +143,29 @@ CHECKOUT_PAYMENT_CHOICE = (
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
SUPPORT_INIT = (
|
SUPPORT_INIT = (
|
||||||
f"<b>{MALENIA_SUPPORT} <i>Что-то не работает? Жалоба? Предложение?</i></b>\n"
|
f"<b>{MALENIA_SUPPORT} Технический отдел</b>\n"
|
||||||
"— всё сюда, желательно со скриншотами. "
|
"— если интернет превратился в тыкву или есть идеи по конфигам.\n"
|
||||||
"Чем детальнее описание, тем быстрее ответ.\n\n"
|
"Пишите по делу, скриншоты приветствуются.\n\n"
|
||||||
"<i>❌ Для отмены — /cancel</i>"
|
"<i>🌑 /cancel — вернуться к делам</i>"
|
||||||
)
|
)
|
||||||
|
|
||||||
SUPPORT_MSG_SENT = (
|
SUPPORT_MSG_SENT = "⏳ Принято. Инженеры посмотрят, как освободятся. " "Вернуться в меню: /cancel"
|
||||||
"⏳ Сообщение отправлено. Для выхода — /cancel, "
|
|
||||||
"вы можете вернуться сюда из главного меню в любое время."
|
|
||||||
)
|
|
||||||
|
|
||||||
SUPPORT_SIGNATURE = "<b>✍️ Сообщение от технической поддержки:</b>"
|
SUPPORT_SIGNATURE = "<b>📡 Ответ от Malenia Team:</b>"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# ШАБЛОНЫ — ОБЩИЕ БЛОКИ
|
# ШАБЛОНЫ — ОБЩИЕ БЛОКИ
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# Используется в SUPPORT_SUBSCRIPTION и ADMIN_TICKET_WITH_RW
|
|
||||||
_SUPPORT_USER_DESCRIPTION = (
|
_SUPPORT_USER_DESCRIPTION = (
|
||||||
"👤 <b>Имя:</b> {full_name}\n"
|
"👤 <b>Профиль:</b> {full_name}\n"
|
||||||
"🔗 <b>Username:</b> {username_display}\n"
|
"🔗 <b>Alias:</b> {username_display}\n"
|
||||||
"🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n"
|
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
|
||||||
"💰 <b>Реферальный баланс:</b> <code>{balance}</code>"
|
"💰 <b>Баланс:</b> <code>{balance}</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
_DIVIDER = "────────────────────────────\n\n"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -181,14 +173,14 @@ _DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
SUPPORT_SUBSCRIPTION = (
|
SUPPORT_SUBSCRIPTION = (
|
||||||
"<b>🎫 ЗАЯВКА НА ПОКУПКУ:</b>\n\n"
|
"<b>🎫 ВХОДЯЩИЙ ЗАПРОС:</b>\n\n"
|
||||||
+ _SUPPORT_USER_DESCRIPTION
|
+ _SUPPORT_USER_DESCRIPTION
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ _DIVIDER
|
+ _DIVIDER
|
||||||
+ "🗓️ Подписка на <b>{duration}</b> месяцев.\n"
|
+ "🗓️ Срок: <b>{duration} мес.</b>\n"
|
||||||
+ "📱 Количество устройств: <b>{devices}</b>\n"
|
+ "📱 Устройства: <b>{devices}</b>\n"
|
||||||
+ "🏳️ Белые списки: <b>{whitelist_description}</b>\n\n"
|
+ "🏳️ Whitelists: <b>{whitelist_description}</b>\n\n"
|
||||||
+ "💰 Итого: <b>{price}₽</b>"
|
+ "💰 К оплате: <b>{price}₽</b>"
|
||||||
)
|
)
|
||||||
|
|
||||||
ADMIN_TICKET_WITH_RW = (
|
ADMIN_TICKET_WITH_RW = (
|
||||||
@@ -196,35 +188,24 @@ ADMIN_TICKET_WITH_RW = (
|
|||||||
+ _SUPPORT_USER_DESCRIPTION
|
+ _SUPPORT_USER_DESCRIPTION
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ _DIVIDER
|
+ _DIVIDER
|
||||||
+ "📋 <b>ПОДПИСКА REMNAWAVE</b>\n\n"
|
+ "📊 <b>ТЕХ. ДАННЫЕ (Remnawave)</b>\n\n"
|
||||||
+ "{status_icon} <b>Статус:</b> {status}\n"
|
+ "{status_icon} <b>Статус:</b> {status}\n"
|
||||||
+ "📅 <b>Истекает:</b> {expire_date}\n"
|
+ "📅 <b>Deadline:</b> {expire_date}\n"
|
||||||
+ "⏳ <b>Осталось:</b> {days_left}\n"
|
+ "⏳ <b>Остаток:</b> {days_left}\n"
|
||||||
+ "📊 <b>Трафик:</b> {used_traffic}\n"
|
+ "📊 <b>Трафик:</b> {used_traffic}\n"
|
||||||
+ "📱 <b>HWID лимит:</b> {hwid_limit}\n"
|
+ "📱 <b>HWID:</b> {hwid_limit}\n"
|
||||||
+ "🎯 <b>Internal Squads:</b> {squads}\n\n"
|
+ "🎯 <b>Squads:</b> {squads}\n\n"
|
||||||
+ _DIVIDER
|
+ _DIVIDER
|
||||||
+ "🔑 <b>UUID в Remnawave:</b> <code>{remnawave_uuid}</code>"
|
+ "🔑 <b>UUID:</b> <code>{remnawave_uuid}</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
ADMIN_TICKET_WITHOUT_RW = (
|
ADMIN_TICKET_WITHOUT_RW = (
|
||||||
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
||||||
"👤 <b>Имя:</b> {full_name}\n"
|
"👤 <b>Имя:</b> {full_name}\n"
|
||||||
"🔗 <b>Username:</b> {username_display}\n"
|
"🔗 <b>Alias:</b> {username_display}\n"
|
||||||
"🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n"
|
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
|
||||||
+ _DIVIDER
|
+ _DIVIDER
|
||||||
+ "⚠️ <b>Аккаунт в Remnawave не найден</b>"
|
+ "⚠️ <b>Remnawave-профиль отсутствует</b>"
|
||||||
)
|
)
|
||||||
|
|
||||||
ADMIN_STANDARD_FALLBACK = (
|
ADMIN_STANDARD_FALLBACK = "<b>❌ Ошибка выполнения.</b> Проверь логи, что-то пошло не по чертежу."
|
||||||
"<b>❌ Что-то пошло не так</b>, обратитесь к логам для подробностей."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
|
|
||||||
def build_main_menu_text(link: str) -> str:
|
|
||||||
return MAIN_MENU.format(quote=fetch_quote(), link=link)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user