Add subscription link field to user model

- Add subscription_link column to users table (nullable, unique)
- Add SUBSCRIPTION_URL to config settings
- Update user creation to include subscription link from Remnawave
- Add support subscription ticket creation with formatted message
- Add "update link" button to main menu
- Refactor support subscription formatting to include user details
This commit is contained in:
2026-04-09 21:09:29 +07:00
parent 2bb12f2ff8
commit 60de871e0c
13 changed files with 408 additions and 114 deletions

View File

@@ -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 ###

View File

@@ -6,12 +6,13 @@ from dotenv import load_dotenv
load_dotenv(override=True) 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: Optional[str] = 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")
remnawave_token: str = Field(alias="REMNAWAVE_TOKEN") remnawave_token: str = Field(alias="REMNAWAVE_TOKEN")
min_devices: int = Field(3, alias="MIN_DEVICES") min_devices: int = Field(3, alias="MIN_DEVICES")
max_devices: int = Field(12, alias="MAX_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") whitelist_device_threshold: int = Field(6, alias="WHITELIST_DEVICE_THRESHOLD")
settings = settings() # type: ignore settings = Settings() # type: ignore

View File

@@ -1,4 +1,4 @@
from sqlalchemy import BIGINT from sqlalchemy import BIGINT, TEXT
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from db.base import Base from db.base import Base
@@ -11,3 +11,4 @@ class User(Base):
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)

View File

@@ -1,7 +1,9 @@
import logging
from typing import Optional from typing import Optional
from aiogram import Router, F from aiogram import Router, F
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from keyboards.client import ( from keyboards.client import (
devices_selector, devices_selector,
@@ -12,22 +14,26 @@ from keyboards.client import (
from misc.utils import ( from misc.utils import (
calculate_price, calculate_price,
convert_int_to_duration, convert_int_to_duration,
format_support_subscription,
) )
from schemas.di import DependenciesDTO
from schemas.subscriptions import SubscriptionPlan from schemas.subscriptions import SubscriptionPlan
from services.user_service import UserServiceException
from states.buy import SubscriptionStorage from states.buy import SubscriptionStorage
from texts import ( from texts import (
CALLBACK_FALLBACK, CALLBACK_FALLBACK,
CHECKOUT_PAYMENT_CHOICE, CHECKOUT_PAYMENT_CHOICE,
CONTEXT_REINITIATED, CONTEXT_REINITIATED,
GENERAL_PROCESSING,
NOTHING_PLACEHOLDER, NOTHING_PLACEHOLDER,
STANDARD_FALLBACK, STANDARD_FALLBACK,
SUBSCRIPTION_DEVICE_SELECTOR, SUBSCRIPTION_DEVICE_SELECTOR,
SUBSCRIPTION_DURATION_SELECTOR, SUBSCRIPTION_DURATION_SELECTOR,
SUPPORT_MSG_SENT,
) )
from config import settings from config import settings
router = Router() router = Router()
logger = logging.getLogger(__name__)
@router.callback_query(F.data == "buy") @router.callback_query(F.data == "buy")
@@ -160,6 +166,36 @@ async def proceed_to_checkout(cb: CallbackQuery, state: FSMContext):
return return
await cb.message.edit_text( await cb.message.edit_text(
CHECKOUT_PAYMENT_CHOICE.format(code=format_support_subscription(ctx)), CHECKOUT_PAYMENT_CHOICE,
reply_markup=payment_gateways, reply_markup=payment_gateways,
) # FIXME: temp solution that's incredibly stupid ) # 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

View File

@@ -4,9 +4,10 @@ from aiogram.types import CallbackQuery, Message
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
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 schemas.di import DependenciesDTO
from keyboards.client import main_menu from keyboards.client import main_menu, return_to_menu
from texts import build_main_menu_text from texts import GENERAL_PROCESSING, build_main_menu_text, FAQ_TEXT
router = Router() router = Router()
@@ -27,9 +28,14 @@ async def fetch_referal(
return return
referal = data[0] 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"])) @router.message(Command(commands=["start", "cancel"]))
@@ -42,13 +48,48 @@ async def standard_start(
): ):
await state.clear() 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") @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 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)

View File

@@ -15,6 +15,7 @@ main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
InlineKeyboardButton(text="Наш Канал", url="https://goo.gle"), InlineKeyboardButton(text="Наш Канал", url="https://goo.gle"),
InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"), InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"),
], ],
[InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")],
] ]
).as_markup() ).as_markup()
@@ -90,7 +91,7 @@ def duration_selector(
payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder( payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
[ [
[InlineKeyboardButton(text="✍️ Поддержка", callback_data="support")], [InlineKeyboardButton(text="✍️ Перейти в поддержку", callback_data="checkout")],
[_return_to_menu_btn], [_return_to_menu_btn],
] ]
).as_markup() ).as_markup()

View File

@@ -171,16 +171,33 @@ def calculate_price(subscription: SubscriptionPlan):
) * convert_duration_to_int(subscription.duration) ) * convert_duration_to_int(subscription.duration)
def format_support_subscription(subscription: SubscriptionPlan) -> str: def format_subscription_link(link: Optional[str] = None):
return f"<code>{link or '...'}</code>"
def format_support_subscription(
subscription: SubscriptionPlan,
full_name: str,
user_id: int,
username: Optional[str] = None,
) -> str:
username = format_username(username)
whitelist_description = "ВЫКЛ" whitelist_description = "ВЫКЛ"
if subscription.devices >= settings.whitelist_device_threshold: if subscription.devices >= settings.whitelist_device_threshold:
whitelist_description = "ВКЛ (Бесплатно)" whitelist_description = "ВКЛ (Бесплатно)"
elif subscription.whitelists: elif subscription.whitelists:
whitelist_description = "ВКЛ (Платно)" whitelist_description = "ВКЛ (Платно)"
return ( return texts.SUPPORT_SUBSCRIPTION.format(
f"Подписка на {convert_duration_to_int(subscription.duration)} месяцев.\n" full_name=full_name,
f"Количество устройств: {subscription.devices}\n" telegram_id=user_id,
f"Белые списки: {whitelist_description}\n\n" username_display=username,
f"Итого: {calculate_price(subscription)}" 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

View File

@@ -16,7 +16,11 @@ class UserRepository:
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def create_user(self, session: AsyncSession, user_data: NewUserDTO) -> User: 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) session.add(user)
await session.commit() await session.commit()
@@ -32,3 +36,14 @@ class UserRepository:
await session.commit() await session.commit()
return user 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

View File

@@ -5,7 +5,8 @@ from typing import Optional
@dataclass @dataclass
class NewUserDTO: class NewUserDTO:
id: int id: int
referal: int referal: Optional[int] = None
link: Optional[str] = None
@dataclass @dataclass

View File

@@ -26,6 +26,7 @@ class RWUserInfo:
hwid_device_limit: Optional[int] # None = unlimited hwid_device_limit: Optional[int] # None = unlimited
active_squads: list[dict] # [{"uuid": "...", "name": "..."}] active_squads: list[dict] # [{"uuid": "...", "name": "..."}]
description: Optional[str] description: Optional[str]
short_uuid: str
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────
@@ -61,6 +62,7 @@ def _parse_user(user_dto) -> RWUserInfo:
hwid_device_limit=user_dto.hwid_device_limit, hwid_device_limit=user_dto.hwid_device_limit,
active_squads=active_squads, active_squads=active_squads,
description=user_dto.description, description=user_dto.description,
short_uuid=user_dto.short_uuid,
) )

View File

@@ -1,11 +1,14 @@
from typing import Optional from typing import Optional
from aiogram import Bot
from aiogram.types import Message, ReactionTypeEmoji from aiogram.types import Message, ReactionTypeEmoji
from aiogram.types import User as TelegramUser
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from remnawave import RemnawaveSDK from remnawave import RemnawaveSDK
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 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 import UserRepository
@@ -84,16 +87,76 @@ class UserService:
) )
async def add_user( 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:
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:
return 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 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) 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( async def support_forward_message(
self, session: AsyncSession, msg: Message, rw: RemnawaveSDK self, session: AsyncSession, msg: Message, rw: RemnawaveSDK
): ):
@@ -195,3 +258,14 @@ class UserService:
await msg.bot.close_forum_topic( await msg.bot.close_forum_topic(
settings.admin_group_id, message_thread_id=thread_id 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

View File

@@ -4,3 +4,4 @@ from aiogram.fsm.state import StatesGroup, State
class SubscriptionStorage(StatesGroup): class SubscriptionStorage(StatesGroup):
devices = State() devices = State()
duration = State() duration = State()
checkout = State()

250
texts.py
View File

@@ -2,6 +2,10 @@ from misc.utils import fetch_quote
from db.models.tickets import TicketStatus from db.models.tickets import TicketStatus
# ============================================================
# PREMIUM EMOJI
# ============================================================
class PremiumEmoji: class PremiumEmoji:
def __init__(self, emoji_id: int, emoji: str): def __init__(self, emoji_id: int, emoji: str):
self.emoji_id = emoji_id self.emoji_id = emoji_id
@@ -11,135 +15,201 @@ class PremiumEmoji:
return f'<tg-emoji emoji-id="{self.emoji_id}">{self.emoji}</tg-emoji>' return f'<tg-emoji emoji-id="{self.emoji_id}">{self.emoji}</tg-emoji>'
## EMOJIS # — Иконки бота
MALENIA_LOGO = PremiumEmoji(5265053829522562789, "☀️")
MALENIA_HEART = PremiumEmoji(5276516908257025723, "❤️")
MALENIA_LOGO = PremiumEmoji(5265053829522562789, "☀️") MALENIA_LINK = PremiumEmoji(5276012730636082950, "🔗")
MALENIA_HEART = PremiumEmoji(5276516908257025723, "❤️") MALENIA_COINS = PremiumEmoji(5276298354551199712, "🪙")
MALENIA_LINK = PremiumEmoji(5276012730636082950, "🔗") MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒")
MALENIA_COINS = PremiumEmoji(5276298354551199712, "🪙")
MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒")
MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧") MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧")
## MAPPING
# ============================================================
# МАППИНГИ
# ============================================================
TICKET_STATUS_EMOJIS = { TICKET_STATUS_EMOJIS = {
TicketStatus.CLOSED.value: "🔴", TicketStatus.CLOSED.value: "🔴",
TicketStatus.OPEN.value: "🟢", TicketStatus.OPEN.value: "🟢",
TicketStatus.PENDING.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 = ( 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>" + f"<i>{MALENIA_COINS} Личный кабинет</i>\n"
+ f"<b>{MALENIA_LINK} Ваша ссылка на подписку: {{link}}</b>"
) )
FAQ_TEXT = "faq"
GENERAL_PROCESSING = "<i>⏳ Выполняю...</i>"
STANDARD_FALLBACK = ( STANDARD_FALLBACK = (
"<b>❌ Что-то пошло не так</b>, прожмите /start и повторите попытку позже." "<b>❌ Что-то пошло не так</b>, прожмите /start и повторите попытку позже."
) )
TEMP_SUPPORT_FORM = (
"обратитесь в поддержку, прикрепив текст ниже:\n\n" "<code>\n" "{code}\n" "</code>"
)
CHECKOUT_PAYMENT_CHOICE = (
"в поддержку чирканите епта там всё обьяснят\n" + TEMP_SUPPORT_FORM
)
CALLBACK_FALLBACK = "❌ Что-то пошло не так" CALLBACK_FALLBACK = "❌ Что-то пошло не так"
CONTEXT_REINITIATED = "🔴 Контекст данного взаимодействия сброшен. Данные могут отличаться от указанных вами."
GENERAL_PROCESSING = "<i>⏳ Выполняю...</i>" CONTEXT_REINITIATED = (
"🔴 Контекст данного взаимодействия сброшен. "
"Данные могут отличаться от указанных вами."
)
# ============================================================
# ОФОРМЛЕНИЕ ПОДПИСКИ
# ============================================================
SUBSCRIPTION_DEVICE_SELECTOR = ( SUBSCRIPTION_DEVICE_SELECTOR = (
"выбери колво устройств епта. тут ещё цена есть смотри опа: {price}" "выбери колво устройств епта. тут ещё цена есть смотри опа: {price}"
) )
WHITELISTS_ALREADY_ON = "уже включены!"
SUBSCRIPTION_DURATION_SELECTOR = ( SUBSCRIPTION_DURATION_SELECTOR = (
"тут короче сколько время. а ещё цена смотри ОПА {price}" "тут короче сколько время. а ещё цена смотри ОПА {price}"
) )
SUPPORT_INIT = f"<b>{MALENIA_SUPPORT} <i>Что-то не работает? Жалоба? Предложение?</i></b>\n— всё сюда, желательно со скриншотами. Чем детальнее описание, тем быстрее ответ.\n\n<i>❌ Для отмены — /cancel</i>" WHITELISTS_ALREADY_ON = "уже включены!"
SUPPORT_MSG_SENT = "⏳ Сообщение отправлено. Для выхода — /cancel, вы можете вернуться сюда из главного меню в любое время."
TEMP_SUPPORT_FORM = "обратитесь в поддержку, прикрепив текст ниже:\n\n"
CHECKOUT_PAYMENT_CHOICE = (
"в поддержку чирканите епта там всё обьяснят\n"
+ TEMP_SUPPORT_FORM
)
# ============================================================
# ПОДДЕРЖКА
# ============================================================
SUPPORT_INIT = (
f"<b>{MALENIA_SUPPORT} <i>Что-то не работает? Жалоба? Предложение?</i></b>\n"
"— всё сюда, желательно со скриншотами. "
"Чем детальнее описание, тем быстрее ответ.\n\n"
"<i>❌ Для отмены — /cancel</i>"
)
SUPPORT_MSG_SENT = (
"⏳ Сообщение отправлено. Для выхода — /cancel, "
"вы можете вернуться сюда из главного меню в любое время."
)
SUPPORT_SIGNATURE = "<b>✍️ Сообщение от технической поддержки:</b>" SUPPORT_SIGNATURE = "<b>✍️ Сообщение от технической поддержки:</b>"
ADMIN_TICKET_WITH_RW = (
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
# ============================================================
# ШАБЛОНЫ — ОБЩИЕ БЛОКИ
# ============================================================
# Используется в SUPPORT_SUBSCRIPTION и ADMIN_TICKET_WITH_RW
_SUPPORT_USER_DESCRIPTION = (
"👤 <b>Имя:</b> {full_name}\n" "👤 <b>Имя:</b> {full_name}\n"
"🔗 <b>Username:</b> {username_display}\n" "🔗 <b>Username:</b> {username_display}\n"
"🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n" "🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"📋 <b>ПОДПИСКА REMNAWAVE</b>\n\n"
"{status_icon} <b>Статус:</b> {status}\n"
"📅 <b>Истекает:</b> {expire_date}\n"
"⏳ <b>Осталось:</b> {days_left}\n"
"📊 <b>Трафик:</b> {used_traffic}\n"
"📱 <b>HWID лимит:</b> {hwid_limit}\n"
"🎯 <b>Internal Squads:</b> {squads}\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"🔑 <b>UUID в Remnawave:</b> <code>{remnawave_uuid}</code>"
) )
_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
# ============================================================
# ШАБЛОНЫ — ЗАЯВКИ И ТИКЕТЫ
# ============================================================
SUPPORT_SUBSCRIPTION = (
"<b>🎫 ЗАЯВКА НА ПОКУПКУ:</b>\n\n"
+ _SUPPORT_USER_DESCRIPTION
+ "\n" + _DIVIDER
+ "🗓️ Подписка на <b>{duration}</b> месяцев.\n"
+ "📱 Количество устройств: <b>{devices}</b>\n"
+ "🏳️ Белые списки: <b>{whitelist_description}</b>\n\n"
+ "💰 Итого: <b>{price}₽</b>"
)
ADMIN_TICKET_WITH_RW = (
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
+ _SUPPORT_USER_DESCRIPTION
+ "\n" + _DIVIDER
+ "📋 <b>ПОДПИСКА REMNAWAVE</b>\n\n"
+ "{status_icon} <b>Статус:</b> {status}\n"
+ "📅 <b>Истекает:</b> {expire_date}\n"
+ "⏳ <b>Осталось:</b> {days_left}\n"
+ "📊 <b>Трафик:</b> {used_traffic}\n"
+ "📱 <b>HWID лимит:</b> {hwid_limit}\n"
+ "🎯 <b>Internal Squads:</b> {squads}\n\n"
+ _DIVIDER
+ "🔑 <b>UUID в Remnawave:</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>Username:</b> {username_display}\n"
"🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n" "🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + _DIVIDER
"⚠️ <b>Аккаунт в Remnawave не найден</b>" + "⚠️ <b>Аккаунт в Remnawave не найден</b>"
) )
ADMIN_STANDARD_FALLBACK = ( ADMIN_STANDARD_FALLBACK = (
"<b>❌ Что-то пошло не так</b>, обратитесь к логам для подробностей." "<b>❌ Что-то пошло не так</b>, обратитесь к логам для подробностей."
) )
## FUNCTIONS # ============================================================
def build_main_menu_text() -> str: # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
return MAIN_MENU.format(quote=fetch_quote()) # ============================================================
def build_main_menu_text(link: str) -> str:
NO_USERNAME = "нет юзернейма" return MAIN_MENU.format(quote=fetch_quote(), link=link)
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 = "🍃"