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)