From 05c591336b5a4579f8a6373968450cdeec18e524 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Fri, 29 Aug 2025 00:06:47 +0300 Subject: [PATCH] Improved hot lead logic --- database/hot_leads.py | 4 +- database/keys.py | 7 ++ database/notifications.py | 52 ++++++++++++ handlers/keys/key_mode/key_create.py | 46 +++++++++- handlers/keys/key_mode/key_discount_mode.py | 75 +++++++++++++---- handlers/keys/key_renew.py | 61 +++++++++++++- handlers/keys/key_view.py | 12 +++ handlers/keys/operations/renewal.py | 6 ++ handlers/utils.py | 23 ++++- hooks/hook_buttons.py | 93 +++++++++++---------- 10 files changed, 305 insertions(+), 74 deletions(-) diff --git a/database/hot_leads.py b/database/hot_leads.py index 171b3876..47e8fa04 100644 --- a/database/hot_leads.py +++ b/database/hot_leads.py @@ -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] \ No newline at end of file diff --git a/database/keys.py b/database/keys.py index 7a32646d..7af2cfd3 100644 --- a/database/keys.py +++ b/database/keys.py @@ -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() diff --git a/database/notifications.py b/database/notifications.py index 0e938b81..75616bfd 100644 --- a/database/notifications.py +++ b/database/notifications.py @@ -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, diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index dc12510a..b32933bc 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -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🎯 ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!\n
" + if discount_info["type"] == "hot_lead_step_2": + discount_message += "💎 Вам открыт доступ к специальным тарифам\n" + discount_message += "🚀 Эксклюзивные предложения - доступны только для вас!\n" + else: + discount_message += "💎 Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам\n" + discount_message += "🚀 VIP предложения - максимальная выгода!\n" + + expires_at = discount_info["expires_at"] + discount_message += f"
\n⏰ Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!" + 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"] diff --git a/handlers/keys/key_mode/key_discount_mode.py b/handlers/keys/key_mode/key_discount_mode.py index 9e696ae9..1cb6620b 100644 --- a/handlers/keys/key_mode/key_discount_mode.py +++ b/handlers/keys/key_mode/key_discount_mode.py @@ -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( + "🔥 Скидка активна!\n\n" + "💎 Скидка на все тарифы\n" + f"⏰ Осталось: {format_discount_time_left(last_time, DISCOUNT_ACTIVE_HOURS)}\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( + "🔥 Максимальная скидка активна!\n\n" + "💎 Максимальная скидка на все тарифы\n" + f"⏰ Осталось: {format_discount_time_left(last_time, DISCOUNT_ACTIVE_HOURS)}\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"), + ) diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index 3c288c92..9d87741b 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -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🎯 ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!\n
" + if discount_info["type"] == "hot_lead_step_2": + discount_message += "💎 Вам открыт доступ к специальным тарифам для продления\n" + discount_message += "🚀 Эксклюзивные предложения - доступны только для вас!\n" + else: + discount_message += "💎 Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам для продления\n" + discount_message += "🚀 VIP предложения - максимальная выгода!\n" + + expires_at = discount_info["expires_at"] + discount_message += f"
\n⏰ Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!" + 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🎯 ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!\n
" + if discount_info["type"] == "hot_lead_step_2": + discount_message += "💎 Вам открыт доступ к специальным тарифам для продления\n" + discount_message += "🚀 Эксклюзивные предложения - доступны только для вас!\n" + else: + discount_message += "💎 Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам для продления\n" + discount_message += "🚀 VIP предложения - максимальная выгода!\n" + + expires_at = discount_info["expires_at"] + discount_message += f"
\n⏰ Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!" + await edit_or_send_message( target_message=callback.message, - text=f"{subgroup}\n\nВыберите тариф:", + text=f"{subgroup}\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}") diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py index e0d9dc3e..4c290cdb 100644 --- a/handlers/keys/key_view.py +++ b/handlers/keys/key_view.py @@ -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) diff --git a/handlers/keys/operations/renewal.py b/handlers/keys/operations/renewal.py index ef552b50..926aa746 100644 --- a/handlers/keys/operations/renewal.py +++ b/handlers/keys/operations/renewal.py @@ -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 diff --git a/handlers/utils.py b/handlers/utils.py index 18d52977..7d33f588 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -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) diff --git a/hooks/hook_buttons.py b/hooks/hook_buttons.py index faad331a..adac313b 100644 --- a/hooks/hook_buttons.py +++ b/hooks/hook_buttons.py @@ -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))