Merge pull request #177 from Capybara-z/patch-6

Добавление активности юзера, мелкие правки
This commit is contained in:
Vladislav Lisitsyn
2025-04-03 22:31:54 +03:00
committed by GitHub
9 changed files with 466 additions and 142 deletions
+11
View File
@@ -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
View File
@@ -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
+2 -5
View File
@@ -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,
+383 -74
View File
@@ -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()
+18
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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("_")
+21
View File
@@ -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,