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
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
dev.env
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
@@ -173,6 +174,3 @@ cython_debug/
|
|||||||
|
|
||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.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
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
# Install runtime dependencies required by asyncpg
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
libpq5 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements first for better layer caching
|
# Copy installed Python packages from the builder stage
|
||||||
COPY requirements.txt .
|
COPY --from=builder /install /usr/local
|
||||||
|
|
||||||
# Install Python dependencies
|
# Copy application source code
|
||||||
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 . .
|
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 sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from db.base import Base
|
from db.base import Base
|
||||||
@@ -12,3 +12,4 @@ class User(Base):
|
|||||||
)
|
)
|
||||||
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)
|
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:
|
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:
|
bot:
|
||||||
build:
|
build:
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
volumes:
|
container_name: maleniabot_app
|
||||||
- .:/app
|
depends_on:
|
||||||
env_file:
|
postgres:
|
||||||
- .env
|
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
|
restart: unless-stopped
|
||||||
# Optional: uncomment to run with auto-reload for development
|
|
||||||
# command: python -m watchfiles main.py
|
volumes:
|
||||||
stdin_open: true
|
postgres_data:
|
||||||
tty: true
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
from .referals import router as referal_router
|
||||||
from .menus import router as menus_router
|
from .menus import router as menus_router
|
||||||
from .support import router as support_router
|
from .support import router as support_router
|
||||||
from .buy import router as buy_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 = 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(
|
async def fetch_referal(
|
||||||
msg: Message,
|
msg: Message,
|
||||||
command: CommandObject,
|
command: CommandObject,
|
||||||
@@ -22,12 +22,12 @@ async def fetch_referal(
|
|||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
data = command.args.split()
|
data = command.args.split("_")
|
||||||
if not data:
|
if not data:
|
||||||
await standard_start(msg, command, state)
|
await standard_start(msg, command, state)
|
||||||
return
|
return
|
||||||
|
|
||||||
referal = data[0]
|
referal = int(data[1])
|
||||||
user = await deps.user_service.add_user(
|
user = await deps.user_service.add_user(
|
||||||
session, user_id=msg.from_user.id, referal=referal, rw_sdk=deps.rw_sdk
|
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"]))
|
@router.message(Command(commands=["start", "cancel"]))
|
||||||
async def standard_start(
|
async def standard_start(
|
||||||
msg: Message,
|
msg: Message,
|
||||||
command: CommandObject,
|
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
deps: DependenciesDTO,
|
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.message_thread_id.is_not(None),
|
||||||
F.from_user.is_bot == False, # noqa: E712
|
F.from_user.is_bot == False, # noqa: E712
|
||||||
~Command("close"),
|
~Command("close"),
|
||||||
|
~Command("refpay"),
|
||||||
F.text,
|
F.text,
|
||||||
)
|
)
|
||||||
async def handle_admin_message(
|
async def handle_admin_message(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from config import settings
|
|||||||
from misc.utils import convert_duration_to_int
|
from misc.utils import convert_duration_to_int
|
||||||
from schemas.subscriptions import SubscriptionDuration
|
from schemas.subscriptions import SubscriptionDuration
|
||||||
|
|
||||||
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
main_menu = InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(text="⚙️ Купить", callback_data="buy"),
|
InlineKeyboardButton(text="⚙️ Купить", callback_data="buy"),
|
||||||
@@ -16,6 +16,7 @@ main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
|||||||
InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"),
|
InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"),
|
||||||
],
|
],
|
||||||
[InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")],
|
[InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")],
|
||||||
|
[InlineKeyboardButton(text="👤 Реферальная система", callback_data="referal")],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
@@ -23,6 +24,17 @@ _return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton(
|
|||||||
text="⬅️ Назад", callback_data="menu:main"
|
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(
|
def _return_btn(
|
||||||
data: str = "menu:main", text: str = "⬅️ Назад"
|
data: str = "menu:main", text: str = "⬅️ Назад"
|
||||||
@@ -30,11 +42,6 @@ def _return_btn(
|
|||||||
return InlineKeyboardButton(text=text, callback_data=data)
|
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):
|
def devices_selector(current: int = settings.min_devices, whitelists: bool = False):
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
@@ -89,9 +96,10 @@ def duration_selector(
|
|||||||
return builder.as_markup()
|
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],
|
[_return_to_menu_btn],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -38,7 +38,7 @@ async def main():
|
|||||||
)
|
)
|
||||||
dp.update.middleware.register(DIMiddleware(deps, async_session))
|
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")
|
default = DefaultBotProperties(parse_mode="HTML")
|
||||||
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
|
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import urllib.parse
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from random import choice
|
from random import choice
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
@@ -175,10 +176,15 @@ def format_subscription_link(link: Optional[str] = None):
|
|||||||
return f"<code>{link or '...'}</code>"
|
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(
|
def format_support_subscription(
|
||||||
subscription: SubscriptionPlan,
|
subscription: SubscriptionPlan,
|
||||||
full_name: str,
|
full_name: str,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
|
balance: int,
|
||||||
username: Optional[str] = None,
|
username: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
username = format_username(username)
|
username = format_username(username)
|
||||||
@@ -196,6 +202,7 @@ def format_support_subscription(
|
|||||||
devices=subscription.devices,
|
devices=subscription.devices,
|
||||||
whitelist_description=whitelist_description,
|
whitelist_description=whitelist_description,
|
||||||
price=calculate_price(subscription),
|
price=calculate_price(subscription),
|
||||||
|
balance=balance,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
prob
1
prob
@@ -1 +0,0 @@
|
|||||||
Мне нужно, чтобы ссылка для подключения пользователя была доступна в главном меню, но если при каждом /start я буду делать запрос, каждая обработка будет занимать по году. Как это реализовать?
|
|
||||||
@@ -47,3 +47,25 @@ class UserRepository:
|
|||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return user
|
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
|
@staticmethod
|
||||||
async def format_admin_support_message(
|
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)
|
username_display = utils.format_username(user.username)
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ class UserService:
|
|||||||
full_name=user.full_name,
|
full_name=user.full_name,
|
||||||
username_display=username_display,
|
username_display=username_display,
|
||||||
telegram_id=user.id,
|
telegram_id=user.id,
|
||||||
|
balance=balance,
|
||||||
status_icon=status_icon,
|
status_icon=status_icon,
|
||||||
status=status_text,
|
status=status_text,
|
||||||
expire_date=expire_date,
|
expire_date=expire_date,
|
||||||
@@ -98,8 +99,10 @@ class UserService:
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
|
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
|
||||||
link = utils.get_subscription_link(rw_user.short_uuid)
|
link = utils.get_subscription_link(rw_user.short_uuid) if rw_user else None
|
||||||
referal = int(referal) if str(referal).isdigit() 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)
|
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)
|
||||||
@@ -114,6 +117,7 @@ class UserService:
|
|||||||
ticket = await self.ticket_repository.get_available_ticket_by_user_id(
|
ticket = await self.ticket_repository.get_available_ticket_by_user_id(
|
||||||
session, user.id
|
session, user.id
|
||||||
)
|
)
|
||||||
|
db_user = await self.user_repository.get_user_by_id(session, user.id)
|
||||||
|
|
||||||
if ticket:
|
if ticket:
|
||||||
try:
|
try:
|
||||||
@@ -121,7 +125,11 @@ class UserService:
|
|||||||
chat_id=settings.admin_group_id,
|
chat_id=settings.admin_group_id,
|
||||||
message_thread_id=ticket.thread_id,
|
message_thread_id=ticket.thread_id,
|
||||||
text=utils.format_support_subscription(
|
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
|
return
|
||||||
@@ -148,12 +156,17 @@ class UserService:
|
|||||||
ticket = await self.ticket_repository.update_ticket_thread_id(
|
ticket = await self.ticket_repository.update_ticket_thread_id(
|
||||||
session, ticket, thread.message_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(
|
await bot.send_message(
|
||||||
chat_id=settings.admin_group_id,
|
chat_id=settings.admin_group_id,
|
||||||
message_thread_id=ticket.thread_id,
|
message_thread_id=ticket.thread_id,
|
||||||
text=utils.format_support_subscription(
|
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,
|
settings.admin_group_id,
|
||||||
message_thread_id=thread.message_thread_id,
|
message_thread_id=thread.message_thread_id,
|
||||||
text=await self.format_admin_support_message(
|
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(
|
await msg.copy_to(
|
||||||
@@ -269,3 +285,23 @@ class UserService:
|
|||||||
user = await self.user_repository.update_user_link(session, user_id, link)
|
user = await self.user_repository.update_user_link(session, user_id, link)
|
||||||
|
|
||||||
return user
|
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 misc.utils import fetch_quote
|
||||||
from db.models.tickets import TicketStatus
|
from db.models.tickets import TicketStatus
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# PREMIUM EMOJI
|
# 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
|
||||||
@@ -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"
|
TEMP_SUPPORT_FORM = "обратитесь в поддержку, прикрепив текст ниже:\n\n"
|
||||||
|
|
||||||
CHECKOUT_PAYMENT_CHOICE = (
|
CHECKOUT_PAYMENT_CHOICE = (
|
||||||
"в поддержку чирканите епта там всё обьяснят\n"
|
"в поддержку чирканите епта там всё обьяснят\n" + TEMP_SUPPORT_FORM
|
||||||
+ TEMP_SUPPORT_FORM
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,6 +170,7 @@ _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"
|
||||||
|
"💰 <b>Реферальный баланс:</b> <code>{balance}</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||||
@@ -171,7 +183,8 @@ _DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━
|
|||||||
SUPPORT_SUBSCRIPTION = (
|
SUPPORT_SUBSCRIPTION = (
|
||||||
"<b>🎫 ЗАЯВКА НА ПОКУПКУ:</b>\n\n"
|
"<b>🎫 ЗАЯВКА НА ПОКУПКУ:</b>\n\n"
|
||||||
+ _SUPPORT_USER_DESCRIPTION
|
+ _SUPPORT_USER_DESCRIPTION
|
||||||
+ "\n" + _DIVIDER
|
+ "\n"
|
||||||
|
+ _DIVIDER
|
||||||
+ "🗓️ Подписка на <b>{duration}</b> месяцев.\n"
|
+ "🗓️ Подписка на <b>{duration}</b> месяцев.\n"
|
||||||
+ "📱 Количество устройств: <b>{devices}</b>\n"
|
+ "📱 Количество устройств: <b>{devices}</b>\n"
|
||||||
+ "🏳️ Белые списки: <b>{whitelist_description}</b>\n\n"
|
+ "🏳️ Белые списки: <b>{whitelist_description}</b>\n\n"
|
||||||
@@ -181,7 +194,8 @@ SUPPORT_SUBSCRIPTION = (
|
|||||||
ADMIN_TICKET_WITH_RW = (
|
ADMIN_TICKET_WITH_RW = (
|
||||||
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
||||||
+ _SUPPORT_USER_DESCRIPTION
|
+ _SUPPORT_USER_DESCRIPTION
|
||||||
+ "\n" + _DIVIDER
|
+ "\n"
|
||||||
|
+ _DIVIDER
|
||||||
+ "📋 <b>ПОДПИСКА REMNAWAVE</b>\n\n"
|
+ "📋 <b>ПОДПИСКА REMNAWAVE</b>\n\n"
|
||||||
+ "{status_icon} <b>Статус:</b> {status}\n"
|
+ "{status_icon} <b>Статус:</b> {status}\n"
|
||||||
+ "📅 <b>Истекает:</b> {expire_date}\n"
|
+ "📅 <b>Истекает:</b> {expire_date}\n"
|
||||||
@@ -211,5 +225,6 @@ ADMIN_STANDARD_FALLBACK = (
|
|||||||
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
def build_main_menu_text(link: str) -> str:
|
def build_main_menu_text(link: str) -> str:
|
||||||
return MAIN_MENU.format(quote=fetch_quote(), link=link)
|
return MAIN_MENU.format(quote=fetch_quote(), link=link)
|
||||||
Reference in New Issue
Block a user