Merge pull request #177 from Capybara-z/patch-6
Добавление активности юзера, мелкие правки
This commit is contained in:
@@ -72,11 +72,22 @@ CREATE TABLE IF NOT EXISTS coupons
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
code TEXT UNIQUE NOT NULL,
|
code TEXT UNIQUE NOT NULL,
|
||||||
amount INTEGER NOT NULL,
|
amount INTEGER NOT NULL,
|
||||||
|
days INTEGER CHECK (days > 0 OR days IS NULL),
|
||||||
usage_limit INTEGER NOT NULL DEFAULT 1,
|
usage_limit INTEGER NOT NULL DEFAULT 1,
|
||||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||||
is_used BOOLEAN NOT NULL DEFAULT FALSE
|
is_used BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'coupons' AND column_name = 'days'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE coupons ADD COLUMN days INTEGER CHECK (days > 0 OR days IS NULL);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS coupon_usages
|
CREATE TABLE IF NOT EXISTS coupon_usages
|
||||||
(
|
(
|
||||||
coupon_id INTEGER NOT NULL REFERENCES coupons (id) ON DELETE CASCADE,
|
coupon_id INTEGER NOT NULL REFERENCES coupons (id) ON DELETE CASCADE,
|
||||||
|
|||||||
+18
-20
@@ -124,33 +124,36 @@ async def check_server_name_by_cluster(server_name: str, session: Any) -> dict |
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any):
|
async def create_coupon(coupon_code: str, amount: int, usage_limit: int, session: Any, days: int = None):
|
||||||
"""
|
"""
|
||||||
Создает новый купон в базе данных.
|
Создает новый купон в базе данных.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coupon_code (str): Уникальный код купона.
|
coupon_code (str): Уникальный код купона.
|
||||||
amount (float): Сумма, которую дает купон.
|
amount (int): Сумма, которую дает купон (0 для купонов на дни).
|
||||||
usage_limit (int): Максимальное количество использований купона.
|
usage_limit (int): Максимальное количество использований купона.
|
||||||
session (Any): Сессия базы данных для выполнения запроса.
|
session (Any): Сессия базы данных для выполнения запроса.
|
||||||
|
days (int, optional): Количество дней для продления подписки.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: В случае ошибки при создании купона.
|
Exception: В случае ошибки при создании купона.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
await create_coupon('SALE50', 50.0, 5, session)
|
await create_coupon('SALE50', 50, 5, session)
|
||||||
|
await create_coupon('DAYS10', 0, 50, session, days=10)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await session.execute(
|
await session.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used)
|
INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used, days)
|
||||||
VALUES ($1, $2, $3, 0, FALSE)
|
VALUES ($1, $2, $3, 0, FALSE, $4)
|
||||||
""",
|
""",
|
||||||
coupon_code,
|
coupon_code,
|
||||||
amount,
|
amount,
|
||||||
usage_limit,
|
usage_limit,
|
||||||
|
days,
|
||||||
)
|
)
|
||||||
logger.info(f"Успешно создан купон с кодом {coupon_code} на сумму {amount}")
|
logger.info(f"Успешно создан купон с кодом {coupon_code} на сумму {amount} или {days} дней")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании купона {coupon_code}: {e}")
|
logger.error(f"Ошибка при создании купона {coupon_code}: {e}")
|
||||||
raise
|
raise
|
||||||
@@ -170,7 +173,8 @@ async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None:
|
|||||||
- usage_limit (int): Лимит использований
|
- usage_limit (int): Лимит использований
|
||||||
- usage_count (int): Текущее количество использований
|
- usage_count (int): Текущее количество использований
|
||||||
- is_used (bool): Флаг использования
|
- is_used (bool): Флаг использования
|
||||||
- amount (float): Сумма купона
|
- amount (int): Сумма купона
|
||||||
|
- days (int): Количество дней (если есть)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: В случае ошибки при выполнении запроса
|
Exception: В случае ошибки при выполнении запроса
|
||||||
@@ -178,7 +182,7 @@ async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None:
|
|||||||
try:
|
try:
|
||||||
result = await session.fetchrow(
|
result = await session.fetchrow(
|
||||||
"""
|
"""
|
||||||
SELECT id, usage_limit, usage_count, is_used, amount
|
SELECT id, usage_limit, usage_count, is_used, amount, days
|
||||||
FROM coupons
|
FROM coupons
|
||||||
WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
|
WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
|
||||||
""",
|
""",
|
||||||
@@ -213,7 +217,7 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10):
|
|||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
coupons = await session.fetch(
|
coupons = await session.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT code, amount, usage_limit, usage_count
|
SELECT id, code, amount, usage_limit, usage_count, days, is_used -- Добавлено id
|
||||||
FROM coupons
|
FROM coupons
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
LIMIT $1 OFFSET $2
|
LIMIT $1 OFFSET $2
|
||||||
@@ -221,12 +225,9 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10):
|
|||||||
per_page,
|
per_page,
|
||||||
offset,
|
offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
total_count = await session.fetchval("SELECT COUNT(*) FROM coupons")
|
total_count = await session.fetchval("SELECT COUNT(*) FROM coupons")
|
||||||
total_pages = -(-total_count // per_page) # Округление вверх
|
total_pages = -(-total_count // per_page)
|
||||||
|
|
||||||
logger.info(f"Успешно получено {len(coupons)} купонов из базы данных (страница {page})")
|
logger.info(f"Успешно получено {len(coupons)} купонов из базы данных (страница {page})")
|
||||||
|
|
||||||
return {"coupons": coupons, "total": total_count, "pages": total_pages, "current_page": page}
|
return {"coupons": coupons, "total": total_count, "pages": total_pages, "current_page": page}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Критическая ошибка при получении списка купонов: {e}")
|
logger.error(f"Критическая ошибка при получении списка купонов: {e}")
|
||||||
@@ -1683,12 +1684,12 @@ async def get_last_payments(tg_id: int, session: Any):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def get_coupon_details(coupon_id: str, session: Any):
|
async def get_coupon_details(coupon_id: int, session: Any):
|
||||||
"""
|
"""
|
||||||
Получает детали купона по его ID.
|
Получает детали купона по его ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coupon_id (str): ID купона
|
coupon_id (int): ID купона
|
||||||
session (Any): Сессия базы данных
|
session (Any): Сессия базы данных
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -1700,20 +1701,17 @@ async def get_coupon_details(coupon_id: str, session: Any):
|
|||||||
try:
|
try:
|
||||||
record = await session.fetchrow(
|
record = await session.fetchrow(
|
||||||
"""
|
"""
|
||||||
SELECT id, code, discount, usage_count, usage_limit, is_used
|
SELECT id, code, amount, days, usage_count, usage_limit, is_used
|
||||||
FROM coupons
|
FROM coupons
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
""",
|
""",
|
||||||
coupon_id,
|
coupon_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if record:
|
if record:
|
||||||
logger.info(f"Успешно получены детали купона {coupon_id}")
|
logger.info(f"Успешно получены детали купона {coupon_id}")
|
||||||
return dict(record)
|
return dict(record)
|
||||||
|
|
||||||
logger.warning(f"Купон {coupon_id} не найден")
|
logger.warning(f"Купон {coupon_id} не найден")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при получении деталей купона {coupon_id}: {e}")
|
logger.error(f"Ошибка при получении деталей купона {coupon_id}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses
|
|||||||
)
|
)
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=f"✅ Название кластера успешно изменено с '{old_cluster_name}' на '{new_cluster_name}'!\n\n⚠️ Не забудьте сделать \"Синхронизацию\".",
|
text=f"✅ Название кластера успешно изменено с '{old_cluster_name}' на '{new_cluster_name}'!",
|
||||||
reply_markup=build_admin_back_kb("clusters"),
|
reply_markup=build_admin_back_kb("clusters"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -722,10 +722,7 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess
|
|||||||
old_server_name
|
old_server_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# Формируем текст сообщения с учетом USE_COUNTRY_SELECTION
|
final_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!"
|
||||||
base_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!"
|
|
||||||
sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"."
|
|
||||||
final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "")
|
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=final_text,
|
text=final_text,
|
||||||
|
|||||||
@@ -1,33 +1,58 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import html
|
||||||
|
import pytz
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import (
|
||||||
|
CallbackQuery,
|
||||||
|
InlineQuery,
|
||||||
|
InlineQueryResultArticle,
|
||||||
|
InputTextMessageContent,
|
||||||
|
Message,
|
||||||
|
)
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
from config import USERNAME_BOT
|
from config import INLINE_MODE, USERNAME_BOT
|
||||||
from database import create_coupon, delete_coupon, get_all_coupons
|
from database import (
|
||||||
|
add_connection,
|
||||||
|
check_connection_exists,
|
||||||
|
create_coupon,
|
||||||
|
create_coupon_usage,
|
||||||
|
delete_coupon,
|
||||||
|
get_all_coupons,
|
||||||
|
get_keys,
|
||||||
|
update_key_expiry,
|
||||||
|
)
|
||||||
from filters.admin import IsAdminFilter
|
from filters.admin import IsAdminFilter
|
||||||
|
from handlers.buttons import BACK
|
||||||
|
from handlers.keys.key_utils import renew_key_in_cluster
|
||||||
|
from handlers.profile import process_callback_view_profile
|
||||||
|
from handlers.utils import format_days
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
|
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
|
||||||
from .keyboard import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
|
from .keyboard import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb, format_coupons_list
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
class AdminCouponsState(StatesGroup):
|
class AdminCouponsState(StatesGroup):
|
||||||
waiting_for_coupon_data = State()
|
waiting_for_coupon_type = State()
|
||||||
|
waiting_for_balance_data = State()
|
||||||
|
waiting_for_days_data = State()
|
||||||
|
waiting_for_key_selection = State()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(
|
@router.callback_query(
|
||||||
AdminPanelCallback.filter(F.action == "coupons"),
|
AdminPanelCallback.filter(F.action == "coupons"),
|
||||||
IsAdminFilter(),
|
IsAdminFilter(),
|
||||||
)
|
)
|
||||||
async def handle_coupons(
|
async def handle_coupons(callback_query: CallbackQuery):
|
||||||
callback_query: CallbackQuery,
|
|
||||||
):
|
|
||||||
await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb())
|
await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb())
|
||||||
|
|
||||||
|
|
||||||
@@ -36,25 +61,52 @@ async def handle_coupons(
|
|||||||
IsAdminFilter(),
|
IsAdminFilter(),
|
||||||
)
|
)
|
||||||
async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext):
|
async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext):
|
||||||
|
text = "🎫 <b>Выберите тип купона:</b>"
|
||||||
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.button(text="💰 Баланс", callback_data="coupon_type_balance")
|
||||||
|
kb.button(text="⏳ Время", callback_data="coupon_type_days")
|
||||||
|
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
|
kb.adjust(1)
|
||||||
|
|
||||||
|
await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
|
||||||
|
await state.set_state(AdminCouponsState.waiting_for_coupon_type)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "coupon_type_balance")
|
||||||
|
async def handle_balance_coupon_selection(callback_query: CallbackQuery, state: FSMContext):
|
||||||
text = (
|
text = (
|
||||||
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
||||||
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
|
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
|
||||||
"Пример: <b>'COUPON1 50 5'</b> 👈\n\n"
|
"Пример: <b>'COUPON1 50 5'</b> 👈\n\n"
|
||||||
)
|
)
|
||||||
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
|
|
||||||
await callback_query.message.edit_text(
|
await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
|
||||||
text=text,
|
await state.set_state(AdminCouponsState.waiting_for_balance_data)
|
||||||
reply_markup=build_admin_back_kb("coupons"),
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "coupon_type_days")
|
||||||
|
async def handle_days_coupon_selection(callback_query: CallbackQuery, state: FSMContext):
|
||||||
|
text = (
|
||||||
|
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
||||||
|
"📝 <i>код</i> ⏳ <i>дни</i> 🔢 <i>лимит</i>\n\n"
|
||||||
|
"Пример: <b>'DAYS10 10 50'</b> 👈\n\n"
|
||||||
)
|
)
|
||||||
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
|
|
||||||
|
await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
|
||||||
|
await state.set_state(AdminCouponsState.waiting_for_days_data)
|
||||||
|
|
||||||
|
|
||||||
@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
|
@router.message(AdminCouponsState.waiting_for_balance_data, IsAdminFilter())
|
||||||
async def handle_coupon_data_input(message: Message, state: FSMContext, session: Any):
|
async def handle_balance_coupon_input(message: Message, state: FSMContext, session: Any):
|
||||||
text = message.text.strip()
|
text = message.text.strip()
|
||||||
parts = text.split()
|
parts = text.split()
|
||||||
|
|
||||||
kb = build_admin_back_kb("coupons")
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
|
|
||||||
if len(parts) != 3:
|
if len(parts) != 3:
|
||||||
text = (
|
text = (
|
||||||
@@ -62,41 +114,96 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session:
|
|||||||
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <b>лимит</b>\n"
|
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <b>лимит</b>\n"
|
||||||
"Пример: <b>'COUPON1 50 5'</b> 👈"
|
"Пример: <b>'COUPON1 50 5'</b> 👈"
|
||||||
)
|
)
|
||||||
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
await message.answer(
|
|
||||||
text=text,
|
|
||||||
reply_markup=kb,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
coupon_code = parts[0]
|
coupon_code = parts[0]
|
||||||
coupon_amount = float(parts[1])
|
coupon_amount = int(parts[1])
|
||||||
usage_limit = int(parts[2])
|
usage_limit = int(parts[2])
|
||||||
|
if coupon_amount <= 0:
|
||||||
|
raise ValueError("Сумма должна быть больше 0")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Сумма должна быть числом, а лимит — целым числом."
|
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Сумма должна быть числом, а лимит — целым числом."
|
||||||
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
await message.answer(
|
|
||||||
text=text,
|
|
||||||
reply_markup=kb,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await create_coupon(coupon_code, coupon_amount, usage_limit, session)
|
await create_coupon(coupon_code, coupon_amount, usage_limit, session, days=None)
|
||||||
|
|
||||||
|
coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}"
|
||||||
text = (
|
text = (
|
||||||
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
|
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
|
||||||
f"💰 Сумма: <b>{coupon_amount} рублей</b> \n"
|
f"💰 Сумма: <b>{coupon_amount} рублей</b>\n"
|
||||||
f"🔢 Лимит использования: <b>{usage_limit} раз</b>\n"
|
f"🔢 Лимит использования: <b>{usage_limit} раз</b>\n"
|
||||||
f"🔗 <b>Ссылка:</b> <code>https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}</code>\n"
|
f"🔗 <b>Ссылка:</b> <code>{coupon_link}</code>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
await message.answer(text=text, reply_markup=kb)
|
kb = InlineKeyboardBuilder()
|
||||||
|
if INLINE_MODE:
|
||||||
|
kb.button(text="📤 Поделиться", switch_inline_query=f"coupon_{coupon_code}")
|
||||||
|
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
|
kb.adjust(1)
|
||||||
|
|
||||||
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании купона: {e}")
|
logger.error(f"Ошибка при создании купона: {e}")
|
||||||
|
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(AdminCouponsState.waiting_for_days_data, IsAdminFilter())
|
||||||
|
async def handle_days_coupon_input(message: Message, state: FSMContext, session: Any):
|
||||||
|
text = message.text.strip()
|
||||||
|
parts = text.split()
|
||||||
|
|
||||||
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
|
|
||||||
|
if len(parts) != 3:
|
||||||
|
text = (
|
||||||
|
"❌ <b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
|
||||||
|
"🏷️ <b>код</b> ⏳ <i>дни</i> 🔢 <b>лимит</b>\n"
|
||||||
|
"Пример: <b>'DAYS10 10 50'</b> 👈"
|
||||||
|
)
|
||||||
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
coupon_code = parts[0]
|
||||||
|
days = int(parts[1])
|
||||||
|
usage_limit = int(parts[2])
|
||||||
|
if days <= 0:
|
||||||
|
raise ValueError("Количество дней должно быть больше 0")
|
||||||
|
except ValueError:
|
||||||
|
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Дни должны быть числом, а лимит — целым числом."
|
||||||
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await create_coupon(coupon_code, 0, usage_limit, session, days=days)
|
||||||
|
|
||||||
|
coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}"
|
||||||
|
text = (
|
||||||
|
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
|
||||||
|
f"⏳ <b>{format_days(days)}</b>\n"
|
||||||
|
f"🔢 Лимит использования: <b>{usage_limit} раз</b>\n"
|
||||||
|
f"🔗 <b>Ссылка:</b> <code>{coupon_link}</code>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
kb = InlineKeyboardBuilder()
|
||||||
|
if INLINE_MODE:
|
||||||
|
kb.button(text="📤 Поделиться", switch_inline_query=f"coupon_{coupon_code}")
|
||||||
|
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
|
kb.adjust(1)
|
||||||
|
|
||||||
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при создании купона: {e}")
|
||||||
|
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(
|
@router.callback_query(
|
||||||
@@ -107,46 +214,55 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any):
|
|||||||
try:
|
try:
|
||||||
data = AdminPanelCallback.unpack(callback_query.data)
|
data = AdminPanelCallback.unpack(callback_query.data)
|
||||||
page = data.page if data.page is not None else 1
|
page = data.page if data.page is not None else 1
|
||||||
per_page = 10
|
await update_coupons_list(callback_query.message, session, page)
|
||||||
result = await get_all_coupons(session, page, per_page)
|
except Exception as e:
|
||||||
coupons = result["coupons"]
|
logger.error(f"Ошибка при получении списка купонов: {e}")
|
||||||
|
await callback_query.message.edit_text("Произошла ошибка при получении списка купонов.")
|
||||||
|
|
||||||
if not coupons:
|
|
||||||
|
@router.callback_query(AdminCouponDeleteCallback.filter(F.confirm.is_(None)), IsAdminFilter())
|
||||||
|
async def handle_coupon_delete(callback_query: CallbackQuery, callback_data: AdminCouponDeleteCallback, session: Any):
|
||||||
|
coupon_code = callback_data.coupon_code
|
||||||
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.button(
|
||||||
|
text="✅ Да, удалить",
|
||||||
|
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack()
|
||||||
|
)
|
||||||
|
kb.button(
|
||||||
|
text="❌ Нет, отменить",
|
||||||
|
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack()
|
||||||
|
)
|
||||||
|
kb.adjust(1)
|
||||||
|
|
||||||
|
await callback_query.message.edit_text(
|
||||||
|
f"Вы уверены, что хотите удалить купон <b>{coupon_code}</b>?",
|
||||||
|
reply_markup=kb.as_markup()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(AdminCouponDeleteCallback.filter(F.confirm.is_not(None)), IsAdminFilter())
|
||||||
|
async def confirm_coupon_delete(callback_query: CallbackQuery, callback_data: AdminCouponDeleteCallback, session: Any):
|
||||||
|
coupon_code = callback_data.coupon_code
|
||||||
|
confirm = callback_data.confirm
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
try:
|
||||||
|
result = await delete_coupon(coupon_code, session)
|
||||||
|
if not result:
|
||||||
|
await callback_query.message.edit_text(
|
||||||
|
f"❌ Купон с кодом {coupon_code} не найден.",
|
||||||
|
reply_markup=build_admin_back_kb("coupons")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при удалении купона: {e}")
|
||||||
await callback_query.message.edit_text(
|
await callback_query.message.edit_text(
|
||||||
text="❌ На данный момент нет доступных купонов!",
|
"Произошла ошибка при удалении купона.",
|
||||||
reply_markup=build_admin_back_kb("coupons"),
|
reply_markup=build_admin_back_kb("coupons")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"])
|
await update_coupons_list(callback_query.message, session)
|
||||||
coupon_list = "📜 Список всех купонов:\n\n"
|
|
||||||
for coupon in coupons:
|
|
||||||
coupon_list += (
|
|
||||||
f"🏷️ <b>Код:</b> {coupon['code']}\n"
|
|
||||||
f"💰 <b>Сумма:</b> {coupon['amount']} рублей\n"
|
|
||||||
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
|
|
||||||
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n"
|
|
||||||
f"🔗 <b>Ссылка:</b> <code>https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}</code>\n"
|
|
||||||
)
|
|
||||||
await callback_query.message.edit_text(text=coupon_list, reply_markup=kb)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении списка купонов: {e}")
|
|
||||||
await callback_query.message.answer("Произошла ошибка при получении списка купонов.")
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(AdminCouponDeleteCallback.filter(), IsAdminFilter())
|
|
||||||
async def handle_coupon_delete(callback_query: CallbackQuery, callback_data: AdminCouponDeleteCallback, session: Any):
|
|
||||||
coupon_code = callback_data.coupon_code
|
|
||||||
try:
|
|
||||||
result = await delete_coupon(coupon_code, session)
|
|
||||||
if result:
|
|
||||||
await callback_query.message.edit_text(f"Купон {coupon_code} удалён!")
|
|
||||||
else:
|
|
||||||
await callback_query.message.edit_text(f"❌ Купон с кодом {coupon_code} не найден.", show_alert=True)
|
|
||||||
await update_coupons_list(callback_query.message, session)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при удалении купона: {e}")
|
|
||||||
await callback_query.message.edit_text("Произошла ошибка при удалении купона.", show_alert=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_coupons_list(message, session: Any, page: int = 1):
|
async def update_coupons_list(message, session: Any, page: int = 1):
|
||||||
@@ -162,13 +278,206 @@ async def update_coupons_list(message, session: Any, page: int = 1):
|
|||||||
return
|
return
|
||||||
|
|
||||||
kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"])
|
kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"])
|
||||||
coupon_list = "📜 Список всех купонов:\n\n"
|
text = format_coupons_list(coupons, USERNAME_BOT)
|
||||||
for coupon in coupons:
|
await message.edit_text(text=text, reply_markup=kb)
|
||||||
coupon_list += (
|
|
||||||
f"🏷️ <b>Код:</b> {coupon['code']}\n"
|
|
||||||
f"💰 <b>Сумма:</b> {coupon['amount']} рублей\n"
|
@router.inline_query(F.query.startswith("coupon_"))
|
||||||
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
|
async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
|
||||||
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n"
|
if not INLINE_MODE:
|
||||||
f"🔗 <b>Ссылка:</b> <code>https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}</code>\n"
|
return
|
||||||
|
|
||||||
|
coupon_code = inline_query.query.split("coupon_")[1]
|
||||||
|
coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}"
|
||||||
|
|
||||||
|
coupons = await get_all_coupons(session, page=1, per_page=10)
|
||||||
|
coupon = next((c for c in coupons["coupons"] if c["code"] == coupon_code), None)
|
||||||
|
|
||||||
|
if not coupon:
|
||||||
|
await inline_query.answer(
|
||||||
|
results=[],
|
||||||
|
switch_pm_text="Купон не найден",
|
||||||
|
switch_pm_parameter="coupons",
|
||||||
|
cache_time=1,
|
||||||
)
|
)
|
||||||
await message.edit_text(text=coupon_list, reply_markup=kb)
|
return
|
||||||
|
|
||||||
|
title = f"Купон {coupon['code']}"
|
||||||
|
description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {format_days(coupon['days'])}!"
|
||||||
|
message_text = (
|
||||||
|
f"🎫 <b>Купон:</b> {coupon['code']}\n"
|
||||||
|
f"{'💰 <b>Бонус:</b> ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ <b>Продление:</b> ' + format_days(coupon['days'])}\n"
|
||||||
|
f"👇 Нажми, чтобы активировать!"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.button(text="Активировать купон", url=coupon_link)
|
||||||
|
|
||||||
|
result = InlineQueryResultArticle(
|
||||||
|
id=coupon_code,
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
input_message_content=InputTextMessageContent(
|
||||||
|
message_text=message_text,
|
||||||
|
parse_mode=ParseMode.HTML
|
||||||
|
),
|
||||||
|
reply_markup=builder.as_markup(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await inline_query.answer(
|
||||||
|
results=[result],
|
||||||
|
cache_time=86400,
|
||||||
|
is_personal=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.message(F.text.regexp(r"^/start coupons_(.+)$"))
|
||||||
|
async def handle_coupon_activation(message: Message, state: FSMContext, session: Any, admin: bool = False):
|
||||||
|
coupon_code = message.text.split("coupons_")[1]
|
||||||
|
|
||||||
|
coupons = await get_all_coupons(session, page=1, per_page=10)
|
||||||
|
coupon = next((c for c in coupons["coupons"] if c["code"] == coupon_code), None)
|
||||||
|
|
||||||
|
if not coupon:
|
||||||
|
await message.answer("❌ Купон не найден.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if coupon["usage_count"] >= coupon["usage_limit"] or coupon["is_used"]:
|
||||||
|
await message.answer("❌ Лимит активаций купона исчерпан.")
|
||||||
|
return
|
||||||
|
|
||||||
|
usage = await session.fetchrow(
|
||||||
|
"SELECT * FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2",
|
||||||
|
coupon["id"],
|
||||||
|
message.from_user.id
|
||||||
|
)
|
||||||
|
if usage:
|
||||||
|
await message.answer("❌ Вы уже активировали этот купон.")
|
||||||
|
return
|
||||||
|
|
||||||
|
connection_exists = await check_connection_exists(message.from_user.id)
|
||||||
|
if not connection_exists:
|
||||||
|
await add_connection(tg_id=message.from_user.id, session=session)
|
||||||
|
|
||||||
|
if coupon["amount"] > 0:
|
||||||
|
await session.execute(
|
||||||
|
"UPDATE connections SET balance = balance + $1 WHERE tg_id = $2",
|
||||||
|
coupon["amount"],
|
||||||
|
message.from_user.id
|
||||||
|
)
|
||||||
|
await session.execute(
|
||||||
|
"UPDATE coupons SET usage_count = usage_count + 1, is_used = $1 WHERE id = $2",
|
||||||
|
coupon["usage_count"] + 1 >= coupon["usage_limit"],
|
||||||
|
coupon["id"]
|
||||||
|
)
|
||||||
|
await create_coupon_usage(coupon["id"], message.from_user.id, session)
|
||||||
|
await message.answer(f"✅ Купон активирован, на баланс начислено {coupon['amount']} рублей.")
|
||||||
|
await process_callback_view_profile(message, state, admin)
|
||||||
|
return
|
||||||
|
|
||||||
|
if coupon["days"] is not None and coupon["days"] > 0:
|
||||||
|
keys = await get_keys(message.from_user.id, session)
|
||||||
|
active_keys = [k for k in keys if not k["is_frozen"]]
|
||||||
|
|
||||||
|
if not active_keys:
|
||||||
|
await message.answer("❌ У вас нет активных подписок для продления.")
|
||||||
|
return
|
||||||
|
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
moscow_tz = pytz.timezone("Europe/Moscow")
|
||||||
|
response_message = "<b>🔑 Выберите подписку для продления:</b>\n\n<blockquote>"
|
||||||
|
|
||||||
|
for key in active_keys:
|
||||||
|
alias = key.get("alias")
|
||||||
|
email = key["email"]
|
||||||
|
client_id = key["client_id"]
|
||||||
|
expiry_time = key.get("expiry_time")
|
||||||
|
|
||||||
|
key_display = html.escape(alias.strip() if alias else email)
|
||||||
|
expiry_date = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz).strftime("до %d.%m.%y, %H:%M")
|
||||||
|
response_message += f"• <b>{key_display}</b> ({expiry_date})\n"
|
||||||
|
builder.button(text=key_display, callback_data=f"extend_key|{client_id}|{coupon['id']}")
|
||||||
|
|
||||||
|
response_message += "</blockquote>"
|
||||||
|
builder.button(text="Отмена", callback_data="cancel_coupon_activation")
|
||||||
|
builder.adjust(1)
|
||||||
|
|
||||||
|
await message.answer(response_message, reply_markup=builder.as_markup())
|
||||||
|
await state.set_state(AdminCouponsState.waiting_for_key_selection)
|
||||||
|
await state.update_data(coupon_id=coupon["id"])
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer("❌ Купон недействителен (нет суммы или дней).")
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("extend_key|"))
|
||||||
|
async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool = False):
|
||||||
|
parts = callback_query.data.split("|")
|
||||||
|
client_id = parts[1]
|
||||||
|
coupon_id = int(parts[2])
|
||||||
|
|
||||||
|
coupon = await session.fetchrow("SELECT * FROM coupons WHERE id = $1", coupon_id)
|
||||||
|
if not coupon or coupon["usage_count"] >= coupon["usage_limit"]:
|
||||||
|
await callback_query.message.edit_text("❌ Купон недействителен или лимит исчерпан.")
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
usage = await session.fetchrow(
|
||||||
|
"SELECT * FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2",
|
||||||
|
coupon_id,
|
||||||
|
callback_query.from_user.id
|
||||||
|
)
|
||||||
|
if usage:
|
||||||
|
await callback_query.message.edit_text("❌ Вы уже активировали этот купон.")
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
key = await session.fetchrow(
|
||||||
|
"SELECT * FROM keys WHERE tg_id = $1 AND client_id = $2",
|
||||||
|
callback_query.from_user.id,
|
||||||
|
client_id
|
||||||
|
)
|
||||||
|
if not key or key["is_frozen"]:
|
||||||
|
await callback_query.message.edit_text("❌ Выбранная подписка не найдена или заморожена.")
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
now_ms = int(datetime.now().timestamp() * 1000)
|
||||||
|
current_expiry = key["expiry_time"]
|
||||||
|
new_expiry = max(now_ms, current_expiry) + (coupon["days"] * 86400 * 1000)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await renew_key_in_cluster(
|
||||||
|
cluster_id=key["server_id"],
|
||||||
|
email=key["email"],
|
||||||
|
client_id=client_id,
|
||||||
|
new_expiry_time=new_expiry,
|
||||||
|
total_gb=0
|
||||||
|
)
|
||||||
|
await update_key_expiry(client_id, new_expiry, session)
|
||||||
|
|
||||||
|
await session.execute(
|
||||||
|
"UPDATE coupons SET usage_count = usage_count + 1, is_used = $1 WHERE id = $2",
|
||||||
|
coupon["usage_count"] + 1 >= coupon["usage_limit"],
|
||||||
|
coupon["id"]
|
||||||
|
)
|
||||||
|
await create_coupon_usage(coupon["id"], callback_query.from_user.id, session)
|
||||||
|
|
||||||
|
alias = key.get("alias") or key["email"]
|
||||||
|
expiry_date = datetime.fromtimestamp(new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")).strftime("%d.%m.%y, %H:%M")
|
||||||
|
text = f"✅ Купон активирован, подписка <b>{alias}</b> продлена на {format_days(coupon['days'])}⏳ до {expiry_date}📆."
|
||||||
|
|
||||||
|
await callback_query.message.answer(text)
|
||||||
|
await process_callback_view_profile(callback_query.message, state, admin)
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при продлении ключа: {e}")
|
||||||
|
await callback_query.message.edit_text("❌ Произошла ошибка при продлении подписки.")
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "cancel_coupon_activation")
|
||||||
|
async def cancel_coupon_activation(callback_query: CallbackQuery, state: FSMContext, admin: bool = False):
|
||||||
|
await callback_query.message.answer("⚠️ Активация купона отменена.")
|
||||||
|
await process_callback_view_profile(callback_query.message, state, admin)
|
||||||
|
await state.clear()
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from aiogram.filters.callback_data import CallbackData
|
from aiogram.filters.callback_data import CallbackData
|
||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
from handlers.buttons import BACK
|
from handlers.buttons import BACK
|
||||||
|
from handlers.utils import format_days
|
||||||
|
|
||||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
|
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
|
||||||
|
|
||||||
|
|
||||||
class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"):
|
class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"):
|
||||||
coupon_code: str
|
coupon_code: str
|
||||||
|
confirm: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
def build_coupons_kb() -> InlineKeyboardMarkup:
|
def build_coupons_kb() -> InlineKeyboardMarkup:
|
||||||
@@ -50,3 +54,17 @@ def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) ->
|
|||||||
builder.row(build_admin_back_btn("coupons"))
|
builder.row(build_admin_back_btn("coupons"))
|
||||||
builder.adjust(2)
|
builder.adjust(2)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def format_coupons_list(coupons: list, username_bot: str) -> str:
|
||||||
|
coupon_list = "📜 Список всех купонов:\n\n"
|
||||||
|
for coupon in coupons:
|
||||||
|
value_text = f"💰 <b>Сумма:</b> {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ <b>{format_days(coupon['days'])}</b>"
|
||||||
|
coupon_list += (
|
||||||
|
f"🏷️ <b>Код:</b> {coupon['code']}\n"
|
||||||
|
f"{value_text}\n"
|
||||||
|
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
|
||||||
|
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n"
|
||||||
|
f"🔗 <b>Ссылка:</b> <code>https://t.me/{username_bot}?start=coupons_{coupon['code']}</code>\n\n"
|
||||||
|
)
|
||||||
|
return coupon_list
|
||||||
|
|||||||
@@ -612,14 +612,19 @@ async def process_user_search(
|
|||||||
|
|
||||||
balance = int(balance)
|
balance = int(balance)
|
||||||
|
|
||||||
|
user_data = await session.fetchrow("SELECT username, created_at, updated_at FROM users WHERE tg_id = $1", tg_id)
|
||||||
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
|
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
|
||||||
key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id)
|
key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id)
|
||||||
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
|
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
|
||||||
|
created_at = user_data["created_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
|
||||||
|
updated_at = user_data["updated_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
f"<b>📊 Информация о пользователе</b>"
|
f"<b>📊 Информация о пользователе</b>"
|
||||||
f"\n\n🆔 ID: <b>{tg_id}</b>"
|
f"\n\n🆔 ID: <b>{tg_id}</b>"
|
||||||
f"\n📄 Логин: <b>@{username}</b>"
|
f"\n📄 Логин: <b>@{username}</b>"
|
||||||
|
f"\n📅 Дата регистрации: <b>{created_at}</b>"
|
||||||
|
f"\n🏃 Дата активности: <b>{updated_at}</b>"
|
||||||
f"\n💰 Баланс: <b>{balance}</b>"
|
f"\n💰 Баланс: <b>{balance}</b>"
|
||||||
f"\n👥 Количество рефералов: <b>{referral_count}</b>"
|
f"\n👥 Количество рефералов: <b>{referral_count}</b>"
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-2
@@ -113,9 +113,12 @@ async def process_callback_view_profile(
|
|||||||
|
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
if trial_status == 0 or key_count == 0:
|
if trial_status == 0 or key_count == 0:
|
||||||
builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key"))
|
if key_count > 0:
|
||||||
else:
|
|
||||||
builder.row(InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys"))
|
builder.row(InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys"))
|
||||||
|
elif trial_status == 0:
|
||||||
|
builder.row(InlineKeyboardButton(text="🎁 Пробная подписка", callback_data="create_key"))
|
||||||
|
else:
|
||||||
|
builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key"))
|
||||||
builder.row(InlineKeyboardButton(text=BALANCE, callback_data="balance"))
|
builder.row(InlineKeyboardButton(text=BALANCE, callback_data="balance"))
|
||||||
|
|
||||||
row_buttons = []
|
row_buttons = []
|
||||||
|
|||||||
+3
-41
@@ -31,6 +31,7 @@ from database import (
|
|||||||
get_trial,
|
get_trial,
|
||||||
update_balance,
|
update_balance,
|
||||||
)
|
)
|
||||||
|
from handlers.admin.coupons.coupons_handler import handle_coupon_activation
|
||||||
from handlers.buttons import ABOUT_VPN, BACK, CHANNEL, MAIN_MENU, SUPPORT
|
from handlers.buttons import ABOUT_VPN, BACK, CHANNEL, MAIN_MENU, SUPPORT
|
||||||
from handlers.captcha import generate_captcha
|
from handlers.captcha import generate_captcha
|
||||||
from handlers.keys.key_management import create_key
|
from handlers.keys.key_management import create_key
|
||||||
@@ -119,47 +120,8 @@ async def process_start_logic(
|
|||||||
try:
|
try:
|
||||||
if "coupons_" in text:
|
if "coupons_" in text:
|
||||||
logger.info(f"Обнаружена ссылка на купон: {text}")
|
logger.info(f"Обнаружена ссылка на купон: {text}")
|
||||||
coupon_code = text.split("coupons_")[1].strip()
|
await handle_coupon_activation(message, state, session)
|
||||||
|
return
|
||||||
coupon = await session.fetchrow(
|
|
||||||
"SELECT id, code, amount, usage_limit, usage_count, is_used FROM coupons WHERE code = $1",
|
|
||||||
coupon_code,
|
|
||||||
)
|
|
||||||
if not coupon:
|
|
||||||
await message.answer("❌ Купон не найден!")
|
|
||||||
return await process_callback_view_profile(message, state, admin)
|
|
||||||
|
|
||||||
usage_exists = await session.fetchval(
|
|
||||||
"SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2",
|
|
||||||
coupon["id"],
|
|
||||||
message.chat.id,
|
|
||||||
)
|
|
||||||
if usage_exists:
|
|
||||||
await message.answer("❌ Вы уже использовали этот купон!")
|
|
||||||
return await process_callback_view_profile(message, state, admin)
|
|
||||||
|
|
||||||
if coupon["is_used"] or coupon["usage_count"] >= coupon["usage_limit"]:
|
|
||||||
await message.answer("❌ Этот купон уже использован!")
|
|
||||||
return await process_callback_view_profile(message, state, admin)
|
|
||||||
|
|
||||||
connection_exists = await check_connection_exists(message.chat.id)
|
|
||||||
if not connection_exists:
|
|
||||||
await add_connection(tg_id=message.chat.id, session=session)
|
|
||||||
|
|
||||||
await update_balance(message.chat.id, coupon["amount"])
|
|
||||||
await session.execute(
|
|
||||||
"UPDATE coupons SET usage_count = $1, is_used = $2 WHERE code = $3",
|
|
||||||
coupon["usage_count"] + 1,
|
|
||||||
coupon["usage_count"] + 1 >= coupon["usage_limit"],
|
|
||||||
coupon_code,
|
|
||||||
)
|
|
||||||
await session.execute(
|
|
||||||
"INSERT INTO coupon_usages (coupon_id, user_id, used_at) VALUES ($1, $2, NOW())",
|
|
||||||
coupon["id"],
|
|
||||||
message.chat.id,
|
|
||||||
)
|
|
||||||
await message.answer(COUPON_SUCCESS_MSG.format(amount=coupon["amount"]))
|
|
||||||
return await process_callback_view_profile(message, state, admin)
|
|
||||||
|
|
||||||
if "gift_" in text:
|
if "gift_" in text:
|
||||||
parts = text.split("gift_")[1].split("_")
|
parts = text.split("gift_")[1].split("_")
|
||||||
|
|||||||
@@ -152,6 +152,27 @@ def format_time_until_deletion(seconds: int) -> str:
|
|||||||
return " и ".join(parts) if parts else "менее минуты"
|
return " и ".join(parts) if parts else "менее минуты"
|
||||||
|
|
||||||
|
|
||||||
|
def get_plural_form(num: int, form1: str, form2: str, form3: str) -> str:
|
||||||
|
n = abs(num) % 100
|
||||||
|
if 10 < n < 20:
|
||||||
|
return form3
|
||||||
|
return {1: form1, 2: form2, 3: form2, 4: form2}.get(n % 10, form3)
|
||||||
|
|
||||||
|
def format_days(days: int) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует количество дней с правильным склонением.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
days (int): Количество дней.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Строка с числом и склонённым словом "день/дня/дней".
|
||||||
|
"""
|
||||||
|
if days <= 0:
|
||||||
|
return "0 дней"
|
||||||
|
return f"{days} {get_plural_form(days, 'день', 'дня', 'дней')}"
|
||||||
|
|
||||||
|
|
||||||
async def edit_or_send_message(
|
async def edit_or_send_message(
|
||||||
target_message: Message,
|
target_message: Message,
|
||||||
text: str,
|
text: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user