diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b0ef6b1 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +# Telegram Bot Token (Required) +# Get it from @BotFather on Telegram +BOT_TOKEN=your_bot_token_here + +# Telegram Admin Group ID (Required) +# The numeric ID of the group where admin notifications will be sent +ADMIN_GROUP_ID=your_admin_group_id_here + +# Remnawave Configuration (Required) +REMNAWAVE_URL=https://your-remnawave-instance.com +REMNAWAVE_TOKEN=your_remnawave_api_token_here +SUBSCRIPTION_URL=https://your-subscription-page.com + +# PostgreSQL Database Configuration +# These variables are used by docker-compose to set up the database container +POSTGRES_USER=malenia +POSTGRES_PASSWORD=change_me_to_a_secure_password +POSTGRES_DB=malenia_db + +# Proxy Configuration (Optional) +# Leave empty if no proxy is needed +PROXY= + +# Pricing and Limits Configuration (Optional - defaults provided in code) +MIN_DEVICES=3 +MAX_DEVICES=12 +PER_DEVICE_COST=50 +WHITELIST_COST=100 +WHITELIST_DEVICE_THRESHOLD=6 diff --git a/.gitignore b/.gitignore index fb5aab0..852a71b 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,7 @@ celerybeat.pid # Environments .env +dev.env .venv env/ venv/ @@ -172,7 +173,4 @@ cython_debug/ .ruff_cache/ # PyPI configuration file -.pypirc - -# Static / IMG -static/img \ No newline at end of file +.pypirc \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 292cc94..cae46cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,38 @@ -FROM python:3.13-slim +# Stage 1: Builder - Install dependencies +FROM python:3.13-slim AS builder + +WORKDIR /build + +# Install build-time dependencies required for compiling packages like asyncpg +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + +# Stage 2: Runner - Final lightweight image +FROM python:3.13-slim AS runner + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 WORKDIR /app -# Install system dependencies +# Install runtime dependencies required by asyncpg RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ + libpq5 \ && rm -rf /var/lib/apt/lists/* -# Copy requirements first for better layer caching -COPY requirements.txt . +# Copy installed Python packages from the builder stage +COPY --from=builder /install /usr/local -# Install Python dependencies -RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir -r requirements.txt - -# Copy entrypoint script -COPY entrypoints/startup.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Copy application code (will be overridden by volume mount in development) +# Copy application source code COPY . . -ENTRYPOINT ["/entrypoint.sh"] +# Ensure the startup script is executable +RUN chmod +x entrypoints/startup.sh + +# Default command runs linting, migrations, and starts the bot +CMD ["entrypoints/startup.sh"] diff --git a/alembic/versions/f40cab941564_user_balance.py b/alembic/versions/f40cab941564_user_balance.py new file mode 100644 index 0000000..db06689 --- /dev/null +++ b/alembic/versions/f40cab941564_user_balance.py @@ -0,0 +1,32 @@ +"""user -> +balance. + +Revision ID: f40cab941564 +Revises: 5985b2df0830 +Create Date: 2026-04-10 21:11:15.209314 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "f40cab941564" +down_revision: Union[str, Sequence[str], None] = "5985b2df0830" +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("balance", sa.INTEGER(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("users", "balance") + # ### end Alembic commands ### diff --git a/db/models/users.py b/db/models/users.py index 2ef7a7d..8150668 100644 --- a/db/models/users.py +++ b/db/models/users.py @@ -1,4 +1,4 @@ -from sqlalchemy import BIGINT, TEXT +from sqlalchemy import BIGINT, INTEGER, TEXT from sqlalchemy.orm import Mapped, mapped_column from db.base import Base @@ -12,3 +12,4 @@ class User(Base): ) referal_id: Mapped[int] = mapped_column(BIGINT, nullable=True) subscription_link: Mapped[str] = mapped_column(TEXT, nullable=True, unique=True) + balance: Mapped[int] = mapped_column(INTEGER, nullable=False, default=0) diff --git a/docker-compose.yml b/docker-compose.yml index c4a02a3..d651a01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,46 @@ services: + postgres: + image: postgres:16-alpine + container_name: maleniabot_db + environment: + POSTGRES_USER: ${POSTGRES_USER:-malenia} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-malenia_password} + POSTGRES_DB: ${POSTGRES_DB:-malenia_db} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U ${POSTGRES_USER:-malenia} -d ${POSTGRES_DB:-malenia_db}", + ] + interval: 5s + timeout: 10s + retries: 5 + bot: build: - context: . dockerfile: Dockerfile - volumes: - - .:/app - env_file: - - .env + container_name: maleniabot_app + depends_on: + postgres: + condition: service_healthy + environment: + BOT_TOKEN: ${BOT_TOKEN} + POSTGRES_URL: postgresql+asyncpg://${POSTGRES_USER:-malenia}:${POSTGRES_PASSWORD:-malenia_password}@postgres:5432/${POSTGRES_DB:-malenia_db} + PROXY: ${PROXY:-} + ADMIN_GROUP_ID: ${ADMIN_GROUP_ID} + REMNAWAVE_URL: ${REMNAWAVE_URL} + SUBSCRIPTION_URL: ${SUBSCRIPTION_URL} + REMNAWAVE_TOKEN: ${REMNAWAVE_TOKEN} + MIN_DEVICES: ${MIN_DEVICES:-3} + MAX_DEVICES: ${MAX_DEVICES:-12} + PER_DEVICE_COST: ${PER_DEVICE_COST:-50} + WHITELIST_COST: ${WHITELIST_COST:-100} + WHITELIST_DEVICE_THRESHOLD: ${WHITELIST_DEVICE_THRESHOLD:-6} restart: unless-stopped - # Optional: uncomment to run with auto-reload for development - # command: python -m watchfiles main.py - stdin_open: true - tty: true + +volumes: + postgres_data: diff --git a/handlers/__init__.py b/handlers/__init__.py index 4a98d04..dab8038 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -1,5 +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 -routers = [menus_router, support_router, buy_router] +routers = [referal_router, menus_router, support_router, buy_router] diff --git a/handlers/menus.py b/handlers/menus.py index 1136486..60f6309 100644 --- a/handlers/menus.py +++ b/handlers/menus.py @@ -12,7 +12,7 @@ from texts import GENERAL_PROCESSING, build_main_menu_text, FAQ_TEXT router = Router() -@router.message(CommandStart(deep_link=True, deep_link_encoded=False)) +@router.message(CommandStart(deep_link=True, deep_link_encoded=True)) async def fetch_referal( msg: Message, command: CommandObject, @@ -22,12 +22,12 @@ async def fetch_referal( ): await state.clear() - data = command.args.split() + data = command.args.split("_") if not data: await standard_start(msg, command, state) return - referal = data[0] + referal = int(data[1]) user = await deps.user_service.add_user( session, user_id=msg.from_user.id, referal=referal, rw_sdk=deps.rw_sdk ) @@ -41,7 +41,6 @@ async def fetch_referal( @router.message(Command(commands=["start", "cancel"])) async def standard_start( msg: Message, - command: CommandObject, state: FSMContext, session: AsyncSession, deps: DependenciesDTO, diff --git a/handlers/referals.py b/handlers/referals.py new file mode 100644 index 0000000..aa6f024 --- /dev/null +++ b/handlers/referals.py @@ -0,0 +1,63 @@ +from aiogram import Router, F +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 sqlalchemy.ext.asyncio import AsyncSession + +from keyboards.client import referals_kb +from misc.utils import format_share_deep_link +from schemas.di import DependenciesDTO +from texts import GENERAL_PROCESSING, REFERALS_MENU +from config import settings + +router = Router() + + +@router.callback_query(F.data == "referal") +async def referal_menu( + cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO +): + await state.clear() + + user = await deps.user_repository.get_user_by_id(session, cb.from_user.id) + referal_link = await create_start_link(cb.bot, f"ref_{user.id}", encode=True) + share_link = format_share_deep_link(referal_link) + + await cb.message.edit_text( + REFERALS_MENU.format(balance=user.balance, link=referal_link), + reply_markup=referals_kb(share_link), + ) + + +@router.message(F.chat.id == settings.admin_group_id, Command("refpay")) +async def refpay( + msg: Message, + command: CommandObject, + state: FSMContext, + session: AsyncSession, + deps: DependenciesDTO, +): + await state.clear() + + await msg.delete() + status_msg = await msg.answer(GENERAL_PROCESSING) + user = await deps.user_service.fetch_user_by_thread(session, msg.message_thread_id) + referal = await deps.user_repository.get_user_by_id(session, user.referal_id) + if not referal: + await status_msg.edit_text("❌ У пользователя нет реферала.") + return + + amount = command.args + + if not amount.isdigit(): + await status_msg.edit_text( + "❌ Укажите в аргументах количество средств, которые необходимо перевести рефералу пользователя." + ) + return + + amount = int(amount) + await deps.user_repository.increase_user_balance(session, user.id, amount) + await status_msg.edit_text( + f"💸 Баланс реферала ({referal.id}) пополнен на {amount}₽" + ) diff --git a/handlers/support.py b/handlers/support.py index 05ec05b..ad83715 100644 --- a/handlers/support.py +++ b/handlers/support.py @@ -51,6 +51,7 @@ async def received_message( F.message_thread_id.is_not(None), F.from_user.is_bot == False, # noqa: E712 ~Command("close"), + ~Command("refpay"), F.text, ) async def handle_admin_message( diff --git a/keyboards/client.py b/keyboards/client.py index 3ee70f3..744841a 100644 --- a/keyboards/client.py +++ b/keyboards/client.py @@ -5,7 +5,7 @@ from config import settings from misc.utils import convert_duration_to_int from schemas.subscriptions import SubscriptionDuration -main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( +main_menu = InlineKeyboardBuilder( [ [ InlineKeyboardButton(text="⚙️ Купить", callback_data="buy"), @@ -16,6 +16,7 @@ main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"), ], [InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")], + [InlineKeyboardButton(text="👤 Реферальная система", callback_data="referal")], ] ).as_markup() @@ -23,6 +24,17 @@ _return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton( text="⬅️ Назад", callback_data="menu:main" ) +return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( + [[_return_to_menu_btn]] +).as_markup() + +payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder( + [ + [InlineKeyboardButton(text="✍️ Перейти в поддержку", callback_data="checkout")], + [_return_to_menu_btn], + ] +).as_markup() + def _return_btn( data: str = "menu:main", text: str = "⬅️ Назад" @@ -30,11 +42,6 @@ def _return_btn( return InlineKeyboardButton(text=text, callback_data=data) -return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( - [[_return_to_menu_btn]] -).as_markup() - - def devices_selector(current: int = settings.min_devices, whitelists: bool = False): builder = InlineKeyboardBuilder() @@ -89,9 +96,10 @@ def duration_selector( return builder.as_markup() -payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder( - [ - [InlineKeyboardButton(text="✍️ Перейти в поддержку", callback_data="checkout")], - [_return_to_menu_btn], - ] -).as_markup() +def referals_kb(share_link: str) -> InlineKeyboardMarkup: + return InlineKeyboardBuilder( + [ + [InlineKeyboardButton(text="🔗 Поделиться ссылкой", url=share_link)], + [_return_to_menu_btn], + ] + ).as_markup() diff --git a/main.py b/main.py index d89c2a8..975ec61 100644 --- a/main.py +++ b/main.py @@ -38,7 +38,7 @@ async def main(): ) dp.update.middleware.register(DIMiddleware(deps, async_session)) - aiohttp_session = AiohttpSession(proxy=settings.proxy) + aiohttp_session = AiohttpSession(proxy=settings.proxy) if settings.proxy else None default = DefaultBotProperties(parse_mode="HTML") bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session) diff --git a/misc/utils.py b/misc/utils.py index 632e2bb..4b0e6f0 100644 --- a/misc/utils.py +++ b/misc/utils.py @@ -1,3 +1,4 @@ +import urllib.parse from typing import Optional from random import choice from datetime import datetime, timedelta, timezone @@ -175,10 +176,15 @@ def format_subscription_link(link: Optional[str] = None): return f"{link or '...'}" +def format_share_deep_link(link: str): + return f"https://t.me/share/url/?url={urllib.parse.quote_plus(link)}" + + def format_support_subscription( subscription: SubscriptionPlan, full_name: str, user_id: int, + balance: int, username: Optional[str] = None, ) -> str: username = format_username(username) @@ -196,6 +202,7 @@ def format_support_subscription( devices=subscription.devices, whitelist_description=whitelist_description, price=calculate_price(subscription), + balance=balance, ) diff --git a/prob b/prob deleted file mode 100644 index a8ca72a..0000000 --- a/prob +++ /dev/null @@ -1 +0,0 @@ -Мне нужно, чтобы ссылка для подключения пользователя была доступна в главном меню, но если при каждом /start я буду делать запрос, каждая обработка будет занимать по году. Как это реализовать? \ No newline at end of file diff --git a/repositories/users.py b/repositories/users.py index 8bcc759..446148f 100644 --- a/repositories/users.py +++ b/repositories/users.py @@ -47,3 +47,25 @@ class UserRepository: await session.commit() return user + + async def update_user_balance( + self, session: AsyncSession, user_id: int, balance: int + ) -> Optional[User]: + user = await self.get_user_by_id(session, user_id=user_id) + if not user: + return None + user.balance = balance + + await session.commit() + return user + + async def increase_user_balance( + self, session: AsyncSession, user_id: int, amount: int + ) -> Optional[User]: + user = await self.get_user_by_id(session, user_id=user_id) + if not user: + return None + user.balance += amount + + await session.commit() + return user diff --git a/services/user_service.py b/services/user_service.py index 13a4b07..969dfec 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -49,7 +49,7 @@ class UserService: @staticmethod async def format_admin_support_message( - ticket_number: int, user: User, rw_user: Optional[RWUserInfo] + ticket_number: int, user: User, rw_user: Optional[RWUserInfo], balance: int ): username_display = utils.format_username(user.username) @@ -76,6 +76,7 @@ class UserService: full_name=user.full_name, username_display=username_display, telegram_id=user.id, + balance=balance, status_icon=status_icon, status=status_text, expire_date=expire_date, @@ -98,8 +99,10 @@ class UserService: 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 + link = utils.get_subscription_link(rw_user.short_uuid) if rw_user else None + referal = ( + int(referal) if str(referal).isdigit() and user_id != int(referal) else None + ) model = NewUserDTO(id=user_id, referal=referal, link=link) return await self.user_repository.create_user(session, model) @@ -114,6 +117,7 @@ class UserService: ticket = await self.ticket_repository.get_available_ticket_by_user_id( session, user.id ) + db_user = await self.user_repository.get_user_by_id(session, user.id) if ticket: try: @@ -121,7 +125,11 @@ class UserService: 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 + subscription=subscription, + full_name=user.full_name, + user_id=user.id, + username=user.username, + balance=db_user.balance, ), ) return @@ -148,12 +156,17 @@ class UserService: ticket = await self.ticket_repository.update_ticket_thread_id( session, ticket, thread.message_thread_id ) + db_user = await self.user_repository.get_user_by_id(session, ticket.creator_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 + subscription=subscription, + full_name=user.full_name, + user_id=user.id, + username=user.username, + balance=db_user, ), ) @@ -204,7 +217,10 @@ class UserService: settings.admin_group_id, message_thread_id=thread.message_thread_id, text=await self.format_admin_support_message( - ticket_number=ticket.id, user=msg.from_user, rw_user=rw_user + ticket_number=ticket.id, + user=msg.from_user, + rw_user=rw_user, + balance=user.balance, ), ) await msg.copy_to( @@ -269,3 +285,23 @@ class UserService: user = await self.user_repository.update_user_link(session, user_id, link) return user + + async def fetch_user_by_thread( + self, session: AsyncSession, thread_id: int + ) -> Optional[User]: + ticket = await self.ticket_repository.get_ticket_by_thread_id( + session, thread_id=thread_id + ) + + return await self.user_repository.get_user_by_id(session, ticket.creator_id) + + async def fetch_referal_by_thread( + self, session: AsyncSession, thread_id: int + ) -> Optional[User]: + ticket = await self.ticket_repository.get_ticket_by_thread_id( + session, thread_id=thread_id + ) + + return await self.user_repository.get_referal_by_user_id( + session, ticket.creator_id + ) diff --git a/texts.py b/texts.py index 953beaa..969ea18 100644 --- a/texts.py +++ b/texts.py @@ -1,11 +1,11 @@ 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 @@ -16,11 +16,11 @@ class PremiumEmoji: # — Иконки бота -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, "🎧") @@ -29,8 +29,8 @@ MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧") # ============================================================ TICKET_STATUS_EMOJIS = { - TicketStatus.CLOSED.value: "🔴", - TicketStatus.OPEN.value: "🟢", + TicketStatus.CLOSED.value: "🔴", + TicketStatus.OPEN.value: "🟢", TicketStatus.PENDING.value: "🟡", } @@ -40,18 +40,18 @@ TICKET_STATUS_EMOJIS = { # ============================================================ # — Иконки статусов -STATUS_ICON_ACTIVE = "🟢" +STATUS_ICON_ACTIVE = "🟢" STATUS_ICON_DISABLED = "🔴" -STATUS_ICON_EXPIRED = "🟡" -STATUS_ICON_LIMITED = "🟠" -STATUS_ICON_UNKNOWN = "⚪" +STATUS_ICON_EXPIRED = "🟡" +STATUS_ICON_LIMITED = "🟠" +STATUS_ICON_UNKNOWN = "⚪" # — Тексты статусов -STATUS_ACTIVE = "Активна" +STATUS_ACTIVE = "Активна" STATUS_DISABLED = "Отключена" -STATUS_EXPIRED = "Истекла" -STATUS_LIMITED = "Превышен лимит трафика" -STATUS_UNKNOWN = "Неизвестен" +STATUS_EXPIRED = "Истекла" +STATUS_LIMITED = "Превышен лимит трафика" +STATUS_UNKNOWN = "Неизвестен" # ============================================================ @@ -60,11 +60,11 @@ STATUS_UNKNOWN = "Неизвестен" # — Дни до истечения подписки DAYS_LEFT_POSITIVE = "{days} дн." -DAYS_LEFT_EXPIRED = "истекла {days} дн. назад" -DAYS_LEFT_TODAY = "истекает сегодня" +DAYS_LEFT_EXPIRED = "истекла {days} дн. назад" +DAYS_LEFT_TODAY = "истекает сегодня" # — Username -NO_USERNAME = "нет username" +NO_USERNAME = "нет username" USERNAME_DISPLAY = "@{username}" # — Internal Squads @@ -74,9 +74,9 @@ NO_SQUADS = "нет" UNLIMITED_HWID = "без ограничений" # — Трафик -NO_TRAFFIC_LIMIT = "без ограничений" +NO_TRAFFIC_LIMIT = "без ограничений" TRAFFIC_WITH_LIMIT = "{used} / {limit}" -TRAFFIC_NO_LIMIT = "{used}" +TRAFFIC_NO_LIMIT = "{used}" # — Прочее NOTHING_PLACEHOLDER = "🍃" @@ -109,6 +109,18 @@ CONTEXT_REINITIATED = ( ) +# ============================================================ +# РЕФЕРАЛЬНАЯ СИСТЕМА +# ============================================================ + +REFERALS_MENU = ( + "👥 Реферальная система\n\n" + f"{MALENIA_COINS} Ваш реферальный баланс: " + "{balance}₽\n" + f"{MALENIA_LINK} Ваша реферальная ссылка: " + "{link}" +) + # ============================================================ # ОФОРМЛЕНИЕ ПОДПИСКИ # ============================================================ @@ -126,8 +138,7 @@ WHITELISTS_ALREADY_ON = "уже включены!" TEMP_SUPPORT_FORM = "обратитесь в поддержку, прикрепив текст ниже:\n\n" CHECKOUT_PAYMENT_CHOICE = ( - "в поддержку чирканите епта там всё обьяснят\n" - + TEMP_SUPPORT_FORM + "в поддержку чирканите епта там всё обьяснят\n" + TEMP_SUPPORT_FORM ) @@ -159,6 +170,7 @@ _SUPPORT_USER_DESCRIPTION = ( "👤 Имя: {full_name}\n" "🔗 Username: {username_display}\n" "🆔 Telegram ID: {telegram_id}\n\n" + "💰 Реферальный баланс: {balance}" ) _DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" @@ -171,7 +183,8 @@ _DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━ SUPPORT_SUBSCRIPTION = ( "🎫 ЗАЯВКА НА ПОКУПКУ:\n\n" + _SUPPORT_USER_DESCRIPTION - + "\n" + _DIVIDER + + "\n" + + _DIVIDER + "🗓️ Подписка на {duration} месяцев.\n" + "📱 Количество устройств: {devices}\n" + "🏳️ Белые списки: {whitelist_description}\n\n" @@ -181,7 +194,8 @@ SUPPORT_SUBSCRIPTION = ( ADMIN_TICKET_WITH_RW = ( "🎫 ТИКЕТ T-{ticket_number:04d}\n\n" + _SUPPORT_USER_DESCRIPTION - + "\n" + _DIVIDER + + "\n" + + _DIVIDER + "📋 ПОДПИСКА REMNAWAVE\n\n" + "{status_icon} Статус: {status}\n" + "📅 Истекает: {expire_date}\n" @@ -211,5 +225,6 @@ ADMIN_STANDARD_FALLBACK = ( # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ # ============================================================ + def build_main_menu_text(link: str) -> str: - return MAIN_MENU.format(quote=fetch_quote(), link=link) \ No newline at end of file + return MAIN_MENU.format(quote=fetch_quote(), link=link)