From d4c623fb539b84ed8b95e4be292a733da619833b Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 21:12:23 +0300 Subject: [PATCH 01/14] =?UTF-8?q?=D0=94=D0=B0=D1=82=D0=B0=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8/=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена стата даты регистрации/активности --- handlers/admin/users/users_handler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 87c1a9ae..2863496c 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -612,14 +612,19 @@ async def process_user_search( 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) 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) + 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 = ( f"📊 Информация о пользователе" f"\n\n🆔 ID: {tg_id}" f"\n📄 Логин: @{username}" + f"\n📅 Дата регистрации: {created_at}" + f"\n🏃 Дата активности: {updated_at}" f"\n💰 Баланс: {balance}" f"\n👥 Количество рефералов: {referral_count}" ) From 10601761e6329ea5ca583707b414632fed09b974 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 21:24:49 +0300 Subject: [PATCH 02/14] =?UTF-8?q?=D0=9E=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлено отображение пробной подписки если доступна в профиле. --- handlers/profile.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index 8fe644b0..37014528 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -113,9 +113,12 @@ async def process_callback_view_profile( builder = InlineKeyboardBuilder() if trial_status == 0 or key_count == 0: - builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key")) - else: + if key_count > 0: 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")) row_buttons = [] From 137c5e064007ae1b4b381a179319d69c66c6dc1b Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 23:07:05 +0300 Subject: [PATCH 03/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=B4=D0=B5=D0=BB=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BA=D1=83=D0=BF=D0=BE=D0=BD=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добалена возможность делиться купоном при инлайн режиме. --- handlers/admin/coupons/coupons_handler.py | 84 ++++++++++++++++++----- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index 2b5e276e..f48b1148 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -3,9 +3,11 @@ from typing import Any from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, Message +from aiogram.types import CallbackQuery, Message, InlineQuery, InlineQueryResultArticle, InputTextMessageContent +from aiogram.enums import ParseMode +from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import USERNAME_BOT +from config import USERNAME_BOT, INLINE_MODE from database import create_coupon, delete_coupon, get_all_coupons from filters.admin import IsAdminFilter from logger import logger @@ -54,7 +56,8 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session: text = message.text.strip() parts = text.split() - kb = build_admin_back_kb("coupons") + kb = InlineKeyboardBuilder() + kb.button(text="Назад", callback_data=AdminPanelCallback(action="coupons").pack()) if len(parts) != 3: text = ( @@ -62,11 +65,7 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session: "🏷️ код 💰 сумма 🔢 лимит\n" "Пример: 'COUPON1 50 5' 👈" ) - - await message.answer( - text=text, - reply_markup=kb, - ) + await message.answer(text=text, reply_markup=kb.as_markup()) return try: @@ -75,28 +74,32 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session: usage_limit = int(parts[2]) except ValueError: text = "⚠️ Проверьте правильность введенных данных!\n💱 Сумма должна быть числом, а лимит — целым числом." - - await message.answer( - text=text, - reply_markup=kb, - ) + await message.answer(text=text, reply_markup=kb.as_markup()) return try: await create_coupon(coupon_code, coupon_amount, usage_limit, session) + coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}" text = ( f"✅ Купон с кодом {coupon_code} успешно создан!\n" - f"💰 Сумма: {coupon_amount} рублей \n" + f"💰 Сумма: {coupon_amount} рублей\n" f"🔢 Лимит использования: {usage_limit} раз\n" - f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}\n" + f"🔗 Ссылка: {coupon_link}\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="Назад", 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( @@ -172,3 +175,52 @@ async def update_coupons_list(message, session: Any, page: int = 1): f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n" ) await message.edit_text(text=coupon_list, reply_markup=kb) + + +@router.inline_query(F.query.startswith("coupon_")) +async def inline_coupon_handler(inline_query: InlineQuery, session: Any): + if not INLINE_MODE: + 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, + ) + return + + title = f"Купон {coupon['code']}" + description = f"Получи {coupon['amount']} рублей!" + message_text = ( + f"🎫 Купон: {coupon['code']}\n" + f"💰 Бонус: {coupon['amount']} рублей\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 + ) From 6d6989e3c39c3d0deed3e887e15afb904b6def16 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 02:04:07 +0300 Subject: [PATCH 04/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=D1=82=D0=BE=D0=BB=D0=B1=D1=86=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавил создание столбца в купонах days --- assets/schema.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/assets/schema.sql b/assets/schema.sql index b3de6a4b..9db6f6ae 100644 --- a/assets/schema.sql +++ b/assets/schema.sql @@ -72,11 +72,22 @@ CREATE TABLE IF NOT EXISTS coupons id SERIAL PRIMARY KEY, code TEXT UNIQUE NOT NULL, amount INTEGER NOT NULL, + days INTEGER CHECK (days > 0 OR days IS NULL), usage_limit INTEGER NOT NULL DEFAULT 1, usage_count INTEGER NOT NULL DEFAULT 0, 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 ( coupon_id INTEGER NOT NULL REFERENCES coupons (id) ON DELETE CASCADE, From 46aa5c46d459aedf1ac8217922cdab93c087c1e9 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 02:05:36 +0300 Subject: [PATCH 05/14] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BA=D1=83=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Скорректировал работу с купонами так как ранее работа была напрямую из start - Скорретировал работу с купонам под новый столбец days --- database.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/database.py b/database.py index 6b9406d5..ab89a022 100644 --- a/database.py +++ b/database.py @@ -124,33 +124,36 @@ async def check_server_name_by_cluster(server_name: str, session: Any) -> dict | 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: coupon_code (str): Уникальный код купона. - amount (float): Сумма, которую дает купон. + amount (int): Сумма, которую дает купон (0 для купонов на дни). usage_limit (int): Максимальное количество использований купона. session (Any): Сессия базы данных для выполнения запроса. + days (int, optional): Количество дней для продления подписки. Raises: Exception: В случае ошибки при создании купона. 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: await session.execute( """ - INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) - VALUES ($1, $2, $3, 0, FALSE) - """, + INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used, days) + VALUES ($1, $2, $3, 0, FALSE, $4) + """, coupon_code, amount, usage_limit, + days, ) - logger.info(f"Успешно создан купон с кодом {coupon_code} на сумму {amount}") + logger.info(f"Успешно создан купон с кодом {coupon_code} на сумму {amount} или {days} дней") except Exception as e: logger.error(f"Ошибка при создании купона {coupon_code}: {e}") raise @@ -170,7 +173,8 @@ async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None: - usage_limit (int): Лимит использований - usage_count (int): Текущее количество использований - is_used (bool): Флаг использования - - amount (float): Сумма купона + - amount (int): Сумма купона + - days (int): Количество дней (если есть) Raises: Exception: В случае ошибки при выполнении запроса @@ -178,7 +182,7 @@ async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None: try: 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 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 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 ORDER BY id LIMIT $1 OFFSET $2 @@ -221,12 +225,9 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): per_page, offset, ) - 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})") - return {"coupons": coupons, "total": total_count, "pages": total_pages, "current_page": page} except Exception as e: logger.error(f"Критическая ошибка при получении списка купонов: {e}") @@ -1683,12 +1684,12 @@ async def get_last_payments(tg_id: int, session: Any): raise -async def get_coupon_details(coupon_id: str, session: Any): +async def get_coupon_details(coupon_id: int, session: Any): """ Получает детали купона по его ID. Args: - coupon_id (str): ID купона + coupon_id (int): ID купона session (Any): Сессия базы данных Returns: @@ -1700,20 +1701,17 @@ async def get_coupon_details(coupon_id: str, session: Any): try: 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 WHERE id = $1 """, coupon_id, ) - if record: logger.info(f"Успешно получены детали купона {coupon_id}") return dict(record) - logger.warning(f"Купон {coupon_id} не найден") return None - except Exception as e: logger.error(f"Ошибка при получении деталей купона {coupon_id}: {e}") raise From ca8a1b85f1fe9c4294bc990e3e819279dc719cb6 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 02:06:28 +0300 Subject: [PATCH 06/14] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BA=D1=83=D0=BF=D0=BE=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Логика купонов перенесена в свое место. --- handlers/start.py | 44 +++----------------------------------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/handlers/start.py b/handlers/start.py index c68c2f7c..e355bd55 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -31,6 +31,7 @@ from database import ( get_trial, 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.captcha import generate_captcha from handlers.keys.key_management import create_key @@ -119,47 +120,8 @@ async def process_start_logic( try: if "coupons_" in text: logger.info(f"Обнаружена ссылка на купон: {text}") - coupon_code = text.split("coupons_")[1].strip() - - 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) + await handle_coupon_activation(message, state, session) + return if "gift_" in text: parts = text.split("gift_")[1].split("_") From 517e69948cc4b143a476dcb6d629bf7566a231db Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 02:31:54 +0300 Subject: [PATCH 07/14] =?UTF-8?q?=D0=9A=D1=83=D0=BF=D0=BE=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=B4=D0=BD=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Купоны теперь можно выдать днями - Добавил поделиться днями через инлайн мод. --- handlers/admin/coupons/coupons_handler.py | 306 ++++++++++++++++++++-- 1 file changed, 282 insertions(+), 24 deletions(-) diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index f48b1148..fa1a877c 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -1,15 +1,36 @@ +from datetime import datetime from typing import Any +import html +import pytz from aiogram import F, Router +from aiogram.enums import ParseMode from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, Message, InlineQuery, InlineQueryResultArticle, InputTextMessageContent -from aiogram.enums import ParseMode +from aiogram.types import ( + CallbackQuery, + InlineQuery, + InlineQueryResultArticle, + InputTextMessageContent, + Message, +) from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import USERNAME_BOT, INLINE_MODE -from database import create_coupon, delete_coupon, get_all_coupons +from config import INLINE_MODE, USERNAME_BOT +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 handlers.buttons import BACK +from handlers.keys.key_utils import renew_key_in_cluster +from handlers.profile import process_callback_view_profile from logger import logger from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb @@ -20,16 +41,17 @@ router = Router() 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( AdminPanelCallback.filter(F.action == "coupons"), IsAdminFilter(), ) -async def handle_coupons( - callback_query: CallbackQuery, -): +async def handle_coupons(callback_query: CallbackQuery): await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb()) @@ -38,26 +60,52 @@ async def handle_coupons( IsAdminFilter(), ) async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext): + text = "🎫 Выберите тип купона:" + 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 = ( "🎫 Введите данные для создания купона в формате:\n\n" "📝 код 💰 сумма 🔢 лимит\n\n" "Пример: 'COUPON1 50 5' 👈\n\n" ) + kb = InlineKeyboardBuilder() + kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack()) - await callback_query.message.edit_text( - text=text, - reply_markup=build_admin_back_kb("coupons"), + await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup()) + await state.set_state(AdminCouponsState.waiting_for_balance_data) + + +@router.callback_query(F.data == "coupon_type_days") +async def handle_days_coupon_selection(callback_query: CallbackQuery, state: FSMContext): + text = ( + "🎫 Введите данные для создания купона в формате:\n\n" + "📝 коддни 🔢 лимит\n\n" + "Пример: 'DAYS10 10 50' 👈\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()) -async def handle_coupon_data_input(message: Message, state: FSMContext, session: Any): +@router.message(AdminCouponsState.waiting_for_balance_data, IsAdminFilter()) +async def handle_balance_coupon_input(message: Message, state: FSMContext, session: Any): text = message.text.strip() parts = text.split() kb = InlineKeyboardBuilder() - kb.button(text="Назад", callback_data=AdminPanelCallback(action="coupons").pack()) + kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack()) if len(parts) != 3: text = ( @@ -70,15 +118,17 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session: try: coupon_code = parts[0] - coupon_amount = float(parts[1]) + coupon_amount = int(parts[1]) usage_limit = int(parts[2]) + if coupon_amount <= 0: + raise ValueError("Сумма должна быть больше 0") except ValueError: text = "⚠️ Проверьте правильность введенных данных!\n💱 Сумма должна быть числом, а лимит — целым числом." await message.answer(text=text, reply_markup=kb.as_markup()) return 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 = ( @@ -91,7 +141,60 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session: kb = InlineKeyboardBuilder() if INLINE_MODE: kb.button(text="📤 Поделиться", switch_inline_query=f"coupon_{coupon_code}") - kb.button(text="Назад", callback_data=AdminPanelCallback(action="coupons").pack()) + 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.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 = ( + "❌ Некорректный формат! 📝 Пожалуйста, введите данные в формате:\n" + "🏷️ коддни 🔢 лимит\n" + "Пример: 'DAYS10 10 50' 👈" + ) + 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 = "⚠️ Проверьте правильность введенных данных!\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"✅ Купон с кодом {coupon_code} успешно создан!\n" + f"⏳ Дней: {days}\n" + f"🔢 Лимит использования: {usage_limit} раз\n" + f"🔗 Ссылка: {coupon_link}\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()) @@ -124,12 +227,13 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any): kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"]) coupon_list = "📜 Список всех купонов:\n\n" for coupon in coupons: + value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}" coupon_list += ( f"🏷️ Код: {coupon['code']}\n" - f"💰 Сумма: {coupon['amount']} рублей\n" + f"{value_text}\n" f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" f"✅ Использовано: {coupon['usage_count']} раз\n" - f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n" + f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n\n" ) await callback_query.message.edit_text(text=coupon_list, reply_markup=kb) except Exception as e: @@ -167,12 +271,13 @@ async def update_coupons_list(message, session: Any, page: int = 1): kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"]) coupon_list = "📜 Список всех купонов:\n\n" for coupon in coupons: + value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}" coupon_list += ( f"🏷️ Код: {coupon['code']}\n" - f"💰 Сумма: {coupon['amount']} рублей\n" + f"{value_text}\n" f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" f"✅ Использовано: {coupon['usage_count']} раз\n" - f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n" + f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n\n" ) await message.edit_text(text=coupon_list, reply_markup=kb) @@ -198,10 +303,10 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any): return title = f"Купон {coupon['code']}" - description = f"Получи {coupon['amount']} рублей!" + description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {coupon['days']} дней!" message_text = ( f"🎫 Купон: {coupon['code']}\n" - f"💰 Бонус: {coupon['amount']} рублей\n" + f"{'💰 Бонус: ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ Продление: ' + str(coupon['days']) + ' дней'}\n" f"👇 Нажми, чтобы активировать!" ) @@ -224,3 +329,156 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any): 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 = "🔑 Выберите подписку для продления:\n\n
" + + 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"• {key_display} ({expiry_date})\n" + builder.button(text=key_display, callback_data=f"extend_key|{client_id}|{coupon['id']}") + + response_message += "
" + 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"✅ Купон активирован, подписка {alias} продлена на {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() From 44f79a52ca6904d070d55d83f495f5e8187e3286 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 09:57:39 +0300 Subject: [PATCH 08/14] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=20=D0=B4=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/admin/coupons/keyboard.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/handlers/admin/coupons/keyboard.py b/handlers/admin/coupons/keyboard.py index d0ddd9c5..6c0a3eaf 100644 --- a/handlers/admin/coupons/keyboard.py +++ b/handlers/admin/coupons/keyboard.py @@ -1,14 +1,15 @@ from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder +from typing import Optional from handlers.buttons import BACK - from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"): coupon_code: str + confirm: Optional[bool] = None def build_coupons_kb() -> InlineKeyboardMarkup: @@ -50,3 +51,17 @@ def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) -> builder.row(build_admin_back_btn("coupons")) builder.adjust(2) 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"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}" + coupon_list += ( + f"🏷️ Код: {coupon['code']}\n" + f"{value_text}\n" + f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" + f"✅ Использовано: {coupon['usage_count']} раз\n" + f"🔗 Ссылка: https://t.me/{username_bot}?start=coupons_{coupon['code']}\n\n" + ) + return coupon_list From fca235ba98c7180693ef92abb87ed90ca92cb54c Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 09:58:23 +0300 Subject: [PATCH 09/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=B4=D0=B2=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавил подверждение удаления - Поправил удаление купонов. --- handlers/admin/coupons/coupons_handler.py | 95 +++++++++++------------ 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index fa1a877c..02bba4c5 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -34,7 +34,7 @@ from handlers.profile import process_callback_view_profile from logger import logger 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() @@ -213,47 +213,55 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any): try: data = AdminPanelCallback.unpack(callback_query.data) page = data.page if data.page is not None else 1 - per_page = 10 - result = await get_all_coupons(session, page, per_page) - coupons = result["coupons"] + await update_coupons_list(callback_query.message, session, page) + except Exception as e: + 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"Вы уверены, что хотите удалить купон {coupon_code}?", + 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( - text="❌ На данный момент нет доступных купонов!", - reply_markup=build_admin_back_kb("coupons"), + "Произошла ошибка при удалении купона.", + reply_markup=build_admin_back_kb("coupons") ) return - kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"]) - coupon_list = "📜 Список всех купонов:\n\n" - for coupon in coupons: - value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}" - coupon_list += ( - f"🏷️ Код: {coupon['code']}\n" - f"{value_text}\n" - f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" - f"✅ Использовано: {coupon['usage_count']} раз\n" - f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n\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) + await update_coupons_list(callback_query.message, session) async def update_coupons_list(message, session: Any, page: int = 1): @@ -269,17 +277,8 @@ async def update_coupons_list(message, session: Any, page: int = 1): return kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"]) - coupon_list = "📜 Список всех купонов:\n\n" - for coupon in coupons: - value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}" - coupon_list += ( - f"🏷️ Код: {coupon['code']}\n" - f"{value_text}\n" - f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" - f"✅ Использовано: {coupon['usage_count']} раз\n" - f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n\n" - ) - await message.edit_text(text=coupon_list, reply_markup=kb) + text = format_coupons_list(coupons, USERNAME_BOT) + await message.edit_text(text=text, reply_markup=kb) @router.inline_query(F.query.startswith("coupon_")) From 9dc00d815bd09d3fbb440534e42f324617b99d2c Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 10:25:39 +0300 Subject: [PATCH 10/14] =?UTF-8?q?=D0=A1=D0=BA=D0=BB=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BD=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Склонение дней --- handlers/admin/coupons/coupons_handler.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index 02bba4c5..4dcd8a78 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -1,7 +1,7 @@ from datetime import datetime -from typing import Any import html import pytz +from typing import Any from aiogram import F, Router from aiogram.enums import ParseMode @@ -31,6 +31,7 @@ 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 ..panel.keyboard import AdminPanelCallback, build_admin_back_kb @@ -186,7 +187,7 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session: coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}" text = ( f"✅ Купон с кодом {coupon_code} успешно создан!\n" - f"⏳ Дней: {days}\n" + f"⏳ {format_days(days)}\n" f"🔢 Лимит использования: {usage_limit} раз\n" f"🔗 Ссылка: {coupon_link}\n" ) @@ -302,10 +303,10 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any): return title = f"Купон {coupon['code']}" - description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {coupon['days']} дней!" + description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {format_days(coupon['days'])}!" message_text = ( f"🎫 Купон: {coupon['code']}\n" - f"{'💰 Бонус: ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ Продление: ' + str(coupon['days']) + ' дней'}\n" + f"{'💰 Бонус: ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ Продление: ' + format_days(coupon['days'])}\n" f"👇 Нажми, чтобы активировать!" ) @@ -329,7 +330,6 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any): 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] @@ -464,7 +464,7 @@ async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext, 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"✅ Купон активирован, подписка {alias} продлена на {coupon['days']}⏳ дней до {expiry_date}📆." + text = f"✅ Купон активирован, подписка {alias} продлена на {format_days(coupon['days'])}⏳ до {expiry_date}📆." await callback_query.message.answer(text) await process_callback_view_profile(callback_query.message, state, admin) From ad7ed3e0b02592f341de52a60f1a4270eb9db4c1 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 10:25:58 +0300 Subject: [PATCH 11/14] =?UTF-8?q?=D0=A1=D0=BA=D0=BB=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BD=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Склонение дней --- handlers/admin/coupons/keyboard.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/handlers/admin/coupons/keyboard.py b/handlers/admin/coupons/keyboard.py index 6c0a3eaf..f497ec8e 100644 --- a/handlers/admin/coupons/keyboard.py +++ b/handlers/admin/coupons/keyboard.py @@ -1,9 +1,12 @@ +from typing import Optional + from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from typing import Optional from handlers.buttons import BACK +from handlers.utils import format_days + from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn @@ -56,7 +59,7 @@ def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) -> def format_coupons_list(coupons: list, username_bot: str) -> str: coupon_list = "📜 Список всех купонов:\n\n" for coupon in coupons: - value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}" + value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ {format_days(coupon['days'])}" coupon_list += ( f"🏷️ Код: {coupon['code']}\n" f"{value_text}\n" From b836bcdc7030ec57dfca6fb3959b278ba09403ee Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 10:26:47 +0300 Subject: [PATCH 12/14] =?UTF-8?q?=D0=A4=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BA=D0=BB=D0=BE=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B4=D0=BD=D0=B5=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена функция склонения дней. --- handlers/utils.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/handlers/utils.py b/handlers/utils.py index 75171ac4..b9c2dd5c 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -152,6 +152,27 @@ def format_time_until_deletion(seconds: int) -> str: return " и ".join(parts) if parts else "менее минуты" +def format_days(days: int) -> str: + """ + Форматирует количество дней с правильным склонением. + + Args: + days (int): Количество дней. + + Returns: + str: Строка с числом и склонённым словом "день/дня/дней". + """ + if days <= 0: + return "0 дней" + + if days % 10 == 1 and days % 100 != 11: + return f"{days} день" + elif days % 10 in [2, 3, 4] and days % 100 not in [12, 13, 14]: + return f"{days} дня" + else: + return f"{days} дней" + + async def edit_or_send_message( target_message: Message, text: str, From d4e4bbabe823cd18d046255cc219cf2151bb9ba7 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 11:32:05 +0300 Subject: [PATCH 13/14] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Скорректировал текст, при смене названия синхронизация не нужна. --- handlers/admin/clusters/clusters_handler.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index a169d8b2..0132fbbd 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -616,7 +616,7 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses ) 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"), ) except Exception as e: @@ -722,10 +722,7 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess old_server_name ) - # Формируем текст сообщения с учетом USE_COUNTRY_SELECTION - 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 "") + final_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!" await message.answer( text=final_text, From 252c072cec473d436eb545966182d3d0069a0e92 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Thu, 3 Apr 2025 22:17:19 +0300 Subject: [PATCH 14/14] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB?= =?UTF-8?q?=D1=8B=20=D1=81=D0=BA=D0=BB=D0=BE=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добалвение формулы склонения - Адаптация клонение дней к новой формуле. --- handlers/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/handlers/utils.py b/handlers/utils.py index b9c2dd5c..72b939ef 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -152,6 +152,12 @@ def format_time_until_deletion(seconds: int) -> str: 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: """ Форматирует количество дней с правильным склонением. @@ -164,13 +170,7 @@ def format_days(days: int) -> str: """ if days <= 0: return "0 дней" - - if days % 10 == 1 and days % 100 != 11: - return f"{days} день" - elif days % 10 in [2, 3, 4] and days % 100 not in [12, 13, 14]: - return f"{days} дня" - else: - return f"{days} дней" + return f"{days} {get_plural_form(days, 'день', 'дня', 'дней')}" async def edit_or_send_message(