diff --git a/database/gifts.py b/database/gifts.py
index 68083ce1..530381c2 100644
--- a/database/gifts.py
+++ b/database/gifts.py
@@ -16,6 +16,8 @@ async def store_gift_link(
expiry_time: datetime,
gift_link: str,
tariff_id: int | None = None,
+ is_unlimited: bool = False,
+ max_usages: int | None = None,
):
try:
stmt = insert(Gift).values(
@@ -28,10 +30,12 @@ async def store_gift_link(
created_at=datetime.utcnow(),
is_used=False,
tariff_id=tariff_id,
+ is_unlimited=is_unlimited,
+ max_usages=max_usages,
)
await session.execute(stmt)
await session.commit()
- logger.info(f"🎁 Подарок {gift_id} сохранён (tariff_id={tariff_id})")
+ logger.info(f"🎁 Подарок {gift_id} сохранён (tariff_id={tariff_id}, max_usages={max_usages})")
return True
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при сохранении подарка {gift_id}: {e}")
diff --git a/database/models.py b/database/models.py
index f587fb7d..3c6f48df 100644
--- a/database/models.py
+++ b/database/models.py
@@ -152,9 +152,19 @@ class Gift(DictLikeMixin, Base):
gift_link = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
is_used = Column(Boolean, default=False)
+ is_unlimited = Column(Boolean, default=False)
+ max_usages = Column(Integer, nullable=True)
tariff_id: Mapped[int | None] = mapped_column(ForeignKey("tariffs.id"))
+class GiftUsage(DictLikeMixin, Base):
+ __tablename__ = "gift_usages"
+
+ gift_id = Column(String, ForeignKey("gifts.gift_id"), primary_key=True)
+ tg_id = Column(BigInteger, primary_key=True)
+ used_at = Column(DateTime, default=datetime.utcnow)
+
+
class ManualBan(DictLikeMixin, Base):
__tablename__ = "manual_bans"
diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py
index 3a45164f..9428cd31 100644
--- a/handlers/admin/__init__.py
+++ b/handlers/admin/__init__.py
@@ -15,6 +15,7 @@ from .servers import router as servers_router
from .stats import router as stats_router
from .tariffs import router as tariffs_router
from .users import router as users_router
+from .gifts import router as gifts_router
router = Router(name="admins_main_router")
@@ -32,4 +33,5 @@ router.include_routers(
bans_router,
ads_router,
tariffs_router,
+ gifts_router
)
diff --git a/handlers/admin/gifts/__init__.py b/handlers/admin/gifts/__init__.py
new file mode 100644
index 00000000..8de4fd7f
--- /dev/null
+++ b/handlers/admin/gifts/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ("router",)
+
+from .gifts_handler import router
\ No newline at end of file
diff --git a/handlers/admin/gifts/gifts_handler.py b/handlers/admin/gifts/gifts_handler.py
new file mode 100644
index 00000000..94d6cfce
--- /dev/null
+++ b/handlers/admin/gifts/gifts_handler.py
@@ -0,0 +1,207 @@
+from aiogram import F, Router, types, Bot
+from aiogram.types import CallbackQuery
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select, delete, func
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from ..panel.keyboard import AdminPanelCallback
+from .keyboard import build_admin_gifts_kb, build_gifts_list_kb
+from database.models import Tariff, Gift, GiftUsage
+from handlers.payments.gift import finalize_gift
+
+router = Router()
+
+class GiftCreationState(StatesGroup):
+ waiting_for_gift_limit = State()
+ waiting_for_limit_input_or_unlimited = State()
+
+@router.callback_query(AdminPanelCallback.filter(F.action == "gifts"))
+async def admin_gift_menu(callback: CallbackQuery):
+ await callback.message.edit_text(
+ text="🎁 Подарки\nВыберите, что хотите сделать:",
+ reply_markup=build_admin_gifts_kb()
+ )
+
+@router.callback_query(F.data == "admin_gift_create")
+async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession):
+ result = await session.execute(
+ select(Tariff)
+ .where(Tariff.group_code == "gifts", Tariff.is_active == True)
+ .order_by(Tariff.id)
+ )
+ tariffs = result.scalars().all()
+
+ if not tariffs:
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())
+ await callback.message.edit_text("❌ Нет активных тарифов в группе 'gifts'.", reply_markup=builder.as_markup())
+ return
+
+ kb = InlineKeyboardBuilder()
+ for t in tariffs:
+ kb.button(text=f"{t.name} – {t.duration_days // 30} мес", callback_data=f"admin_gift_select|{t.id}")
+ kb.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()
+))
+
+ await callback.message.edit_text(
+ "🎁 Выберите тариф для подарка:",
+ reply_markup=kb.as_markup()
+ )
+
+
+@router.callback_query(F.data.startswith("admin_gift_select|"))
+async def handle_tariff_selection(callback: CallbackQuery, state: FSMContext):
+ tariff_id = int(callback.data.split("|")[1])
+ await state.update_data(tariff_id=tariff_id)
+ await state.set_state(GiftCreationState.waiting_for_limit_input_or_unlimited)
+
+ kb = InlineKeyboardBuilder()
+ kb.button(text="🔙 Назад", callback_data="admin_gift_create")
+ await callback.message.edit_text(
+ "🔢 Введите максимальное количество активаций подарка:",
+ reply_markup=kb.as_markup()
+ )
+
+
+@router.callback_query(F.data == "gift_limit_unlimited")
+async def handle_unlimited_gift(callback: CallbackQuery, state: FSMContext, bot: Bot):
+ data = await state.get_data()
+ session: AsyncSession = callback.bot["session"]
+ await state.clear()
+ await finalize_gift(callback.message, session, bot, data, is_unlimited=True)
+
+@router.message(GiftCreationState.waiting_for_limit_input_or_unlimited)
+async def handle_limited_gift_input(message: types.Message, session: AsyncSession, state: FSMContext, bot: Bot):
+ try:
+ max_usages = int(message.text.strip())
+ if max_usages <= 0:
+ raise ValueError
+ except ValueError:
+ await message.answer("❌ Введите корректное положительное число.")
+ return
+
+ data = await state.get_data()
+ data["max_usages"] = max_usages
+ await state.clear()
+ await finalize_gift(message, session, bot, data, is_unlimited=False)
+
+@router.callback_query(F.data == "admin_gifts_all")
+async def show_gifts_page(callback: CallbackQuery, session: AsyncSession):
+ await show_gift_list(callback, session, page=1)
+
+@router.callback_query(F.data.startswith("gifts_page|"))
+async def paginate_gifts(callback: CallbackQuery, session: AsyncSession):
+ page = int(callback.data.split("|")[1])
+ await show_gift_list(callback, session, page)
+
+
+async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: int):
+ limit = 10
+ offset = (page - 1) * limit
+
+ stmt = select(Gift).order_by(Gift.created_at.desc()).offset(offset).limit(limit)
+ result = await session.execute(stmt)
+ gifts = result.scalars().all()
+
+ from aiogram.utils.keyboard import InlineKeyboardBuilder
+ builder = InlineKeyboardBuilder()
+
+ if not gifts:
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())
+ await callback.message.edit_text(
+ "❌ Подарки не найдены.",
+ reply_markup=builder.as_markup()
+ )
+ return
+
+ keyboard = build_gifts_list_kb(gifts, page, total=len(gifts))
+
+ builder.inline_keyboard.extend(keyboard.inline_keyboard)
+ builder.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()))
+
+ await callback.message.edit_text(
+ f"🎁 Список подарков\nСтраница {page}:",
+ reply_markup=builder.as_markup()
+ )
+
+
+async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: int):
+ limit = 10
+ offset = (page - 1) * limit
+
+ stmt = select(Gift).order_by(Gift.created_at.desc()).offset(offset).limit(limit)
+ result = await session.execute(stmt)
+ gifts = result.scalars().all()
+
+ builder = InlineKeyboardBuilder()
+
+ if not gifts:
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())
+ await callback.message.edit_text(
+ "❌ Подарки не найдены.",
+ reply_markup=builder.as_markup()
+ )
+ return
+
+ keyboard = build_gifts_list_kb(gifts, page, total=len(gifts))
+
+ for row in keyboard.inline_keyboard:
+ builder.row(*row)
+
+ await callback.message.edit_text(
+ f"🎁 Список подарков\nСтраница {page}:",
+ reply_markup=builder.as_markup()
+ )
+
+
+@router.callback_query(F.data.startswith("gift_view|"))
+async def view_gift(callback: CallbackQuery, session: AsyncSession):
+ gift_id = callback.data.split("|")[1]
+
+ result = await session.execute(select(Gift).where(Gift.gift_id == gift_id))
+ gift = result.scalar_one_or_none()
+
+ if not gift:
+ await callback.message.edit_text("❌ Подарок не найден.")
+ return
+
+ usage_result = await session.execute(
+ select(func.count()).select_from(GiftUsage).where(GiftUsage.gift_id == gift_id)
+ )
+ used_count = usage_result.scalar_one()
+
+ usage_text = f"{used_count}/{gift.max_usages}" if gift.max_usages else "∞"
+
+ text = (
+ f"🎁 Подарок\n"
+ f"ID: {gift.gift_id}\n"
+ f"Месяцев: {gift.selected_months}\n"
+ f"Активаций: {usage_text}\n"
+ f"Срок действия: {gift.expiry_time.strftime('%d.%m.%Y')}\n"
+ f"Ссылка для активации:\n
{gift.gift_link}" + ) + + builder = InlineKeyboardBuilder() + builder.button(text="🗑 Удалить", callback_data=f"gift_delete|{gift_id}") + builder.button(text="🔙 Назад", callback_data="admin_gifts_all") + + await callback.message.edit_text(text, reply_markup=builder.as_markup()) + + +@router.callback_query(F.data.startswith("gift_delete|")) +async def delete_gift(callback: CallbackQuery, session: AsyncSession): + gift_id = callback.data.split("|")[1] + + await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id)) + await session.execute(delete(Gift).where(Gift.gift_id == gift_id)) + await session.commit() + + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад к списку", callback_data="admin_gifts_all") + + await callback.message.edit_text( + "✅ Подарок удалён.", + reply_markup=builder.as_markup() + ) diff --git a/handlers/admin/gifts/keyboard.py b/handlers/admin/gifts/keyboard.py new file mode 100644 index 00000000..2be1c4cd --- /dev/null +++ b/handlers/admin/gifts/keyboard.py @@ -0,0 +1,55 @@ +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder +from database.models import Tariff, Gift +from ..panel.keyboard import AdminPanelCallback + +from handlers.buttons import BACK + +def build_admin_gifts_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + builder.row( + InlineKeyboardButton(text="🎁 Создать подарок", callback_data="admin_gift_create") + ) + builder.row( + InlineKeyboardButton(text="📦 Все подарки", callback_data="admin_gifts_all"), + ) + builder.row( + InlineKeyboardButton(text=BACK, callback_data="admin") + ) + + return builder.as_markup() + + +def build_gift_tariffs_kb(tariffs: list[Tariff]) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + for tariff in tariffs: + builder.button( + text=f"{tariff.name} — {tariff.duration_days // 30} мес.", + callback_data=f"admin_gift_confirm|{tariff.id}", + ) + builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + builder.adjust(1) + return builder.as_markup() + + +def build_gifts_list_kb(gifts: list[Gift], page: int, total: int) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for gift in gifts: + button_text = f"{gift.gift_id[:6]}... — {gift.selected_months} мес." + builder.button( + text=button_text, + callback_data=f"gift_view|{gift.gift_id}", + ) + + nav = [] + if page > 1: + nav.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"gifts_page|{page - 1}")) + if len(gifts) == 10: + nav.append(InlineKeyboardButton(text="➡️ Далее", callback_data=f"gifts_page|{page + 1}")) + + builder.row(*nav) + builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + + return builder.as_markup() \ No newline at end of file diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py index b7985acd..f77f0da6 100644 --- a/handlers/admin/panel/keyboard.py +++ b/handlers/admin/panel/keyboard.py @@ -49,6 +49,11 @@ def build_panel_kb() -> InlineKeyboardMarkup: text="💸 Тарифы", callback_data=AdminPanelCallback(action="tariffs").pack() ) ) + builder.row( + InlineKeyboardButton( + text="🎁 Подарки", callback_data=AdminPanelCallback(action="gifts").pack() + ) + ) builder.row( InlineKeyboardButton( text="📊 Статистика", @@ -59,7 +64,7 @@ def build_panel_kb() -> InlineKeyboardMarkup: ), ) builder.button(text=MAIN_MENU, callback_data="profile") - builder.adjust(1, 1, 2, 2, 1, 2, 1) + builder.adjust(1, 1, 2, 2, 1, 1, 2, 1) return builder.as_markup() diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py index 0febdab4..aa8a4e34 100644 --- a/handlers/admin/panel/panel_handler.py +++ b/handlers/admin/panel/panel_handler.py @@ -47,8 +47,8 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC @router.callback_query(F.data == "admin", IsAdminFilter()) -async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): - await handle_admin_message(callback_query.message, state) +async def handle_admin_callback_query_simple(callback_query: CallbackQuery, state: FSMContext): + await handle_admin_callback_query(callback_query, state) @router.message(Command("admin"), IsAdminFilter()) diff --git a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so index b9fed8bd..6d2500c8 100644 Binary files a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so differ