add extend keyboard hooks / refactor hot leads texts / fix hot leads notification logic

This commit is contained in:
Capybara-z
2025-09-18 21:23:58 +03:00
parent 17b9e92809
commit b68b539c68
7 changed files with 56 additions and 59 deletions
-5
View File
@@ -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}")
-20
View File
@@ -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,
+6 -9
View File
@@ -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🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>"
if discount_info["type"] == "hot_lead_step_2":
discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b>\n"
discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n"
else:
discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b>\n"
discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\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"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}</b>, не упустите свой шанс!"
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,
+9 -18
View File
@@ -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🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>"
if discount_info["type"] == "hot_lead_step_2":
discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b> для продления\n"
discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n"
else:
discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b> для продления\n"
discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\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"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!</b>"
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🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>"
if discount_info["type"] == "hot_lead_step_2":
discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b> для продления\n"
discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n"
else:
discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b> для продления\n"
discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\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"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!</b>"
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,
-5
View File
@@ -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}")
@@ -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({
+29 -2
View File
@@ -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: