Add referral system and user balance tracking
Introduce balance field to User model with Alembic migration. Add referral menu, admin /refpay command, and deep link sharing. Update Dockerfile to multi-stage build and add Postgres service to compose. Fix deep link parsing logic and conditional proxy initialization.
This commit is contained in:
29
.env.example
Normal file
29
.env.example
Normal file
@@ -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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -130,6 +130,7 @@ celerybeat.pid
|
||||
|
||||
# Environments
|
||||
.env
|
||||
dev.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@@ -173,6 +174,3 @@ cython_debug/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Static / IMG
|
||||
static/img
|
||||
44
Dockerfile
44
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"]
|
||||
|
||||
32
alembic/versions/f40cab941564_user_balance.py
Normal file
32
alembic/versions/f40cab941564_user_balance.py
Normal file
@@ -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 ###
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
63
handlers/referals.py
Normal file
63
handlers/referals.py
Normal file
@@ -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"<b>💸 Баланс реферала ({referal.id}) пополнен на {amount}₽</b>"
|
||||
)
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
def referals_kb(share_link: str) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardBuilder(
|
||||
[
|
||||
[InlineKeyboardButton(text="✍️ Перейти в поддержку", callback_data="checkout")],
|
||||
[InlineKeyboardButton(text="🔗 Поделиться ссылкой", url=share_link)],
|
||||
[_return_to_menu_btn],
|
||||
]
|
||||
).as_markup()
|
||||
|
||||
2
main.py
2
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)
|
||||
|
||||
|
||||
@@ -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"<code>{link or '...'}</code>"
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
1
prob
1
prob
@@ -1 +0,0 @@
|
||||
Мне нужно, чтобы ссылка для подключения пользователя была доступна в главном меню, но если при каждом /start я буду делать запрос, каждая обработка будет занимать по году. Как это реализовать?
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
25
texts.py
25
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
|
||||
@@ -109,6 +109,18 @@ CONTEXT_REINITIATED = (
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# РЕФЕРАЛЬНАЯ СИСТЕМА
|
||||
# ============================================================
|
||||
|
||||
REFERALS_MENU = (
|
||||
"<b>👥 Реферальная система</b>\n\n"
|
||||
f"<b>{MALENIA_COINS} Ваш реферальный баланс:</b> "
|
||||
"<code>{balance}₽</code>\n"
|
||||
f"<b>{MALENIA_LINK} Ваша реферальная ссылка:</b> "
|
||||
"<code>{link}</code>"
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# ОФОРМЛЕНИЕ ПОДПИСКИ
|
||||
# ============================================================
|
||||
@@ -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 = (
|
||||
"👤 <b>Имя:</b> {full_name}\n"
|
||||
"🔗 <b>Username:</b> {username_display}\n"
|
||||
"🆔 <b>Telegram ID:</b> <code>{telegram_id}</code>\n\n"
|
||||
"💰 <b>Реферальный баланс:</b> <code>{balance}</code>"
|
||||
)
|
||||
|
||||
_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||
@@ -171,7 +183,8 @@ _DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━
|
||||
SUPPORT_SUBSCRIPTION = (
|
||||
"<b>🎫 ЗАЯВКА НА ПОКУПКУ:</b>\n\n"
|
||||
+ _SUPPORT_USER_DESCRIPTION
|
||||
+ "\n" + _DIVIDER
|
||||
+ "\n"
|
||||
+ _DIVIDER
|
||||
+ "🗓️ Подписка на <b>{duration}</b> месяцев.\n"
|
||||
+ "📱 Количество устройств: <b>{devices}</b>\n"
|
||||
+ "🏳️ Белые списки: <b>{whitelist_description}</b>\n\n"
|
||||
@@ -181,7 +194,8 @@ SUPPORT_SUBSCRIPTION = (
|
||||
ADMIN_TICKET_WITH_RW = (
|
||||
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
||||
+ _SUPPORT_USER_DESCRIPTION
|
||||
+ "\n" + _DIVIDER
|
||||
+ "\n"
|
||||
+ _DIVIDER
|
||||
+ "📋 <b>ПОДПИСКА REMNAWAVE</b>\n\n"
|
||||
+ "{status_icon} <b>Статус:</b> {status}\n"
|
||||
+ "📅 <b>Истекает:</b> {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)
|
||||
Reference in New Issue
Block a user