diff --git a/alembic/versions/5985b2df0830_user_subscription_link_optional_text.py b/alembic/versions/5985b2df0830_user_subscription_link_optional_text.py new file mode 100644 index 0000000..4598f54 --- /dev/null +++ b/alembic/versions/5985b2df0830_user_subscription_link_optional_text.py @@ -0,0 +1,34 @@ +"""user -> +subscription_link optional text. + +Revision ID: 5985b2df0830 +Revises: 756210f564a5 +Create Date: 2026-04-09 20:38:15.157762 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "5985b2df0830" +down_revision: Union[str, Sequence[str], None] = "756210f564a5" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("users", sa.Column("subscription_link", sa.TEXT(), nullable=True)) + op.create_unique_constraint(None, "users", ["subscription_link"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "users", type_="unique") + op.drop_column("users", "subscription_link") + # ### end Alembic commands ### diff --git a/config.py b/config.py index 16ea2d4..58324cb 100644 --- a/config.py +++ b/config.py @@ -6,12 +6,13 @@ from dotenv import load_dotenv load_dotenv(override=True) -class settings(BaseSettings): +class Settings(BaseSettings): bot_token: str = Field(alias="BOT_TOKEN") postgres_url: str = Field(alias="POSTGRES_URL") proxy: Optional[str] = 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") remnawave_token: str = Field(alias="REMNAWAVE_TOKEN") min_devices: int = Field(3, alias="MIN_DEVICES") max_devices: int = Field(12, alias="MAX_DEVICES") @@ -20,4 +21,4 @@ class settings(BaseSettings): whitelist_device_threshold: int = Field(6, alias="WHITELIST_DEVICE_THRESHOLD") -settings = settings() # type: ignore +settings = Settings() # type: ignore diff --git a/db/models/users.py b/db/models/users.py index c69e56d..2ef7a7d 100644 --- a/db/models/users.py +++ b/db/models/users.py @@ -1,4 +1,4 @@ -from sqlalchemy import BIGINT +from sqlalchemy import BIGINT, TEXT from sqlalchemy.orm import Mapped, mapped_column from db.base import Base @@ -11,3 +11,4 @@ class User(Base): 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) diff --git a/handlers/buy.py b/handlers/buy.py index fa966de..dacc3fe 100644 --- a/handlers/buy.py +++ b/handlers/buy.py @@ -1,7 +1,9 @@ +import logging from typing import Optional from aiogram import Router, F from aiogram.types import CallbackQuery from aiogram.fsm.context import FSMContext +from sqlalchemy.ext.asyncio import AsyncSession from keyboards.client import ( devices_selector, @@ -12,22 +14,26 @@ from keyboards.client import ( from misc.utils import ( calculate_price, convert_int_to_duration, - format_support_subscription, ) +from schemas.di import DependenciesDTO from schemas.subscriptions import SubscriptionPlan +from services.user_service import UserServiceException from states.buy import SubscriptionStorage from texts import ( CALLBACK_FALLBACK, CHECKOUT_PAYMENT_CHOICE, CONTEXT_REINITIATED, + GENERAL_PROCESSING, NOTHING_PLACEHOLDER, STANDARD_FALLBACK, SUBSCRIPTION_DEVICE_SELECTOR, SUBSCRIPTION_DURATION_SELECTOR, + SUPPORT_MSG_SENT, ) from config import settings router = Router() +logger = logging.getLogger(__name__) @router.callback_query(F.data == "buy") @@ -160,6 +166,36 @@ async def proceed_to_checkout(cb: CallbackQuery, state: FSMContext): return await cb.message.edit_text( - CHECKOUT_PAYMENT_CHOICE.format(code=format_support_subscription(ctx)), + CHECKOUT_PAYMENT_CHOICE, reply_markup=payment_gateways, ) # FIXME: temp solution that's incredibly stupid + + await state.set_state(SubscriptionStorage.checkout) + await state.set_data({"ctx": ctx}) + + +@router.callback_query(F.data == "checkout") +async def checkout_support( + cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO +): + data = await state.get_data() + await state.clear() + + ctx: Optional[SubscriptionPlan] = 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 cb.message.edit_text(SUPPORT_MSG_SENT) + except UserServiceException: + await cb.message.edit_text(STANDARD_FALLBACK) + raise + except Exception: + await cb.message.edit_text(STANDARD_FALLBACK) + logger.exception("Exception occured while trying to forward a msg to support") + raise diff --git a/handlers/menus.py b/handlers/menus.py index 0118289..1136486 100644 --- a/handlers/menus.py +++ b/handlers/menus.py @@ -4,9 +4,10 @@ from aiogram.types import CallbackQuery, Message from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession +from misc.utils import format_subscription_link from schemas.di import DependenciesDTO -from keyboards.client import main_menu -from texts import build_main_menu_text +from keyboards.client import main_menu, return_to_menu +from texts import GENERAL_PROCESSING, build_main_menu_text, FAQ_TEXT router = Router() @@ -27,9 +28,14 @@ async def fetch_referal( return referal = data[0] - await deps.user_service.add_user(session, user_id=msg.from_user.id, referal=referal) + user = await deps.user_service.add_user( + session, user_id=msg.from_user.id, referal=referal, rw_sdk=deps.rw_sdk + ) - await msg.reply(build_main_menu_text(), reply_markup=main_menu) + await msg.reply( + build_main_menu_text(format_subscription_link(user.subscription_link)), + reply_markup=main_menu, + ) @router.message(Command(commands=["start", "cancel"])) @@ -42,13 +48,48 @@ async def standard_start( ): await state.clear() - await deps.user_service.add_user(session, user_id=msg.from_user.id) + 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(), reply_markup=main_menu) + await msg.reply( + build_main_menu_text(format_subscription_link(user.subscription_link)), + reply_markup=main_menu, + ) @router.callback_query(F.data == "menu:main") -async def main_menu_cb(cb: CallbackQuery, state: FSMContext): +async def main_menu_cb( + cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO +): await state.clear() - await cb.message.edit_text(build_main_menu_text(), reply_markup=main_menu) + user = await deps.user_repository.get_user_by_id(session, cb.from_user.id) + await cb.message.edit_text( + build_main_menu_text(format_subscription_link(user.subscription_link)), + reply_markup=main_menu, + ) + + +@router.callback_query(F.data == "update_link") +async def update_link( + cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO +): + 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 + ) + await cb.answer("✅") + await cb.message.edit_text( + build_main_menu_text(format_subscription_link(user.subscription_link)), + reply_markup=main_menu, + ) + + +@router.callback_query(F.data == "faq") +async def faq(cb: CallbackQuery, state: FSMContext): + await state.clear() + + await cb.message.edit_text(FAQ_TEXT, reply_markup=return_to_menu) diff --git a/keyboards/client.py b/keyboards/client.py index b17e4c1..3ee70f3 100644 --- a/keyboards/client.py +++ b/keyboards/client.py @@ -15,6 +15,7 @@ main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( InlineKeyboardButton(text="Наш Канал", url="https://goo.gle"), InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"), ], + [InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")], ] ).as_markup() @@ -90,7 +91,7 @@ def duration_selector( payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder( [ - [InlineKeyboardButton(text="✍️ Поддержка", callback_data="support")], + [InlineKeyboardButton(text="✍️ Перейти в поддержку", callback_data="checkout")], [_return_to_menu_btn], ] ).as_markup() diff --git a/misc/utils.py b/misc/utils.py index 1a37242..632e2bb 100644 --- a/misc/utils.py +++ b/misc/utils.py @@ -171,16 +171,33 @@ def calculate_price(subscription: SubscriptionPlan): ) * convert_duration_to_int(subscription.duration) -def format_support_subscription(subscription: SubscriptionPlan) -> str: +def format_subscription_link(link: Optional[str] = None): + return f"{link or '...'}" + + +def format_support_subscription( + subscription: SubscriptionPlan, + full_name: str, + user_id: int, + username: Optional[str] = None, +) -> str: + username = format_username(username) whitelist_description = "ВЫКЛ" if subscription.devices >= settings.whitelist_device_threshold: whitelist_description = "ВКЛ (Бесплатно)" elif subscription.whitelists: whitelist_description = "ВКЛ (Платно)" - return ( - f"Подписка на {convert_duration_to_int(subscription.duration)} месяцев.\n" - f"Количество устройств: {subscription.devices}\n" - f"Белые списки: {whitelist_description}\n\n" - f"Итого: {calculate_price(subscription)}₽" + return texts.SUPPORT_SUBSCRIPTION.format( + full_name=full_name, + telegram_id=user_id, + username_display=username, + duration=convert_duration_to_int(subscription.duration), + devices=subscription.devices, + whitelist_description=whitelist_description, + price=calculate_price(subscription), ) + + +def get_subscription_link(short_uuid: str): + return settings.subscription_url + "/" + short_uuid diff --git a/repositories/users.py b/repositories/users.py index ee35aa3..8bcc759 100644 --- a/repositories/users.py +++ b/repositories/users.py @@ -16,7 +16,11 @@ class UserRepository: return result.scalar_one_or_none() async def create_user(self, session: AsyncSession, user_data: NewUserDTO) -> User: - user = User(id=user_data.id, referal_id=user_data.referal) + user = User( + id=user_data.id, + referal_id=user_data.referal, + subscription_link=user_data.link, + ) session.add(user) await session.commit() @@ -32,3 +36,14 @@ class UserRepository: await session.commit() return user + + async def update_user_link( + self, session: AsyncSession, user_id: int, link: str + ) -> Optional[User]: + user = await self.get_user_by_id(session, user_id=user_id) + if not user: + return None + user.subscription_link = link + + await session.commit() + return user diff --git a/schemas/users.py b/schemas/users.py index cd6466c..15f29af 100644 --- a/schemas/users.py +++ b/schemas/users.py @@ -5,7 +5,8 @@ from typing import Optional @dataclass class NewUserDTO: id: int - referal: int + referal: Optional[int] = None + link: Optional[str] = None @dataclass diff --git a/services/rw.py b/services/rw.py index 03acf7d..70c65b6 100644 --- a/services/rw.py +++ b/services/rw.py @@ -26,6 +26,7 @@ class RWUserInfo: hwid_device_limit: Optional[int] # None = unlimited active_squads: list[dict] # [{"uuid": "...", "name": "..."}] description: Optional[str] + short_uuid: str # ───────────────────────────────────────────────────────────────── @@ -61,6 +62,7 @@ def _parse_user(user_dto) -> RWUserInfo: hwid_device_limit=user_dto.hwid_device_limit, active_squads=active_squads, description=user_dto.description, + short_uuid=user_dto.short_uuid, ) diff --git a/services/user_service.py b/services/user_service.py index 8cd9466..13a4b07 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -1,11 +1,14 @@ 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 remnawave import RemnawaveSDK from config import settings from db.models.tickets import TicketStatus from db.models.users import User +from schemas.subscriptions import SubscriptionPlan from schemas.tickets import NewTicketDTO from schemas.users import NewUserDTO, TicketCreator from repositories import UserRepository @@ -84,16 +87,76 @@ class UserService: ) async def add_user( - self, session: AsyncSession, user_id: int, referal: Optional[int] = None + self, + session: AsyncSession, + user_id: int, + rw_sdk: RemnawaveSDK, + referal: Optional[int] = None, ) -> User: user = await self.user_repository.get_user_by_id(session, user_id) if user: return user + rw_user = await get_user_by_telegram_id(rw_sdk, user_id) + link = utils.get_subscription_link(rw_user.short_uuid) referal = int(referal) if str(referal).isdigit() else None - model = NewUserDTO(id=user_id, referal=referal) + + model = NewUserDTO(id=user_id, referal=referal, link=link) return await self.user_repository.create_user(session, model) + async def create_subscription_ticket( + self, + session: AsyncSession, + user: TelegramUser, + subscription: SubscriptionPlan, + bot: Bot, + ): + ticket = await self.ticket_repository.get_available_ticket_by_user_id( + session, user.id + ) + + if ticket: + try: + await bot.send_message( + chat_id=settings.admin_group_id, + message_thread_id=ticket.thread_id, + text=utils.format_support_subscription( + subscription, user.full_name, user.id, user.username + ), + ) + return + except Exception: + await self.ticket_repository.update_ticket_status( + session, ticket, TicketStatus.CLOSED + ) + raise + creator = TicketCreator(id=user.id, username=user.username) + + ticket_dto = NewTicketDTO( + creator_id=user.id, + ) + 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 + ), + ) + ticket = await self.ticket_repository.update_ticket_thread_id( + session, ticket, thread.message_thread_id + ) + + await bot.send_message( + chat_id=settings.admin_group_id, + message_thread_id=ticket.thread_id, + text=utils.format_support_subscription( + subscription, user.full_name, user.id, user.username + ), + ) + async def support_forward_message( self, session: AsyncSession, msg: Message, rw: RemnawaveSDK ): @@ -195,3 +258,14 @@ class UserService: 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]: + rw_user = await get_user_by_telegram_id(rw_sdk, user_id) + + link = utils.get_subscription_link(rw_user.short_uuid) + + user = await self.user_repository.update_user_link(session, user_id, link) + + return user diff --git a/states/buy.py b/states/buy.py index 078dd2b..7ac22a7 100644 --- a/states/buy.py +++ b/states/buy.py @@ -4,3 +4,4 @@ from aiogram.fsm.state import StatesGroup, State class SubscriptionStorage(StatesGroup): devices = State() duration = State() + checkout = State() diff --git a/texts.py b/texts.py index c81199a..953beaa 100644 --- a/texts.py +++ b/texts.py @@ -2,6 +2,10 @@ from misc.utils import fetch_quote from db.models.tickets import TicketStatus +# ============================================================ +# PREMIUM EMOJI +# ============================================================ + class PremiumEmoji: def __init__(self, emoji_id: int, emoji: str): self.emoji_id = emoji_id @@ -11,135 +15,201 @@ class PremiumEmoji: return f'{self.emoji}' -## EMOJIS - - -MALENIA_LOGO = PremiumEmoji(5265053829522562789, "☀️") -MALENIA_HEART = PremiumEmoji(5276516908257025723, "❤️") -MALENIA_LINK = PremiumEmoji(5276012730636082950, "🔗") -MALENIA_COINS = PremiumEmoji(5276298354551199712, "🪙") -MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒") +# — Иконки бота +MALENIA_LOGO = PremiumEmoji(5265053829522562789, "☀️") +MALENIA_HEART = PremiumEmoji(5276516908257025723, "❤️") +MALENIA_LINK = PremiumEmoji(5276012730636082950, "🔗") +MALENIA_COINS = PremiumEmoji(5276298354551199712, "🪙") +MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒") MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧") -## MAPPING + +# ============================================================ +# МАППИНГИ +# ============================================================ + TICKET_STATUS_EMOJIS = { - TicketStatus.CLOSED.value: "🔴", - TicketStatus.OPEN.value: "🟢", + TicketStatus.CLOSED.value: "🔴", + TicketStatus.OPEN.value: "🟢", TicketStatus.PENDING.value: "🟡", } -## TEXTS + +# ============================================================ +# СТАТУСЫ ПОДПИСКИ +# ============================================================ + +# — Иконки статусов +STATUS_ICON_ACTIVE = "🟢" +STATUS_ICON_DISABLED = "🔴" +STATUS_ICON_EXPIRED = "🟡" +STATUS_ICON_LIMITED = "🟠" +STATUS_ICON_UNKNOWN = "⚪" + +# — Тексты статусов +STATUS_ACTIVE = "Активна" +STATUS_DISABLED = "Отключена" +STATUS_EXPIRED = "Истекла" +STATUS_LIMITED = "Превышен лимит трафика" +STATUS_UNKNOWN = "Неизвестен" + + +# ============================================================ +# ФОРМАТИРОВАНИЕ ДАННЫХ +# ============================================================ + +# — Дни до истечения подписки +DAYS_LEFT_POSITIVE = "{days} дн." +DAYS_LEFT_EXPIRED = "истекла {days} дн. назад" +DAYS_LEFT_TODAY = "истекает сегодня" + +# — Username +NO_USERNAME = "нет username" +USERNAME_DISPLAY = "@{username}" + +# — Internal Squads +NO_SQUADS = "нет" + +# — HWID лимит +UNLIMITED_HWID = "без ограничений" + +# — Трафик +NO_TRAFFIC_LIMIT = "без ограничений" +TRAFFIC_WITH_LIMIT = "{used} / {limit}" +TRAFFIC_NO_LIMIT = "{used}" + +# — Прочее +NOTHING_PLACEHOLDER = "🍃" + + +# ============================================================ +# ПОЛЬЗОВАТЕЛЬСКИЙ ИНТЕРФЕЙС +# ============================================================ + MAIN_MENU = ( str(MALENIA_LOGO) + " Malenia — {quote}\n\n" - + f"{MALENIA_COINS} Личный кабинет" + + f"{MALENIA_COINS} Личный кабинет\n" + + f"{MALENIA_LINK} Ваша ссылка на подписку: {{link}}" ) + +FAQ_TEXT = "faq" + +GENERAL_PROCESSING = "⏳ Выполняю..." + STANDARD_FALLBACK = ( "❌ Что-то пошло не так, прожмите /start и повторите попытку позже." ) -TEMP_SUPPORT_FORM = ( - "обратитесь в поддержку, прикрепив текст ниже:\n\n" "\n" "{code}\n" "" -) -CHECKOUT_PAYMENT_CHOICE = ( - "в поддержку чирканите епта там всё обьяснят\n" + TEMP_SUPPORT_FORM -) - CALLBACK_FALLBACK = "❌ Что-то пошло не так" -CONTEXT_REINITIATED = "🔴 Контекст данного взаимодействия сброшен. Данные могут отличаться от указанных вами." -GENERAL_PROCESSING = "⏳ Выполняю..." +CONTEXT_REINITIATED = ( + "🔴 Контекст данного взаимодействия сброшен. " + "Данные могут отличаться от указанных вами." +) + + +# ============================================================ +# ОФОРМЛЕНИЕ ПОДПИСКИ +# ============================================================ SUBSCRIPTION_DEVICE_SELECTOR = ( "выбери колво устройств епта. тут ещё цена есть смотри опа: {price}" ) -WHITELISTS_ALREADY_ON = "уже включены!" + SUBSCRIPTION_DURATION_SELECTOR = ( "тут короче сколько время. а ещё цена смотри ОПА {price}" ) -SUPPORT_INIT = f"{MALENIA_SUPPORT} Что-то не работает? Жалоба? Предложение?\n— всё сюда, желательно со скриншотами. Чем детальнее описание, тем быстрее ответ.\n\n❌ Для отмены — /cancel" -SUPPORT_MSG_SENT = "⏳ Сообщение отправлено. Для выхода — /cancel, вы можете вернуться сюда из главного меню в любое время." +WHITELISTS_ALREADY_ON = "уже включены!" + +TEMP_SUPPORT_FORM = "обратитесь в поддержку, прикрепив текст ниже:\n\n" + +CHECKOUT_PAYMENT_CHOICE = ( + "в поддержку чирканите епта там всё обьяснят\n" + + TEMP_SUPPORT_FORM +) + + +# ============================================================ +# ПОДДЕРЖКА +# ============================================================ + +SUPPORT_INIT = ( + f"{MALENIA_SUPPORT} Что-то не работает? Жалоба? Предложение?\n" + "— всё сюда, желательно со скриншотами. " + "Чем детальнее описание, тем быстрее ответ.\n\n" + "❌ Для отмены — /cancel" +) + +SUPPORT_MSG_SENT = ( + "⏳ Сообщение отправлено. Для выхода — /cancel, " + "вы можете вернуться сюда из главного меню в любое время." +) + SUPPORT_SIGNATURE = "✍️ Сообщение от технической поддержки:" -ADMIN_TICKET_WITH_RW = ( - "🎫 ТИКЕТ T-{ticket_number:04d}\n\n" + + +# ============================================================ +# ШАБЛОНЫ — ОБЩИЕ БЛОКИ +# ============================================================ + +# Используется в SUPPORT_SUBSCRIPTION и ADMIN_TICKET_WITH_RW +_SUPPORT_USER_DESCRIPTION = ( "👤 Имя: {full_name}\n" "🔗 Username: {username_display}\n" "🆔 Telegram ID: {telegram_id}\n\n" - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" - "📋 ПОДПИСКА REMNAWAVE\n\n" - "{status_icon} Статус: {status}\n" - "📅 Истекает: {expire_date}\n" - "⏳ Осталось: {days_left}\n" - "📊 Трафик: {used_traffic}\n" - "📱 HWID лимит: {hwid_limit}\n" - "🎯 Internal Squads: {squads}\n\n" - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" - "🔑 UUID в Remnawave: {remnawave_uuid}" ) + +_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + + +# ============================================================ +# ШАБЛОНЫ — ЗАЯВКИ И ТИКЕТЫ +# ============================================================ + +SUPPORT_SUBSCRIPTION = ( + "🎫 ЗАЯВКА НА ПОКУПКУ:\n\n" + + _SUPPORT_USER_DESCRIPTION + + "\n" + _DIVIDER + + "🗓️ Подписка на {duration} месяцев.\n" + + "📱 Количество устройств: {devices}\n" + + "🏳️ Белые списки: {whitelist_description}\n\n" + + "💰 Итого: {price}₽" +) + +ADMIN_TICKET_WITH_RW = ( + "🎫 ТИКЕТ T-{ticket_number:04d}\n\n" + + _SUPPORT_USER_DESCRIPTION + + "\n" + _DIVIDER + + "📋 ПОДПИСКА REMNAWAVE\n\n" + + "{status_icon} Статус: {status}\n" + + "📅 Истекает: {expire_date}\n" + + "⏳ Осталось: {days_left}\n" + + "📊 Трафик: {used_traffic}\n" + + "📱 HWID лимит: {hwid_limit}\n" + + "🎯 Internal Squads: {squads}\n\n" + + _DIVIDER + + "🔑 UUID в Remnawave: {remnawave_uuid}" +) + ADMIN_TICKET_WITHOUT_RW = ( "🎫 ТИКЕТ T-{ticket_number:04d}\n\n" "👤 Имя: {full_name}\n" "🔗 Username: {username_display}\n" "🆔 Telegram ID: {telegram_id}\n\n" - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" - "⚠️ Аккаунт в Remnawave не найден" + + _DIVIDER + + "⚠️ Аккаунт в Remnawave не найден" ) - ADMIN_STANDARD_FALLBACK = ( "❌ Что-то пошло не так, обратитесь к логам для подробностей." ) -## FUNCTIONS -def build_main_menu_text() -> str: - return MAIN_MENU.format(quote=fetch_quote()) +# ============================================================ +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ============================================================ - -NO_USERNAME = "нет юзернейма" -STATUS_ICON_ACTIVE = "🟢" -STATUS_ICON_DISABLED = "🔴" -STATUS_ICON_EXPIRED = "🟡" -STATUS_ICON_LIMITED = "🟠" -STATUS_ICON_UNKNOWN = "⚪" - -# Текст статуса подписки -STATUS_ACTIVE = "Активна" -STATUS_DISABLED = "Отключена" -STATUS_EXPIRED = "Истекла" -STATUS_LIMITED = "Превышен лимит трафика" -STATUS_UNKNOWN = "Неизвестен" - -# Шаблон для остатка дней: {days} дн. -DAYS_LEFT_POSITIVE = "{days} дн." - -# Когда подписка уже истекла -DAYS_LEFT_EXPIRED = "истекла {days} дн. назад" - -# Когда подписка истекает сегодня -DAYS_LEFT_TODAY = "истекает сегодня" - -# Когда нет username в Telegram -NO_USERNAME = "нет username" - -# Формат отображения username (с @) -USERNAME_DISPLAY = "@{username}" - -# Когда у пользователя нет Internal Squads -NO_SQUADS = "нет" - -# Когда лимит HWID не установлен (0 или None) -UNLIMITED_HWID = "без ограничений" - -# Когда лимит трафика не установлен -NO_TRAFFIC_LIMIT = "без ограничений" - -# Шаблон для отображения трафика с лимитом: {used} / {limit} -TRAFFIC_WITH_LIMIT = "{used} / {limit}" - -# Шаблон для отображения трафика без лимита: {used} -TRAFFIC_NO_LIMIT = "{used}" - - -NOTHING_PLACEHOLDER = "🍃" +def build_main_menu_text(link: str) -> str: + return MAIN_MENU.format(quote=fetch_quote(), link=link) \ No newline at end of file