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