feat: implement referral payment system and enhance ticket management
This commit is contained in:
@@ -31,8 +31,11 @@ COPY --from=builder /install /usr/local
|
||||
# Copy application source code
|
||||
COPY . .
|
||||
|
||||
RUN sed -i 's/\r$//' ./entrypoints/startup.sh && \
|
||||
chmod +x ./entrypoints/startup.sh
|
||||
|
||||
# Ensure the startup script is executable
|
||||
RUN chmod +x entrypoints/startup.sh
|
||||
RUN chmod +x ./entrypoints/startup.sh
|
||||
|
||||
# Default command runs linting, migrations, and starts the bot
|
||||
CMD ["entrypoints/startup.sh"]
|
||||
CMD ["sh", "./entrypoints/startup.sh"]
|
||||
|
||||
@@ -16,8 +16,8 @@ services:
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${POSTGRES_USER:-malenia} -d ${POSTGRES_DB:-malenia_db}",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
bot:
|
||||
|
||||
22
entrypoints/dev.sh
Normal file
22
entrypoints/dev.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "[+] Formatting and checking..."
|
||||
echo
|
||||
ruff check --fix
|
||||
black .
|
||||
|
||||
echo "[+] checking psql availability"
|
||||
if ! pg_isready -h localhost -p 5432 ; then
|
||||
echo "psql is down." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[+] psql is available!"
|
||||
echo
|
||||
|
||||
echo "[+] Running migrations..."
|
||||
echo
|
||||
alembic upgrade head
|
||||
|
||||
echo "[+] Starting bot..."
|
||||
python main.py
|
||||
@@ -1,19 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "[+] Formatting and checking..."
|
||||
echo
|
||||
ruff check --fix
|
||||
black .
|
||||
|
||||
echo "[+] checking psql availability"
|
||||
if ! pg_isready -h localhost -p 5432 ; then
|
||||
echo "psql is down." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[+] psql is available!"
|
||||
echo
|
||||
|
||||
echo "[+] Running migrations..."
|
||||
echo
|
||||
alembic upgrade head
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import settings
|
||||
from keyboards.admins import promotion_confirmation
|
||||
from keyboards.client import build_return_menu
|
||||
from keyboards.client import close_kb
|
||||
from schemas.di import DependenciesDTO
|
||||
from schemas.promotions import NewPromotionContext
|
||||
from states.admins import AdminStorage
|
||||
@@ -28,7 +28,7 @@ async def push_cmd(msg: Message, state: FSMContext):
|
||||
|
||||
ctx = NewPromotionContext(creator_id=msg.from_user.id)
|
||||
await msg.delete()
|
||||
await msg.answer(ADMIN_PROMOTION_INIT, reply_markup=build_return_menu("delete", "❌"))
|
||||
await msg.answer(ADMIN_PROMOTION_INIT, reply_markup=close_kb)
|
||||
|
||||
await state.set_state(AdminStorage.promotion_msg)
|
||||
await state.set_data({"ctx": ctx})
|
||||
|
||||
@@ -6,10 +6,19 @@ from aiogram.utils.deep_linking import create_start_link
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import settings
|
||||
from keyboards.client import referals_kb
|
||||
from keyboards.client import close_kb, referals_kb
|
||||
from misc.utils import format_share_deep_link
|
||||
from schemas.common import GeneralMessageContext
|
||||
from schemas.di import DependenciesDTO
|
||||
from texts import GENERAL_PROCESSING, REFERALS_MENU
|
||||
from states.admins import AdminStorage
|
||||
from texts import (
|
||||
GENERAL_PROCESSING,
|
||||
INVALID_ARGUMENT_REFPAY,
|
||||
REFERALS_MENU,
|
||||
SPECIFY_REFERAL_PAYMENT_AMOUNT,
|
||||
SUCCESSFUL_REFPAY,
|
||||
USER_NO_REFERAL,
|
||||
)
|
||||
|
||||
router = Router()
|
||||
|
||||
@@ -45,17 +54,70 @@ async def refpay(
|
||||
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("❌ У пользователя нет реферала.")
|
||||
await status_msg.edit_text(USER_NO_REFERAL)
|
||||
return
|
||||
|
||||
amount = command.args
|
||||
|
||||
if not amount.isdigit():
|
||||
await status_msg.edit_text(
|
||||
"❌ Укажите в аргументах количество средств, которые необходимо перевести рефералу пользователя."
|
||||
)
|
||||
await status_msg.edit_text(INVALID_ARGUMENT_REFPAY)
|
||||
return
|
||||
|
||||
amount = int(amount)
|
||||
await deps.user_repository.increase_user_balance(session, referal.id, amount)
|
||||
await status_msg.edit_text(f"<b>💸 Баланс реферала ({referal.id}) пополнен на {amount}₽</b>")
|
||||
await status_msg.edit_text(SUCCESSFUL_REFPAY.format(referal_id=referal.id, amount=amount))
|
||||
|
||||
|
||||
@router.callback_query(F.message.chat.id == settings.admin_group_id, F.data == "refpay")
|
||||
async def refpay_cb(
|
||||
cb: CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
deps: DependenciesDTO,
|
||||
):
|
||||
await state.clear()
|
||||
|
||||
user = await deps.user_service.fetch_user_by_thread(session, cb.message.message_thread_id)
|
||||
referal = await deps.user_repository.get_user_by_id(session, user.referal_id)
|
||||
|
||||
if not referal:
|
||||
await cb.message.answer(USER_NO_REFERAL, reply_markup=close_kb)
|
||||
return
|
||||
|
||||
msg = await cb.message.answer(
|
||||
SPECIFY_REFERAL_PAYMENT_AMOUNT, reply_markup=close_kb
|
||||
)
|
||||
ctx = GeneralMessageContext(msg=msg)
|
||||
await state.set_state(AdminStorage.refpay_amount)
|
||||
await state.set_data({"ctx": ctx})
|
||||
|
||||
|
||||
@router.message(
|
||||
AdminStorage.refpay_amount,
|
||||
F.chat.id == settings.admin_group_id,
|
||||
F.from_user.is_bot == False, # noqa: E712
|
||||
)
|
||||
async def refpay_execute(
|
||||
msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
|
||||
):
|
||||
data = await state.get_data()
|
||||
await state.clear()
|
||||
await msg.delete()
|
||||
|
||||
ctx: GeneralMessageContext | None = data.get("ctx")
|
||||
if ctx:
|
||||
status_msg = ctx.msg
|
||||
else:
|
||||
status_msg = await msg.answer(GENERAL_PROCESSING)
|
||||
|
||||
amount = msg.text
|
||||
if not amount.isdigit():
|
||||
await status_msg.edit_text(INVALID_ARGUMENT_REFPAY, reply_markup=close_kb)
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
amount = int(amount)
|
||||
await deps.user_repository.increase_user_balance(session, referal.id, amount)
|
||||
await status_msg.edit_text(SUCCESSFUL_REFPAY.format(referal_id=referal.id, amount=amount))
|
||||
|
||||
@@ -79,3 +79,12 @@ async def close_ticket(
|
||||
await state.clear()
|
||||
|
||||
await deps.user_service.close_ticket(session, msg)
|
||||
|
||||
|
||||
@router.callback_query(F.message.chat.id == settings.admin_group_id, F.data == "close")
|
||||
async def close_ticket_cb(
|
||||
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
|
||||
):
|
||||
await state.clear()
|
||||
|
||||
await deps.user_service.close_ticket(session, cb.message)
|
||||
|
||||
@@ -7,3 +7,10 @@ promotion_confirmation: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||
[InlineKeyboardButton(text="❌", callback_data="decline")],
|
||||
]
|
||||
).as_markup()
|
||||
|
||||
ticket_actions: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||
[
|
||||
[InlineKeyboardButton(text="🔒", callback_data="close")],
|
||||
[InlineKeyboardButton(text="💸 Пополнить баланс реферала", callback_data="refpay")],
|
||||
]
|
||||
).as_markup()
|
||||
|
||||
@@ -33,6 +33,7 @@ payment_gateways: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||
]
|
||||
).as_markup()
|
||||
|
||||
close_kb: InlineKeyboardMarkup = InlineKeyboardBuilder([[InlineKeyboardButton(text="❌", callback_data="delete")]]).as_markup()
|
||||
|
||||
def _return_btn(data: str = "menu:main", text: str = "⬅️ Назад") -> InlineKeyboardButton:
|
||||
return InlineKeyboardButton(text=text, callback_data=data)
|
||||
|
||||
@@ -131,6 +131,12 @@ def format_username(username: str | None) -> str:
|
||||
return texts.NO_USERNAME
|
||||
|
||||
|
||||
def format_referal_id(referal_id: int | None) -> str:
|
||||
if referal_id:
|
||||
return str(referal_id)
|
||||
return texts.NOTHING_PLACEHOLDER
|
||||
|
||||
|
||||
def convert_duration_to_int(duration: SubscriptionDuration) -> int:
|
||||
mapping: dict[str, int] = {
|
||||
SubscriptionDuration.MONTH.value: 1,
|
||||
@@ -187,6 +193,7 @@ def format_support_subscription(
|
||||
user_id: int,
|
||||
balance: int,
|
||||
username: str | None = None,
|
||||
referal_id: int | None = None,
|
||||
) -> str:
|
||||
username = format_username(username)
|
||||
whitelist_description = "ВЫКЛ"
|
||||
@@ -204,6 +211,7 @@ def format_support_subscription(
|
||||
whitelist_description=whitelist_description,
|
||||
price=calculate_price(subscription),
|
||||
balance=balance,
|
||||
referal_id=referal_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
8
schemas/common.py
Normal file
8
schemas/common.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiogram.types import Message
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneralMessageContext:
|
||||
msg: Message
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from config import settings
|
||||
from db.models.tickets import TicketStatus
|
||||
from db.models.users import User
|
||||
from keyboards.admins import ticket_actions
|
||||
from misc import utils
|
||||
from repositories import UserRepository
|
||||
from repositories.tickets import TicketRepository
|
||||
@@ -51,9 +52,14 @@ class UserService:
|
||||
|
||||
@staticmethod
|
||||
async def format_admin_support_message(
|
||||
ticket_number: int, user: TelegramUser, rw_user: RWUserInfo | None, balance: int
|
||||
ticket_number: int,
|
||||
user: TelegramUser,
|
||||
rw_user: RWUserInfo | None,
|
||||
balance: int,
|
||||
referal_id: int | None = None,
|
||||
):
|
||||
username_display = utils.format_username(user.username)
|
||||
referal_display = utils.format_referal_id(referal_id)
|
||||
|
||||
if rw_user is None:
|
||||
# Пользователь не найден в Remnawave
|
||||
@@ -62,6 +68,8 @@ class UserService:
|
||||
full_name=user.full_name,
|
||||
username_display=username_display,
|
||||
telegram_id=user.id,
|
||||
referal_id=referal_display,
|
||||
balance=balance,
|
||||
)
|
||||
|
||||
status_icon, status_text = utils.format_status(rw_user.status)
|
||||
@@ -85,6 +93,7 @@ class UserService:
|
||||
hwid_limit=hwid_limit,
|
||||
squads=squads,
|
||||
remnawave_uuid=rw_user.uuid,
|
||||
referal_id=referal_display,
|
||||
)
|
||||
|
||||
async def add_user(
|
||||
@@ -126,7 +135,9 @@ class UserService:
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
balance=db_user.balance,
|
||||
referal_id=db_user.referal_id,
|
||||
),
|
||||
reply_markup=ticket_actions,
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
@@ -159,7 +170,9 @@ class UserService:
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
balance=db_user.balance,
|
||||
referal_id=db_user.referal_id,
|
||||
),
|
||||
reply_markup=ticket_actions,
|
||||
)
|
||||
|
||||
async def support_forward_message(self, session: AsyncSession, msg: Message, rw: RemnawaveSDK):
|
||||
@@ -205,7 +218,9 @@ class UserService:
|
||||
user=msg.from_user,
|
||||
rw_user=rw_user,
|
||||
balance=user.balance,
|
||||
referal_id=user.referal_id,
|
||||
),
|
||||
reply_markup=ticket_actions,
|
||||
)
|
||||
await msg.copy_to(settings.admin_group_id, message_thread_id=thread.message_thread_id)
|
||||
|
||||
@@ -230,6 +245,8 @@ class UserService:
|
||||
|
||||
async def close_ticket(self, session: AsyncSession, msg: Message):
|
||||
thread_id = msg.message_thread_id
|
||||
if not thread_id:
|
||||
return
|
||||
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
|
||||
|
||||
await msg.bot.edit_forum_topic(
|
||||
@@ -247,9 +264,14 @@ class UserService:
|
||||
|
||||
async def update_user_link(
|
||||
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
|
||||
) -> User | None:
|
||||
) -> User:
|
||||
rw_user = await get_user_by_telegram_id(rw_sdk, user_id)
|
||||
|
||||
user = await self.user_repository.get_user_by_id(session, user_id)
|
||||
|
||||
if not rw_user:
|
||||
return user
|
||||
|
||||
link = utils.get_subscription_link(rw_user.short_uuid)
|
||||
|
||||
user = await self.user_repository.update_user_link(session, user_id, link)
|
||||
|
||||
@@ -3,3 +3,4 @@ from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
class AdminStorage(StatesGroup):
|
||||
promotion_msg = State()
|
||||
refpay_amount = State()
|
||||
|
||||
10
texts.py
10
texts.py
@@ -163,6 +163,7 @@ _SUPPORT_USER_DESCRIPTION = (
|
||||
"👤 <b>Профиль:</b> {full_name}\n"
|
||||
"🔗 <b>Alias:</b> {username_display}\n"
|
||||
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
|
||||
"❤️ <b>Referal UID:</b> <code>{referal_id}</code>\n\n"
|
||||
"💰 <b>Баланс:</b> <code>{balance}</code>"
|
||||
)
|
||||
|
||||
@@ -202,9 +203,8 @@ ADMIN_TICKET_WITH_RW = (
|
||||
|
||||
ADMIN_TICKET_WITHOUT_RW = (
|
||||
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
||||
"👤 <b>Имя:</b> {full_name}\n"
|
||||
"🔗 <b>Alias:</b> {username_display}\n"
|
||||
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
|
||||
+ _SUPPORT_USER_DESCRIPTION
|
||||
+ "\n"
|
||||
+ _DIVIDER
|
||||
+ "⚠️ <b>Remnawave-профиль отсутствует</b>"
|
||||
)
|
||||
@@ -219,3 +219,7 @@ ADMIN_STANDARD_FALLBACK = "<b>❌ Ошибка выполнения.</b> Про
|
||||
ADMIN_PROMOTION_INIT = "👥 Введите текст рассылки"
|
||||
ADMIN_PROMOTION_CONFIRMATION = "❓ Отправить сообщение всем пользователям бота?"
|
||||
SUCCESSFULLY_SENT_PROMOTION = "Успешно разосланно {user_count} пользователям."
|
||||
SPECIFY_REFERAL_PAYMENT_AMOUNT = "Укажите количество средств для перевода рефералу пользователя."
|
||||
USER_NO_REFERAL = "❌ У пользователя нет реферала."
|
||||
INVALID_ARGUMENT_REFPAY = "❌ Укажите в аргументах количество средств, которые необходимо перевести рефералу пользователя."
|
||||
SUCCESSFUL_REFPAY = "<b>💸 Баланс реферала ({referal_id}) пополнен на {amount}₽</b>"
|
||||
|
||||
Reference in New Issue
Block a user