add extend keyboard hooks / refactor hot leads texts / fix hot leads notification logic
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user