diff --git a/database/keys.py b/database/keys.py index 73377012..1936f630 100644 --- a/database/keys.py +++ b/database/keys.py @@ -5,7 +5,6 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from database.models import Key, User -from database.notifications import clear_hot_lead_notifications from logger import logger @@ -43,10 +42,6 @@ async def store_key( await session.commit() logger.info(f"✅ Ключ сохранён: tg_id={tg_id}, client_id={client_id}, server_id={server_id}") - try: - await clear_hot_lead_notifications(session, tg_id) - except Exception: - pass except SQLAlchemyError as e: logger.error(f"❌ Ошибка при сохранении ключа: {e}") diff --git a/database/notifications.py b/database/notifications.py index 64d4d214..4a842635 100644 --- a/database/notifications.py +++ b/database/notifications.py @@ -96,26 +96,6 @@ async def check_hot_lead_discount(session: AsyncSession, tg_id: int) -> dict: return {"available": False} -async def clear_hot_lead_notifications(session: AsyncSession, tg_id: int): - try: - await session.execute( - delete(Notification).where( - Notification.tg_id == tg_id, - Notification.notification_type.in_([ - "hot_lead_step_1", - "hot_lead_step_2", - "hot_lead_step_3", - "hot_lead_step_2_expired", - ]), - ) - ) - await session.commit() - logger.info(f"✅ Уведомления о скидках горячих лидов очищены для пользователя {tg_id}") - except SQLAlchemyError as e: - logger.error(f"❌ Ошибка при очистке уведомлений о скидках для {tg_id}: {e}") - await session.rollback() - - async def check_notifications_bulk( session: AsyncSession, notification_type: str, diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index ddd5664e..2ad4a2fc 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -37,6 +37,9 @@ from handlers.texts import ( CREATING_CONNECTION_MSG, INSUFFICIENT_FUNDS_MSG, SELECT_TARIFF_PLAN_MSG, + DISCOUNT_OFFER_MESSAGE, + DISCOUNT_OFFER_STEP2, + DISCOUNT_OFFER_STEP3, ) from handlers.utils import edit_or_send_message, format_discount_time_left, get_least_loaded_cluster from hooks.hook_buttons import insert_hook_buttons @@ -252,16 +255,10 @@ async def handle_key_creation( discount_message = "" if discount_info and discount_info.get("available"): - discount_message = "\n\n🎯 ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!\n
" - if discount_info["type"] == "hot_lead_step_2": - discount_message += "💎 Вам открыт доступ к специальным тарифам\n" - discount_message += "🚀 Эксклюзивные предложения - доступны только для вас!\n" - else: - discount_message += "💎 Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам\n" - discount_message += "🚀 VIP предложения - максимальная выгода!\n" - + offer_text = DISCOUNT_OFFER_STEP2 if discount_info["type"] == "hot_lead_step_2" else DISCOUNT_OFFER_STEP3 expires_at = discount_info["expires_at"] - discount_message += f"
\n⏰ Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!" + time_left = format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS) + discount_message = DISCOUNT_OFFER_MESSAGE.format(offer_text=offer_text, time_left=time_left) await edit_or_send_message( target_message=target_message, diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index b8b19bcb..309cded5 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -36,6 +36,9 @@ from handlers.texts import ( KEY_NOT_FOUND_MSG, PLAN_SELECTION_MSG, get_renewal_message, + DISCOUNT_OFFER_MESSAGE, + DISCOUNT_OFFER_STEP2, + DISCOUNT_OFFER_STEP3, ) from handlers.utils import edit_or_send_message, format_discount_time_left, get_russian_month from hooks.hook_buttons import insert_hook_buttons @@ -157,16 +160,10 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo discount_message = "" if discount_info.get("available"): - discount_message = "\n\n🎯 ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!\n
" - if discount_info["type"] == "hot_lead_step_2": - discount_message += "💎 Вам открыт доступ к специальным тарифам для продления\n" - discount_message += "🚀 Эксклюзивные предложения - доступны только для вас!\n" - else: - discount_message += "💎 Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам для продления\n" - discount_message += "🚀 VIP предложения - максимальная выгода!\n" - + offer_text = DISCOUNT_OFFER_STEP2 if discount_info["type"] == "hot_lead_step_2" else DISCOUNT_OFFER_STEP3 expires_at = discount_info["expires_at"] - discount_message += f"
\n⏰ Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!" + time_left = format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS) + discount_message = DISCOUNT_OFFER_MESSAGE.format(offer_text=offer_text, time_left=time_left) response_message = ( PLAN_SELECTION_MSG.format( @@ -281,16 +278,10 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont discount_message = "" if discount_info.get("available"): - discount_message = "\n\n🎯 ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!\n
" - if discount_info["type"] == "hot_lead_step_2": - discount_message += "💎 Вам открыт доступ к специальным тарифам для продления\n" - discount_message += "🚀 Эксклюзивные предложения - доступны только для вас!\n" - else: - discount_message += "💎 Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам для продления\n" - discount_message += "🚀 VIP предложения - максимальная выгода!\n" - + offer_text = DISCOUNT_OFFER_STEP2 if discount_info["type"] == "hot_lead_step_2" else DISCOUNT_OFFER_STEP3 expires_at = discount_info["expires_at"] - discount_message += f"
\n⏰ Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!" + time_left = format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS) + discount_message = DISCOUNT_OFFER_MESSAGE.format(offer_text=offer_text, time_left=time_left) await edit_or_send_message( target_message=callback.message, diff --git a/handlers/keys/operations/renewal.py b/handlers/keys/operations/renewal.py index 982bd0fa..c088647e 100644 --- a/handlers/keys/operations/renewal.py +++ b/handlers/keys/operations/renewal.py @@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE from database import delete_notification, get_servers from database.models import Key, Server, Tariff -from database.notifications import clear_hot_lead_notifications from logger import logger from panels._3xui import ClientConfig, add_client, extend_client_key, get_xui_instance from panels.remnawave import RemnawaveAPI @@ -229,10 +228,6 @@ async def renew_key_in_cluster( await delete_notification(session, tg_id, notification_id) logger.info(f"🧹 Уведомления для ключа {email} очищены при продлении.") - try: - await clear_hot_lead_notifications(session, tg_id) - except Exception as e: - logger.warning(f"Не удалось очистить уведомления о скидках для {tg_id} при продлении: {e}") except Exception as e: logger.error(f"Не удалось продлить ключ {client_id} в кластере/на сервере {cluster_id}: {e}") diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py index 644ffe32..5af22148 100644 --- a/handlers/notifications/special_notifications.py +++ b/handlers/notifications/special_notifications.py @@ -30,6 +30,8 @@ from handlers.texts import ( ZERO_TRAFFIC_MSG, ) from handlers.utils import format_days, is_full_remnawave_cluster +from hooks.hook_buttons import insert_hook_buttons +from hooks.hooks import run_hooks from logger import logger @@ -167,6 +169,16 @@ async def notify_users_no_traffic(bot: Bot, session: AsyncSession, current_time: builder.row(InlineKeyboardButton(text="🔧 Написать в поддержку", url=SUPPORT_CHAT_URL)) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) + + try: + hook_commands = await run_hooks( + "zero_traffic_notification", chat_id=tg_id, admin=False, session=session, email=email + ) + if hook_commands: + builder = insert_hook_buttons(builder, hook_commands) + except Exception as e: + logger.warning(f"[ZERO_TRAFFIC_NOTIFICATION] Ошибка при применении хуков: {e}") + keyboard = builder.as_markup() message = ZERO_TRAFFIC_MSG.format(email=email) messages.append({ diff --git a/hooks/hook_buttons.py b/hooks/hook_buttons.py index 9c3edd91..4130c9d1 100644 --- a/hooks/hook_buttons.py +++ b/hooks/hook_buttons.py @@ -12,6 +12,9 @@ def insert_hook_buttons(builder: InlineKeyboardBuilder, buttons: list) -> Inline - {"insert_at": int, "button": InlineKeyboardButton} — вставить по индексу (0 = начало) - {"remove": str | list[str]} — удалить кнопки с указанным callback_data - {"remove_prefix": str} — удалить кнопки, у которых callback_data начинается с префикса + - {"remove_url": str | list[str]} — удалить кнопки с указанным URL + - {"remove_url_prefix": str} — удалить кнопки, у которых URL начинается с префикса + - {"replace_keyboard": InlineKeyboardBuilder} — полностью заменить клавиатуру """ markup = builder.as_markup() new_rows = markup.inline_keyboard.copy() @@ -24,7 +27,15 @@ def insert_hook_buttons(builder: InlineKeyboardBuilder, buttons: list) -> Inline else: flat_buttons.append(item) - remove_operations = [b for b in flat_buttons if isinstance(b, dict) and ("remove" in b or "remove_prefix" in b)] + replace_operations = [b for b in flat_buttons if isinstance(b, dict) and "replace_keyboard" in b] + if replace_operations: + replace_data = replace_operations[0]["replace_keyboard"] + if isinstance(replace_data, InlineKeyboardBuilder): + return replace_data + else: + return builder + + remove_operations = [b for b in flat_buttons if isinstance(b, dict) and ("remove" in b or "remove_prefix" in b or "remove_url" in b or "remove_url_prefix" in b)] for module in remove_operations: removes = module.get("remove") if isinstance(removes, str): @@ -32,12 +43,28 @@ def insert_hook_buttons(builder: InlineKeyboardBuilder, buttons: list) -> Inline removes = set(removes or []) prefix = module.get("remove_prefix") + remove_urls = module.get("remove_url") + if isinstance(remove_urls, str): + remove_urls = [remove_urls] + remove_urls = set(remove_urls or []) + url_prefix = module.get("remove_url_prefix") + filtered_rows = [] for row in new_rows: filtered_row = [] for btn in row: cdata = getattr(btn, "callback_data", None) - if cdata and (cdata in removes or (prefix and cdata.startswith(prefix))): + url = getattr(btn, "url", None) + webapp_url = getattr(getattr(btn, "web_app", None), "url", None) if getattr(btn, "web_app", None) else None + + should_remove_callback = cdata and (cdata in removes or (prefix and cdata.startswith(prefix))) + + should_remove_url = ( + (url and (url in remove_urls or (url_prefix is not None and url.startswith(url_prefix)))) or + (webapp_url and (webapp_url in remove_urls or (url_prefix is not None and webapp_url.startswith(url_prefix)))) + ) + + if should_remove_callback or should_remove_url: continue filtered_row.append(btn) if filtered_row: