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 application source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN sed -i 's/\r$//' ./entrypoints/startup.sh && \
|
||||||
|
chmod +x ./entrypoints/startup.sh
|
||||||
|
|
||||||
# Ensure the startup script is executable
|
# 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
|
# 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",
|
"CMD-SHELL",
|
||||||
"pg_isready -U ${POSTGRES_USER:-malenia} -d ${POSTGRES_DB:-malenia_db}",
|
"pg_isready -U ${POSTGRES_USER:-malenia} -d ${POSTGRES_DB:-malenia_db}",
|
||||||
]
|
]
|
||||||
interval: 5s
|
interval: 10s
|
||||||
timeout: 10s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
bot:
|
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
|
#!/bin/bash
|
||||||
set -e
|
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 "[+] Running migrations..."
|
||||||
echo
|
echo
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
from keyboards.admins import promotion_confirmation
|
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.di import DependenciesDTO
|
||||||
from schemas.promotions import NewPromotionContext
|
from schemas.promotions import NewPromotionContext
|
||||||
from states.admins import AdminStorage
|
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)
|
ctx = NewPromotionContext(creator_id=msg.from_user.id)
|
||||||
await msg.delete()
|
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_state(AdminStorage.promotion_msg)
|
||||||
await state.set_data({"ctx": ctx})
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import settings
|
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 misc.utils import format_share_deep_link
|
||||||
|
from schemas.common import GeneralMessageContext
|
||||||
from schemas.di import DependenciesDTO
|
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()
|
router = Router()
|
||||||
|
|
||||||
@@ -45,17 +54,70 @@ async def refpay(
|
|||||||
user = await deps.user_service.fetch_user_by_thread(session, msg.message_thread_id)
|
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)
|
referal = await deps.user_repository.get_user_by_id(session, user.referal_id)
|
||||||
if not referal:
|
if not referal:
|
||||||
await status_msg.edit_text("❌ У пользователя нет реферала.")
|
await status_msg.edit_text(USER_NO_REFERAL)
|
||||||
return
|
return
|
||||||
|
|
||||||
amount = command.args
|
amount = command.args
|
||||||
|
|
||||||
if not amount.isdigit():
|
if not amount.isdigit():
|
||||||
await status_msg.edit_text(
|
await status_msg.edit_text(INVALID_ARGUMENT_REFPAY)
|
||||||
"❌ Укажите в аргументах количество средств, которые необходимо перевести рефералу пользователя."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
amount = int(amount)
|
amount = int(amount)
|
||||||
await deps.user_repository.increase_user_balance(session, referal.id, 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 state.clear()
|
||||||
|
|
||||||
await deps.user_service.close_ticket(session, msg)
|
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")],
|
[InlineKeyboardButton(text="❌", callback_data="decline")],
|
||||||
]
|
]
|
||||||
).as_markup()
|
).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()
|
).as_markup()
|
||||||
|
|
||||||
|
close_kb: InlineKeyboardMarkup = InlineKeyboardBuilder([[InlineKeyboardButton(text="❌", callback_data="delete")]]).as_markup()
|
||||||
|
|
||||||
def _return_btn(data: str = "menu:main", text: str = "⬅️ Назад") -> InlineKeyboardButton:
|
def _return_btn(data: str = "menu:main", text: str = "⬅️ Назад") -> InlineKeyboardButton:
|
||||||
return InlineKeyboardButton(text=text, callback_data=data)
|
return InlineKeyboardButton(text=text, callback_data=data)
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ def format_username(username: str | None) -> str:
|
|||||||
return texts.NO_USERNAME
|
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:
|
def convert_duration_to_int(duration: SubscriptionDuration) -> int:
|
||||||
mapping: dict[str, int] = {
|
mapping: dict[str, int] = {
|
||||||
SubscriptionDuration.MONTH.value: 1,
|
SubscriptionDuration.MONTH.value: 1,
|
||||||
@@ -187,6 +193,7 @@ def format_support_subscription(
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
balance: int,
|
balance: int,
|
||||||
username: str | None = None,
|
username: str | None = None,
|
||||||
|
referal_id: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
username = format_username(username)
|
username = format_username(username)
|
||||||
whitelist_description = "ВЫКЛ"
|
whitelist_description = "ВЫКЛ"
|
||||||
@@ -204,6 +211,7 @@ def format_support_subscription(
|
|||||||
whitelist_description=whitelist_description,
|
whitelist_description=whitelist_description,
|
||||||
price=calculate_price(subscription),
|
price=calculate_price(subscription),
|
||||||
balance=balance,
|
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 config import settings
|
||||||
from db.models.tickets import TicketStatus
|
from db.models.tickets import TicketStatus
|
||||||
from db.models.users import User
|
from db.models.users import User
|
||||||
|
from keyboards.admins import ticket_actions
|
||||||
from misc import utils
|
from misc import utils
|
||||||
from repositories import UserRepository
|
from repositories import UserRepository
|
||||||
from repositories.tickets import TicketRepository
|
from repositories.tickets import TicketRepository
|
||||||
@@ -51,9 +52,14 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def format_admin_support_message(
|
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)
|
username_display = utils.format_username(user.username)
|
||||||
|
referal_display = utils.format_referal_id(referal_id)
|
||||||
|
|
||||||
if rw_user is None:
|
if rw_user is None:
|
||||||
# Пользователь не найден в Remnawave
|
# Пользователь не найден в Remnawave
|
||||||
@@ -62,6 +68,8 @@ 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,
|
||||||
|
referal_id=referal_display,
|
||||||
|
balance=balance,
|
||||||
)
|
)
|
||||||
|
|
||||||
status_icon, status_text = utils.format_status(rw_user.status)
|
status_icon, status_text = utils.format_status(rw_user.status)
|
||||||
@@ -85,6 +93,7 @@ class UserService:
|
|||||||
hwid_limit=hwid_limit,
|
hwid_limit=hwid_limit,
|
||||||
squads=squads,
|
squads=squads,
|
||||||
remnawave_uuid=rw_user.uuid,
|
remnawave_uuid=rw_user.uuid,
|
||||||
|
referal_id=referal_display,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_user(
|
async def add_user(
|
||||||
@@ -126,7 +135,9 @@ class UserService:
|
|||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
balance=db_user.balance,
|
balance=db_user.balance,
|
||||||
|
referal_id=db_user.referal_id,
|
||||||
),
|
),
|
||||||
|
reply_markup=ticket_actions,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -159,7 +170,9 @@ class UserService:
|
|||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
balance=db_user.balance,
|
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):
|
async def support_forward_message(self, session: AsyncSession, msg: Message, rw: RemnawaveSDK):
|
||||||
@@ -205,7 +218,9 @@ class UserService:
|
|||||||
user=msg.from_user,
|
user=msg.from_user,
|
||||||
rw_user=rw_user,
|
rw_user=rw_user,
|
||||||
balance=user.balance,
|
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)
|
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):
|
async def close_ticket(self, session: AsyncSession, msg: Message):
|
||||||
thread_id = msg.message_thread_id
|
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)
|
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
|
||||||
|
|
||||||
await msg.bot.edit_forum_topic(
|
await msg.bot.edit_forum_topic(
|
||||||
@@ -247,9 +264,14 @@ class UserService:
|
|||||||
|
|
||||||
async def update_user_link(
|
async def update_user_link(
|
||||||
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
|
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
|
||||||
) -> User | None:
|
) -> 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)
|
||||||
|
|
||||||
|
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)
|
link = utils.get_subscription_link(rw_user.short_uuid)
|
||||||
|
|
||||||
user = await self.user_repository.update_user_link(session, user_id, link)
|
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):
|
class AdminStorage(StatesGroup):
|
||||||
promotion_msg = State()
|
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>Профиль:</b> {full_name}\n"
|
||||||
"🔗 <b>Alias:</b> {username_display}\n"
|
"🔗 <b>Alias:</b> {username_display}\n"
|
||||||
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\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>"
|
"💰 <b>Баланс:</b> <code>{balance}</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -202,9 +203,8 @@ ADMIN_TICKET_WITH_RW = (
|
|||||||
|
|
||||||
ADMIN_TICKET_WITHOUT_RW = (
|
ADMIN_TICKET_WITHOUT_RW = (
|
||||||
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
"🎫 <b>ТИКЕТ T-{ticket_number:04d}</b>\n\n"
|
||||||
"👤 <b>Имя:</b> {full_name}\n"
|
+ _SUPPORT_USER_DESCRIPTION
|
||||||
"🔗 <b>Alias:</b> {username_display}\n"
|
+ "\n"
|
||||||
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
|
|
||||||
+ _DIVIDER
|
+ _DIVIDER
|
||||||
+ "⚠️ <b>Remnawave-профиль отсутствует</b>"
|
+ "⚠️ <b>Remnawave-профиль отсутствует</b>"
|
||||||
)
|
)
|
||||||
@@ -219,3 +219,7 @@ ADMIN_STANDARD_FALLBACK = "<b>❌ Ошибка выполнения.</b> Про
|
|||||||
ADMIN_PROMOTION_INIT = "👥 Введите текст рассылки"
|
ADMIN_PROMOTION_INIT = "👥 Введите текст рассылки"
|
||||||
ADMIN_PROMOTION_CONFIRMATION = "❓ Отправить сообщение всем пользователям бота?"
|
ADMIN_PROMOTION_CONFIRMATION = "❓ Отправить сообщение всем пользователям бота?"
|
||||||
SUCCESSFULLY_SENT_PROMOTION = "Успешно разосланно {user_count} пользователям."
|
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