From 0cd896d35e493f4aa92eeaad54fce1eb1b786322 Mon Sep 17 00:00:00 2001 From: Vladless Date: Fri, 11 Apr 2025 22:40:38 +0300 Subject: [PATCH] gb_count/fix coupons --- bot.py | 2 +- handlers/admin/coupons/coupons_handler.py | 168 +-------------------- handlers/coupons.py | 176 +++++++++++++++++++--- handlers/keys/key_renew.py | 48 ++---- handlers/keys/key_utils.py | 1 + handlers/start.py | 8 +- 6 files changed, 171 insertions(+), 232 deletions(-) diff --git a/bot.py b/bot.py index 76a75826..03bf08ca 100644 --- a/bot.py +++ b/bot.py @@ -19,7 +19,7 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) -version = "4.2-alpha(10.04)" +version = "4.2-alpha(11.04)" register_middleware(dp) diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index 8e04a043..78a598c5 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -18,19 +18,12 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder 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 handlers.utils import format_days from logger import logger @@ -45,7 +38,6 @@ class AdminCouponsState(StatesGroup): waiting_for_coupon_type = State() waiting_for_balance_data = State() waiting_for_days_data = State() - waiting_for_key_selection = State() @router.callback_query( @@ -328,162 +320,4 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any): 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, text: str = None, user_id: int = None -): - coupon_text = text if text is not None else message.text - logger.info(f"Текст купона в handle_coupon_activation: {coupon_text}") - coupon_code = coupon_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 - - effective_user_id = user_id if user_id is not None else message.from_user.id - - usage = await session.fetchrow( - "SELECT * FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2", - coupon["id"], - effective_user_id - ) - if usage: - await message.answer("❌ Вы уже активировали этот купон.") - return - - connection_exists = await check_connection_exists(effective_user_id) - if not connection_exists: - await add_connection(tg_id=effective_user_id, session=session) - - if coupon["amount"] > 0: - await session.execute( - "UPDATE connections SET balance = balance + $1 WHERE tg_id = $2", - coupon["amount"], - effective_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"], effective_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(effective_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"], user_id=effective_user_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} продлена на {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() + ) \ No newline at end of file diff --git a/handlers/coupons.py b/handlers/coupons.py index e536525d..95e41374 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -5,27 +5,36 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder +import pytz +import html +from datetime import datetime from database import ( check_coupon_usage, create_coupon_usage, get_coupon_by_code, + get_keys, update_balance, update_coupon_usage_count, + update_key_expiry, + check_connection_exists, + add_connection, ) -from handlers.buttons import MAIN_MENU, COUPON_RESTART +from handlers.buttons import MAIN_MENU +from handlers.keys.key_utils import renew_key_in_cluster from handlers.texts import ( - COUPON_ACTIVATED_SUCCESS_MSG, COUPON_ALREADY_USED_MSG, COUPON_INPUT_PROMPT, COUPON_NOT_FOUND_MSG, ) - -from .utils import edit_or_send_message +from handlers.utils import edit_or_send_message, format_days +from handlers.profile import process_callback_view_profile +from logger import logger class CouponActivationState(StatesGroup): waiting_for_coupon_code = State() + waiting_for_key_selection = State() router = Router() @@ -54,32 +63,153 @@ async def handle_activate_coupon(callback_query_or_message: Message | CallbackQu @router.message(CouponActivationState.waiting_for_coupon_code) async def process_coupon_code(message: Message, state: FSMContext, session: Any): coupon_code = message.text.strip() - activation_result = await activate_coupon(message.chat.id, coupon_code, session) - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - builder.row(InlineKeyboardButton(text=COUPON_RESTART, callback_data="activate_coupon")) - - await message.answer(activation_result, reply_markup=builder.as_markup()) - - await state.clear() + await activate_coupon(message, state, session, coupon_code=coupon_code) -async def activate_coupon(user_id: int, coupon_code: str, session: Any): +async def activate_coupon( + message: Message, state: FSMContext, session: Any, coupon_code: str, admin: bool = False +): + logger.info(f"Активация купона: {coupon_code}") coupon_record = await get_coupon_by_code(coupon_code, session) if not coupon_record: - return COUPON_NOT_FOUND_MSG + await message.answer(COUPON_NOT_FOUND_MSG) + await state.clear() + return - usage_exists = await check_coupon_usage(coupon_record["id"], user_id, session) + if coupon_record["usage_count"] >= coupon_record["usage_limit"] or coupon_record["is_used"]: + await message.answer("❌ Лимит активаций купона исчерпан.") + await state.clear() + return - if usage_exists: - return COUPON_ALREADY_USED_MSG + user_id = message.chat.id - coupon_amount = coupon_record["amount"] + usage = await check_coupon_usage(coupon_record["id"], user_id, session) + if usage: + await message.answer(COUPON_ALREADY_USED_MSG) + await state.clear() + return - await update_coupon_usage_count(coupon_record["id"], session) - await create_coupon_usage(coupon_record["id"], user_id, session) + connection_exists = await check_connection_exists(user_id) + if not connection_exists: + await add_connection(tg_id=user_id, session=session) - await update_balance(user_id, coupon_amount, session) - return COUPON_ACTIVATED_SUCCESS_MSG.format(coupon_amount=coupon_amount) + if coupon_record["amount"] > 0: + try: + await update_balance(user_id, coupon_record["amount"], session, skip_referral=True) + await update_coupon_usage_count(coupon_record["id"], session) + await create_coupon_usage(coupon_record["id"], user_id, session) + await message.answer(f"✅ Купон активирован, на баланс начислено {coupon_record['amount']} рублей.") + await process_callback_view_profile(message, state, admin) + await state.clear() + except Exception as e: + logger.error(f"Ошибка при активации купона на баланс: {e}") + await message.answer("❌ Ошибка при активации купона.") + await state.clear() + return + + if coupon_record["days"] is not None and coupon_record["days"] > 0: + try: + keys = await get_keys(user_id, session) + active_keys = [k for k in keys if not k["is_frozen"]] + + if not active_keys: + await message.answer("❌ У вас нет активных подписок для продления.") + await state.clear() + 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_record['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(CouponActivationState.waiting_for_key_selection) + await state.update_data(coupon_id=coupon_record["id"], user_id=user_id) + except Exception as e: + logger.error(f"Ошибка при обработке купона на дни: {e}") + await message.answer("❌ Ошибка при активации купона.") + await state.clear() + return + + await message.answer("❌ Купон недействителен (нет суммы или дней).") + await state.clear() + + +@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]) + + try: + 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 check_coupon_usage(coupon_id, callback_query.from_user.id, session) + 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) + + 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 update_coupon_usage_count(coupon["id"], session) + 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") + await callback_query.message.answer( + f"✅ Купон активирован, подписка {alias} продлена на {format_days(coupon['days'])}⏳ до {expiry_date}📆." + ) + 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.edit_text("⚠️ Активация купона отменена.") + await process_callback_view_profile(callback_query.message, state, admin) + await state.clear() \ No newline at end of file diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index ad895e39..a5225344 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -106,8 +106,7 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, session: An plan, client_id = callback_query.data.split("|")[1], callback_query.data.split("|")[2] days_to_extend = 30 * int(plan) - gb_multiplier = {"1": 1, "3": 3, "6": 6, "12": 12} - total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0 + total_gb = int((int(plan) or 1) * TOTAL_GB * 1024**3) try: record = await get_key_by_server(tg_id, client_id, session) @@ -178,17 +177,11 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, session: An async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, plan): - logger.info( - f"[RENEW] Начинаю процесс продления ключа с параметрами: " - f"tg_id={tg_id}, client_id={client_id}, email={email}, " - f"new_expiry_time={new_expiry_time}, total_gb={total_gb}, cost={cost}, " - f"callback_query={'есть' if callback_query else 'отсутствует'}, plan={plan}" - ) - - response_message = SUCCESS_RENEWAL_MSG.format(months=plan) + logger.info(f"[Info] Продление ключа {client_id} на {plan} мес. (Start)") builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) + response_message = SUCCESS_RENEWAL_MSG.format(months=plan) if callback_query: try: @@ -199,52 +192,33 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g media_path=None, ) except Exception as e: - logger.error(f"Ошибка редактирования сообщения в complete_key_renewal: {e}") + logger.error(f"[Error] Ошибка при редактировании сообщения: {e}") await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) else: await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup()) conn = await asyncpg.connect(DATABASE_URL) - - logger.info(f"[RENEW] Получение данных о ключе для email: {email}") key_info = await get_key_details(email, conn) if not key_info: - logger.error(f"[RENEW] Ключ с client_id {client_id} для пользователя {tg_id} не найден.") + logger.error(f"[Error] Ключ с client_id={client_id} не найден в БД.") await conn.close() return server_id = key_info["server_id"] if USE_COUNTRY_SELECTION: - logger.info(f"[RENEW] USE_COUNTRY_SELECTION включён. Проверяю информацию о сервере {server_id}") cluster_info = await check_server_name_by_cluster(server_id, conn) if not cluster_info: - logger.error(f"[RENEW] Сервер {server_id} не найден в таблице servers.") + logger.error(f"[Error] Сервер {server_id} не найден в таблице servers.") await conn.close() return cluster_id = cluster_info["cluster_name"] - logger.info(f"[RENEW] Информация о сервере получена: {cluster_info}. Использую cluster_id: {cluster_id}") else: cluster_id = server_id - logger.info(f"[RENEW] USE_COUNTRY_SELECTION выключен. Использую server_id в качестве cluster_id: {cluster_id}") - logger.info(f"[RENEW] Запуск продления ключа для пользователя {tg_id} на {plan} мес. в кластере {cluster_id}.") - - async def renew_key_on_cluster(): - logger.info( - f"[RENEW] Запуск renew_key_on_cluster с параметрами: " - f"cluster_id={cluster_id}, email={email}, client_id={client_id}, " - f"new_expiry_time={new_expiry_time}, total_gb={total_gb}" - ) - await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb) - logger.info("[RENEW] Продление ключа на сервере завершено. Обновляю срок действия в базе данных.") - await update_key_expiry(client_id, new_expiry_time, conn) - logger.info("[RENEW] Срок действия ключа обновлён. Обновляю баланс пользователя.") - await update_balance(tg_id, -cost, conn) - logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.") - - logger.info("[RENEW] Инициализация процесса продления ключа в кластере.") - await renew_key_on_cluster() - - logger.info("[RENEW] Процесс продления ключа завершён. Закрываю соединение с базой данных.") + await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb) + await update_key_expiry(client_id, new_expiry_time, conn) + await update_balance(tg_id, -cost, conn) await conn.close() + + logger.info(f"[Info] Продление ключа {client_id} завершено успешно (User: {tg_id})") diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 266d8a18..aa160afc 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -258,6 +258,7 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to uuid=client_id, expire_at=expire_iso, active_user_inbounds=remnawave_inbound_ids, + traffic_limit_bytes=total_gb ) if updated: logger.info(f"Подписка Remnawave {client_id} успешно продлена") diff --git a/handlers/start.py b/handlers/start.py index c5b92ac6..d0c28faa 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -31,7 +31,6 @@ 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_mode.key_create import create_key @@ -48,6 +47,7 @@ from handlers.texts import ( get_about_vpn, ) from logger import logger +from handlers.coupons import activate_coupon from .admin.panel.keyboard import AdminPanelCallback from .utils import edit_or_send_message @@ -119,8 +119,8 @@ async def process_start_logic( try: if "coupons_" in text: logger.info(f"Обнаружена ссылка на купон: {text}") - user_id = message.chat.id - await handle_coupon_activation(message, state, session, admin, text=text, user_id=user_id) + coupon_code = text.split("coupons_")[1] + await activate_coupon(message, state, session, coupon_code=coupon_code, admin=admin) return if "gift_" in text: @@ -320,4 +320,4 @@ async def handle_about_vpn(callback_query: CallbackQuery): reply_markup=builder.as_markup(), media_path=None, force_text=False, - ) + ) \ No newline at end of file