Improved hot lead logic
This commit is contained in:
@@ -8,7 +8,7 @@ async def get_hot_leads(session: AsyncSession):
|
||||
"""
|
||||
Возвращает пользователей, у которых есть успешные оплаты, но нет активных ключей.
|
||||
"""
|
||||
subquery = select(Key.tg_id).distinct()
|
||||
subquery = select(Key.tg_id).where(Key.expiry_time > func.extract("epoch", func.now()) * 1000).distinct()
|
||||
|
||||
stmt = (
|
||||
select(Payment.tg_id)
|
||||
@@ -20,4 +20,4 @@ async def get_hot_leads(session: AsyncSession):
|
||||
)
|
||||
|
||||
result = await session.execute(stmt)
|
||||
return [row.tg_id for row in result]
|
||||
return [row.tg_id for row in result]
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
|
||||
|
||||
@@ -41,6 +42,12 @@ async def store_key(
|
||||
session.add(new_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 as e:
|
||||
pass
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"❌ Ошибка при сохранении ключа: {e}")
|
||||
await session.rollback()
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import insert
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import DISCOUNT_ACTIVE_HOURS
|
||||
from database.models import Key, Notification, User
|
||||
from logger import logger
|
||||
|
||||
@@ -64,6 +65,57 @@ async def get_last_notification_time(session: AsyncSession, tg_id: int, notifica
|
||||
return None
|
||||
|
||||
|
||||
async def check_hot_lead_discount(session: AsyncSession, tg_id: int) -> dict:
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(Notification.notification_type, Notification.last_notification_time)
|
||||
.where(Notification.tg_id == tg_id)
|
||||
.where(Notification.notification_type.in_(['hot_lead_step_2', 'hot_lead_step_3']))
|
||||
.order_by(Notification.last_notification_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
row = result.first()
|
||||
if not row:
|
||||
return {"available": False}
|
||||
|
||||
notification_type, last_time = row
|
||||
|
||||
expires_at = last_time + timedelta(hours=DISCOUNT_ACTIVE_HOURS)
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
if current_time > expires_at:
|
||||
return {"available": False}
|
||||
|
||||
tariff_group = "discounts" if notification_type == "hot_lead_step_2" else "discounts_max"
|
||||
|
||||
return {
|
||||
"available": True,
|
||||
"type": notification_type,
|
||||
"tariff_group": tariff_group,
|
||||
"expires_at": expires_at
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при проверке скидки горячего лида для {tg_id}: {e}")
|
||||
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,
|
||||
|
||||
@@ -16,6 +16,7 @@ from config import (
|
||||
TRIAL_TIME_DISABLE,
|
||||
USE_COUNTRY_SELECTION,
|
||||
USE_NEW_PAYMENT_FLOW,
|
||||
DISCOUNT_ACTIVE_HOURS,
|
||||
)
|
||||
from database import (
|
||||
add_user,
|
||||
@@ -26,6 +27,7 @@ from database import (
|
||||
get_tariffs_for_cluster,
|
||||
get_trial,
|
||||
)
|
||||
from database.notifications import check_hot_lead_discount
|
||||
from database.models import Admin
|
||||
from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs
|
||||
from handlers.admin.panel.keyboard import AdminPanelCallback
|
||||
@@ -40,7 +42,7 @@ from handlers.texts import (
|
||||
INSUFFICIENT_FUNDS_MSG,
|
||||
SELECT_TARIFF_PLAN_MSG,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message, get_least_loaded_cluster
|
||||
from handlers.utils import edit_or_send_message, get_least_loaded_cluster, format_discount_time_left
|
||||
from logger import logger
|
||||
from utils.modules_loader import load_module_fast_flow_handlers
|
||||
|
||||
@@ -142,9 +144,21 @@ async def handle_key_creation(
|
||||
|
||||
tariffs = await get_tariffs_for_cluster(session, cluster_name)
|
||||
|
||||
discount_info = None
|
||||
subgroup_weights = {}
|
||||
|
||||
if tariffs:
|
||||
group_code = tariffs[0].get("group_code")
|
||||
if group_code:
|
||||
from database.notifications import check_hot_lead_discount
|
||||
discount_info = await check_hot_lead_discount(session, tg_id)
|
||||
|
||||
if discount_info and discount_info.get("available"):
|
||||
group_code = discount_info["tariff_group"]
|
||||
await state.update_data(discount_info=discount_info)
|
||||
else:
|
||||
await state.update_data(discount_info=None)
|
||||
|
||||
tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True)
|
||||
tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')]
|
||||
subgroup_weights = tariffs_data['subgroup_weights']
|
||||
@@ -211,7 +225,7 @@ async def handle_key_creation(
|
||||
|
||||
sorted_subgroups = sorted(
|
||||
[k for k in grouped_tariffs if k],
|
||||
key=lambda x: (subgroup_weights.get(x, 999999), x)
|
||||
key=lambda x: (subgroup_weights.get(x, 999999) if subgroup_weights else 999999, x)
|
||||
)
|
||||
|
||||
for subgroup in sorted_subgroups:
|
||||
@@ -227,9 +241,23 @@ async def handle_key_creation(
|
||||
|
||||
target_message = message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query
|
||||
|
||||
discount_message = ""
|
||||
|
||||
if discount_info and discount_info.get("available"):
|
||||
discount_message = f"\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"
|
||||
|
||||
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>, не упустите свой шанс!"
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text=SELECT_TARIFF_PLAN_MSG,
|
||||
text=SELECT_TARIFF_PLAN_MSG + discount_message,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
@@ -313,6 +341,18 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
|
||||
await callback_query.message.edit_text("❌ Указанный тариф не найден.")
|
||||
return
|
||||
|
||||
discount_info = await check_hot_lead_discount(session, tg_id)
|
||||
if tariff.get("group_code") in ["discounts", "discounts_max"]:
|
||||
if not discount_info.get("available") or datetime.utcnow() >= discount_info["expires_at"]:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
"❌ Скидка недоступна или истекла. Пожалуйста, выберите тариф заново.",
|
||||
reply_markup=builder.as_markup()
|
||||
)
|
||||
return
|
||||
|
||||
duration_days = tariff["duration_days"]
|
||||
price_rub = tariff["price_rub"]
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.types import CallbackQuery
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import DISCOUNT_ACTIVE_HOURS
|
||||
from database import get_tariffs
|
||||
from database import get_tariffs, get_keys
|
||||
from database.models import Notification
|
||||
from handlers.utils import format_discount_time_left
|
||||
from handlers.notifications.notify_kb import build_tariffs_keyboard
|
||||
from handlers.texts import DISCOUNT_TARIFF, DISCOUNT_TARIFF_MAX
|
||||
from handlers.buttons import RENEW_KEY_NOTIFICATION, MAIN_MENU
|
||||
from logger import logger
|
||||
|
||||
from .key_create import select_tariff_plan
|
||||
@@ -39,15 +42,33 @@ async def handle_discount_entry(callback: CallbackQuery, session: AsyncSession):
|
||||
await callback.message.edit_text("⏳ Срок действия скидки истёк.")
|
||||
return
|
||||
|
||||
tariffs = await get_tariffs(session=session, group_code="discounts")
|
||||
if not tariffs:
|
||||
await callback.message.edit_text("❌ Скидочные тарифы временно недоступны.")
|
||||
return
|
||||
keys = await get_keys(session, tg_id)
|
||||
|
||||
if keys and len(keys) > 0:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=RENEW_KEY_NOTIFICATION,
|
||||
callback_data=f"renew_key|{keys[0].email}"
|
||||
))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔥 <b>Скидка активна!</b>\n\n"
|
||||
"💎 <b>Скидка</b> на все тарифы\n"
|
||||
f"⏰ Осталось: <b>{format_discount_time_left(last_time, DISCOUNT_ACTIVE_HOURS)}</b>\n\n"
|
||||
"У вас есть просроченная подписка. Продлите её со скидкой!",
|
||||
reply_markup=builder.as_markup()
|
||||
)
|
||||
else:
|
||||
tariffs = await get_tariffs(session=session, group_code="discounts")
|
||||
if not tariffs:
|
||||
await callback.message.edit_text("❌ Скидочные тарифы временно недоступны.")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
DISCOUNT_TARIFF,
|
||||
reply_markup=build_tariffs_keyboard(tariffs, prefix="discount_tariff"),
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
DISCOUNT_TARIFF,
|
||||
reply_markup=build_tariffs_keyboard(tariffs, prefix="discount_tariff"),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("discount_tariff|"))
|
||||
@@ -89,12 +110,30 @@ async def handle_ultra_discount(callback: CallbackQuery, session: AsyncSession):
|
||||
await callback.message.edit_text("⏳ Срок действия финальной скидки истёк.")
|
||||
return
|
||||
|
||||
tariffs = await get_tariffs(session, group_code="discounts_max")
|
||||
if not tariffs:
|
||||
await callback.message.edit_text("❌ Скидочные тарифы временно недоступны.")
|
||||
return
|
||||
keys = await get_keys(session, tg_id)
|
||||
|
||||
if keys and len(keys) > 0:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=RENEW_KEY_NOTIFICATION,
|
||||
callback_data=f"renew_key|{keys[0].email}"
|
||||
))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔥 <b>Максимальная скидка активна!</b>\n\n"
|
||||
"💎 <b>Максимальная скидка</b> на все тарифы\n"
|
||||
f"⏰ Осталось: <b>{format_discount_time_left(last_time, DISCOUNT_ACTIVE_HOURS)}</b>\n\n"
|
||||
"У вас есть просроченная подписка. Продлите её с максимальной скидкой!",
|
||||
reply_markup=builder.as_markup()
|
||||
)
|
||||
else:
|
||||
tariffs = await get_tariffs(session, group_code="discounts_max")
|
||||
if not tariffs:
|
||||
await callback.message.edit_text("❌ Скидочные тарифы временно недоступны.")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
DISCOUNT_TARIFF_MAX,
|
||||
reply_markup=build_tariffs_keyboard(tariffs, prefix="discount_tariff"),
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
DISCOUNT_TARIFF_MAX,
|
||||
reply_markup=build_tariffs_keyboard(tariffs, prefix="discount_tariff"),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from sqlalchemy import or_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot import bot
|
||||
from config import USE_NEW_PAYMENT_FLOW
|
||||
from config import USE_NEW_PAYMENT_FLOW, DISCOUNT_ACTIVE_HOURS
|
||||
from database import (
|
||||
check_tariff_exists,
|
||||
create_temporary_data,
|
||||
@@ -26,6 +26,7 @@ from database import (
|
||||
update_key_expiry,
|
||||
)
|
||||
from database.models import Key, Server
|
||||
from database.notifications import check_hot_lead_discount
|
||||
from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs
|
||||
from handlers.buttons import BACK, MAIN_MENU, MY_SUB, PAYMENT
|
||||
from handlers.keys.operations import renew_key_in_cluster
|
||||
@@ -40,7 +41,7 @@ from handlers.texts import (
|
||||
PLAN_SELECTION_MSG,
|
||||
get_renewal_message,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message, get_russian_month
|
||||
from handlers.utils import edit_or_send_message, get_russian_month, format_discount_time_left
|
||||
from hooks.hooks import run_hooks
|
||||
from hooks.hook_buttons import insert_hook_buttons
|
||||
from logger import logger
|
||||
@@ -99,6 +100,11 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
|
||||
if current_tariff["group_code"] not in ["discounts", "discounts_max", "gifts", "trial"]:
|
||||
group_code = current_tariff["group_code"]
|
||||
|
||||
discount_info = await check_hot_lead_discount(session, tg_id)
|
||||
|
||||
if discount_info.get("available"):
|
||||
group_code = discount_info["tariff_group"]
|
||||
|
||||
tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True)
|
||||
tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')]
|
||||
subgroup_weights = tariffs_data['subgroup_weights']
|
||||
@@ -152,10 +158,24 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
|
||||
final_markup = builder.as_markup()
|
||||
|
||||
balance = await get_balance(session, tg_id)
|
||||
|
||||
discount_message = ""
|
||||
if discount_info.get("available"):
|
||||
discount_message = f"\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"
|
||||
|
||||
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>"
|
||||
|
||||
response_message = PLAN_SELECTION_MSG.format(
|
||||
balance=balance,
|
||||
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
) + discount_message
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
@@ -209,6 +229,12 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
|
||||
|
||||
group_code = row[0]
|
||||
|
||||
tg_id = callback.from_user.id
|
||||
discount_info = await check_hot_lead_discount(session, tg_id)
|
||||
|
||||
if discount_info.get("available"):
|
||||
group_code = discount_info["tariff_group"]
|
||||
|
||||
subgroup = await find_subgroup_by_hash(session, subgroup_hash, group_code)
|
||||
if not subgroup:
|
||||
await callback.message.answer("❌ Подгруппа не найдена.")
|
||||
@@ -250,9 +276,22 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
|
||||
logger.warning(f"[RENEW_SUBGROUP] Ошибка при применении хуков: {e}")
|
||||
final_markup = builder.as_markup()
|
||||
|
||||
discount_message = ""
|
||||
if discount_info.get("available"):
|
||||
discount_message = f"\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"
|
||||
|
||||
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>"
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback.message,
|
||||
text=f"<b>{subgroup}</b>\n\nВыберите тариф:",
|
||||
text=f"<b>{subgroup}</b>\n\nВыберите тариф:{discount_message}",
|
||||
reply_markup=final_markup,
|
||||
)
|
||||
|
||||
@@ -281,6 +320,18 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC
|
||||
await callback_query.message.answer("❌ Тариф не найден или отключён.")
|
||||
return
|
||||
|
||||
discount_info = await check_hot_lead_discount(session, tg_id)
|
||||
if tariff.get("group_code") in ["discounts", "discounts_max"]:
|
||||
if not discount_info.get("available") or datetime.utcnow() >= discount_info["expires_at"]:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await callback_query.message.answer(
|
||||
"❌ Скидка недоступна или истекла. Пожалуйста, выберите тариф заново.",
|
||||
reply_markup=builder.as_markup()
|
||||
)
|
||||
return
|
||||
|
||||
duration_days = tariff["duration_days"]
|
||||
cost = tariff["price_rub"]
|
||||
total_gb = tariff["traffic_limit"] or 0
|
||||
@@ -470,3 +521,5 @@ async def complete_key_renewal(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Error] Ошибка в complete_key_renewal: {e}")
|
||||
|
||||
logger.error(f"[Error] Ошибка в complete_key_renewal: {e}")
|
||||
|
||||
@@ -391,6 +391,18 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any):
|
||||
deleted += 1
|
||||
await callback_query.answer(f"✅ Устройства сброшены ({deleted})", show_alert=True)
|
||||
|
||||
hook_result = await run_hooks("after_hwid_reset", chat_id=callback_query.from_user.id, admin=False, session=session, key_name=key_name)
|
||||
if hook_result and any("redirect_to_profile" in str(result) for result in hook_result):
|
||||
|
||||
kb = InlineKeyboardBuilder()
|
||||
kb.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
if callback_query.message.text:
|
||||
await callback_query.message.edit_text("✅ Устройства сброшены", reply_markup=kb.as_markup())
|
||||
else:
|
||||
await callback_query.message.edit_caption(caption="✅ Устройства сброшены", reply_markup=kb.as_markup())
|
||||
return
|
||||
|
||||
image_path = os.path.join("img", "pic_view.jpg")
|
||||
await render_key_info(callback_query.message, session, key_name, image_path)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.remnawave import RemnawaveAPI
|
||||
from panels.three_xui import ClientConfig, add_client, extend_client_key, get_xui_instance
|
||||
@@ -193,6 +194,11 @@ 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}")
|
||||
raise
|
||||
|
||||
+22
-1
@@ -4,7 +4,7 @@ import re
|
||||
import secrets
|
||||
import string
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import aiofiles
|
||||
|
||||
@@ -303,3 +303,24 @@ def get_username(user) -> str:
|
||||
if getattr(user, "username", None):
|
||||
return "@" + html.escape(user.username)
|
||||
return "Пользователь"
|
||||
|
||||
|
||||
def format_discount_time_left(last_time: datetime, discount_hours: int) -> str:
|
||||
expires_at = last_time + timedelta(hours=discount_hours)
|
||||
current_time = datetime.utcnow()
|
||||
time_left = expires_at - current_time
|
||||
|
||||
if time_left.total_seconds() <= 0:
|
||||
return "⏳ Время истекло"
|
||||
|
||||
total_seconds = int(time_left.total_seconds())
|
||||
days = total_seconds // 86400
|
||||
hours = (total_seconds % 86400) // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
|
||||
if days > 0:
|
||||
return format_days(days)
|
||||
elif hours > 0:
|
||||
return format_hours(hours)
|
||||
else:
|
||||
return format_minutes(minutes)
|
||||
|
||||
+47
-46
@@ -24,54 +24,55 @@ def insert_hook_buttons(builder: InlineKeyboardBuilder, buttons: list) -> Inline
|
||||
else:
|
||||
flat_buttons.append(item)
|
||||
|
||||
for module in flat_buttons:
|
||||
if isinstance(module, dict) and ("remove" in module or "remove_prefix" in module):
|
||||
removes = module.get("remove")
|
||||
if isinstance(removes, str):
|
||||
removes = [removes]
|
||||
removes = set(removes or [])
|
||||
prefix = module.get("remove_prefix")
|
||||
remove_operations = [b for b in flat_buttons if isinstance(b, dict) and ("remove" in b or "remove_prefix" in b)]
|
||||
for module in remove_operations:
|
||||
removes = module.get("remove")
|
||||
if isinstance(removes, str):
|
||||
removes = [removes]
|
||||
removes = set(removes or [])
|
||||
prefix = module.get("remove_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))):
|
||||
continue
|
||||
filtered_row.append(btn)
|
||||
if filtered_row:
|
||||
filtered_rows.append(filtered_row)
|
||||
new_rows = filtered_rows
|
||||
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))):
|
||||
continue
|
||||
filtered_row.append(btn)
|
||||
if filtered_row:
|
||||
filtered_rows.append(filtered_row)
|
||||
new_rows = filtered_rows
|
||||
|
||||
for module in flat_buttons:
|
||||
if isinstance(module, dict) and "after" in module and "button" in module:
|
||||
after = module["after"]
|
||||
button = module["button"]
|
||||
|
||||
insert_pos = -1
|
||||
for i, row in enumerate(new_rows):
|
||||
if any(getattr(btn, "callback_data", None) == after for btn in row):
|
||||
insert_pos = i + 1
|
||||
break
|
||||
|
||||
if 0 <= insert_pos <= len(new_rows):
|
||||
new_rows.insert(insert_pos, [button])
|
||||
else:
|
||||
new_rows.append([button])
|
||||
elif isinstance(module, dict) and "insert_at" in module and "button" in module:
|
||||
insert_at = module["insert_at"]
|
||||
button = module["button"]
|
||||
|
||||
if 0 <= insert_at <= len(new_rows):
|
||||
new_rows.insert(insert_at, [button])
|
||||
else:
|
||||
new_rows.append([button])
|
||||
insert_operations = [b for b in flat_buttons if isinstance(b, dict) and "insert_at" in b and "button" in b]
|
||||
for module in insert_operations:
|
||||
insert_at = module["insert_at"]
|
||||
button = module["button"]
|
||||
|
||||
if 0 <= insert_at <= len(new_rows):
|
||||
new_rows.insert(insert_at, [button])
|
||||
else:
|
||||
if isinstance(module, dict) and "button" in module:
|
||||
button = module["button"]
|
||||
new_rows.append([button])
|
||||
elif module and not isinstance(module, dict):
|
||||
new_rows.append([module])
|
||||
new_rows.append([button])
|
||||
|
||||
after_operations = [b for b in flat_buttons if isinstance(b, dict) and "after" in b and "button" in b]
|
||||
for module in after_operations:
|
||||
after = module["after"]
|
||||
button = module["button"]
|
||||
|
||||
insert_pos = -1
|
||||
for i, row in enumerate(new_rows):
|
||||
if any(getattr(btn, "callback_data", None) == after for btn in row):
|
||||
insert_pos = i + 1
|
||||
break
|
||||
|
||||
if 0 <= insert_pos <= len(new_rows):
|
||||
new_rows.insert(insert_pos, [button])
|
||||
else:
|
||||
new_rows.append([button])
|
||||
|
||||
regular_buttons = [b for b in flat_buttons if isinstance(b, dict) and "button" in b and "insert_at" not in b and "after" not in b]
|
||||
for module in regular_buttons:
|
||||
button = module["button"]
|
||||
new_rows.append([button])
|
||||
|
||||
return InlineKeyboardBuilder.from_markup(InlineKeyboardMarkup(inline_keyboard=new_rows))
|
||||
|
||||
Reference in New Issue
Block a user