Revert "Revert "Improve bot configuration admin menu grouping""

This commit is contained in:
Egor
2025-09-25 18:04:04 +03:00
committed by GitHub
parent 9635f9464e
commit 817fb82476

View File

@@ -1,5 +1,5 @@
import math
from typing import Tuple
from typing import Iterable, List, Tuple
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
@@ -16,22 +16,141 @@ CATEGORY_PAGE_SIZE = 10
SETTINGS_PAGE_SIZE = 8
def _parse_category_payload(payload: str) -> Tuple[str, int]:
CATEGORY_GROUP_DEFINITIONS: Tuple[Tuple[str, str, Tuple[str, ...]], ...] = (
(
"core",
"⚙️ Основные настройки",
(
"DEFAULT",
"ADMIN",
"SUPPORT",
"REMNAWAVE",
"VERSION",
"DEBUG",
"LOG",
"BACKUP",
),
),
(
"infrastructure",
"🗄️ Инфраструктура",
("DATABASE", "POSTGRES", "SQLITE", "REDIS", "WEBHOOK", "SERVER", "MONITORING", "MAINTENANCE"),
),
(
"billing",
"💳 Оплаты и тарифы",
("PRICE", "PAYMENT", "YOOKASSA", "CRYPTOBOT", "MULENPAY", "PAL24", "AUTOPAY", "TRIAL"),
),
(
"communication",
"📡 Подключение и коммуникации",
("CONNECT", "CHANNEL", "TELEGRAM", "HAPP"),
),
(
"loyalty",
"🎁 Рефералы и лояльность",
("REFERRAL", "TRIBUTE"),
),
)
CATEGORY_FALLBACK_KEY = "other"
CATEGORY_FALLBACK_TITLE = "📦 Прочие настройки"
def _chunk(buttons: Iterable[types.InlineKeyboardButton], size: int) -> Iterable[List[types.InlineKeyboardButton]]:
buttons_list = list(buttons)
for index in range(0, len(buttons_list), size):
yield buttons_list[index : index + size]
def _parse_category_payload(payload: str) -> Tuple[str, str, int, int]:
parts = payload.split(":")
if len(parts) == 3:
_, category_key, page_raw = parts
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
category_key = parts[2] if len(parts) > 2 else ""
def _safe_int(value: str, default: int = 1) -> int:
try:
return category_key, max(1, int(page_raw))
except ValueError:
return category_key, 1
if len(parts) == 2:
_, category_key = parts
return category_key, 1
return "", 1
return max(1, int(value))
except (TypeError, ValueError):
return default
category_page = _safe_int(parts[3]) if len(parts) > 3 else 1
settings_page = _safe_int(parts[4]) if len(parts) > 4 else 1
return group_key, category_key, category_page, settings_page
def _build_categories_keyboard(language: str, page: int = 1) -> types.InlineKeyboardMarkup:
def _parse_group_payload(payload: str) -> Tuple[str, int]:
parts = payload.split(":")
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
try:
page = max(1, int(parts[2]))
except (IndexError, ValueError):
page = 1
return group_key, page
def _get_grouped_categories() -> List[Tuple[str, str, List[Tuple[str, str, int]]]]:
categories = bot_configuration_service.get_categories()
categories_map = {key: (label, count) for key, label, count in categories}
used: set[str] = set()
grouped: List[Tuple[str, str, List[Tuple[str, str, int]]]] = []
for group_key, title, category_keys in CATEGORY_GROUP_DEFINITIONS:
items: List[Tuple[str, str, int]] = []
for category_key in category_keys:
if category_key in categories_map:
label, count = categories_map[category_key]
items.append((category_key, label, count))
used.add(category_key)
if items:
grouped.append((group_key, title, items))
remaining = [
(key, label, count)
for key, (label, count) in categories_map.items()
if key not in used
]
if remaining:
remaining.sort(key=lambda item: item[1])
grouped.append((CATEGORY_FALLBACK_KEY, CATEGORY_FALLBACK_TITLE, remaining))
return grouped
def _build_groups_keyboard() -> types.InlineKeyboardMarkup:
grouped = _get_grouped_categories()
rows: list[list[types.InlineKeyboardButton]] = []
for group_key, title, items in grouped:
total = sum(count for _, _, count in items)
rows.append(
[
types.InlineKeyboardButton(
text=f"{title} ({total})",
callback_data=f"botcfg_group:{group_key}:1",
)
]
)
rows.append(
[
types.InlineKeyboardButton(
text="⬅️ Назад",
callback_data="admin_submenu_settings",
)
]
)
return types.InlineKeyboardMarkup(inline_keyboard=rows)
def _build_categories_keyboard(
group_key: str,
group_title: str,
categories: List[Tuple[str, str, int]],
page: int = 1,
) -> types.InlineKeyboardMarkup:
total_pages = max(1, math.ceil(len(categories) / CATEGORY_PAGE_SIZE))
page = max(1, min(page, total_pages))
@@ -40,45 +159,68 @@ def _build_categories_keyboard(language: str, page: int = 1) -> types.InlineKeyb
sliced = categories[start:end]
rows: list[list[types.InlineKeyboardButton]] = []
rows.append(
[
types.InlineKeyboardButton(
text=f"{group_title}",
callback_data="botcfg_group:noop",
)
]
)
buttons: List[types.InlineKeyboardButton] = []
for category_key, label, count in sliced:
button_text = f"{label} ({count})"
rows.append([
buttons.append(
types.InlineKeyboardButton(
text=button_text,
callback_data=f"botcfg_cat:{category_key}:1",
callback_data=f"botcfg_cat:{group_key}:{category_key}:{page}:1",
)
])
)
for chunk in _chunk(buttons, 2):
rows.append(list(chunk))
if total_pages > 1:
nav_row: list[types.InlineKeyboardButton] = []
if page > 1:
nav_row.append(
types.InlineKeyboardButton(
text="⬅️", callback_data=f"botcfg_categories:{page - 1}"
text="⬅️",
callback_data=f"botcfg_group:{group_key}:{page - 1}",
)
)
nav_row.append(
types.InlineKeyboardButton(
text=f"{page}/{total_pages}", callback_data="botcfg_categories:noop"
text=f"{page}/{total_pages}",
callback_data="botcfg_group:noop",
)
)
if page < total_pages:
nav_row.append(
types.InlineKeyboardButton(
text="➡️", callback_data=f"botcfg_categories:{page + 1}"
text="➡️",
callback_data=f"botcfg_group:{group_key}:{page + 1}",
)
)
rows.append(nav_row)
rows.append([
types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_settings")
])
rows.append(
[
types.InlineKeyboardButton(
text="⬅️ К разделам",
callback_data="admin_bot_config",
)
]
)
return types.InlineKeyboardMarkup(inline_keyboard=rows)
def _build_settings_keyboard(
category_key: str,
group_key: str,
category_page: int,
language: str,
page: int = 1,
) -> types.InlineKeyboardMarkup:
@@ -94,13 +236,19 @@ def _build_settings_keyboard(
for definition in sliced:
value_preview = bot_configuration_service.format_value_for_list(definition.key)
button_text = f"{definition.key} = {value_preview}"
rows.append([
types.InlineKeyboardButton(
text=button_text,
callback_data=f"botcfg_setting:{definition.key}",
)
])
button_text = f"{definition.display_name} · {value_preview}"
if len(button_text) > 64:
button_text = button_text[:63] + ""
rows.append(
[
types.InlineKeyboardButton(
text=button_text,
callback_data=(
f"botcfg_setting:{group_key}:{category_page}:{page}:{definition.key}"
),
)
]
)
if total_pages > 1:
nav_row: list[types.InlineKeyboardButton] = []
@@ -108,7 +256,9 @@ def _build_settings_keyboard(
nav_row.append(
types.InlineKeyboardButton(
text="⬅️",
callback_data=f"botcfg_cat:{category_key}:{page - 1}",
callback_data=(
f"botcfg_cat:{group_key}:{category_key}:{category_page}:{page - 1}"
),
)
)
nav_row.append(
@@ -120,7 +270,9 @@ def _build_settings_keyboard(
nav_row.append(
types.InlineKeyboardButton(
text="➡️",
callback_data=f"botcfg_cat:{category_key}:{page + 1}",
callback_data=(
f"botcfg_cat:{group_key}:{category_key}:{category_page}:{page + 1}"
),
)
)
rows.append(nav_row)
@@ -128,14 +280,19 @@ def _build_settings_keyboard(
rows.append([
types.InlineKeyboardButton(
text="⬅️ К категориям",
callback_data="admin_bot_config",
callback_data=f"botcfg_group:{group_key}:{category_page}",
)
])
return types.InlineKeyboardMarkup(inline_keyboard=rows)
def _build_setting_keyboard(key: str) -> types.InlineKeyboardMarkup:
def _build_setting_keyboard(
key: str,
group_key: str,
category_page: int,
settings_page: int,
) -> types.InlineKeyboardMarkup:
definition = bot_configuration_service.get_definition(key)
rows: list[list[types.InlineKeyboardButton]] = []
@@ -143,14 +300,18 @@ def _build_setting_keyboard(key: str) -> types.InlineKeyboardMarkup:
rows.append([
types.InlineKeyboardButton(
text="🔁 Переключить",
callback_data=f"botcfg_toggle:{key}",
callback_data=(
f"botcfg_toggle:{group_key}:{category_page}:{settings_page}:{key}"
),
)
])
rows.append([
types.InlineKeyboardButton(
text="✏️ Изменить",
callback_data=f"botcfg_edit:{key}",
callback_data=(
f"botcfg_edit:{group_key}:{category_page}:{settings_page}:{key}"
),
)
])
@@ -158,14 +319,18 @@ def _build_setting_keyboard(key: str) -> types.InlineKeyboardMarkup:
rows.append([
types.InlineKeyboardButton(
text="♻️ Сбросить",
callback_data=f"botcfg_reset:{key}",
callback_data=(
f"botcfg_reset:{group_key}:{category_page}:{settings_page}:{key}"
),
)
])
rows.append([
types.InlineKeyboardButton(
text="⬅️ Назад",
callback_data=f"botcfg_cat:{definition.category_key}:1",
callback_data=(
f"botcfg_cat:{group_key}:{definition.category_key}:{category_page}:{settings_page}"
),
)
])
@@ -177,7 +342,9 @@ def _render_setting_text(key: str) -> str:
lines = [
"🧩 <b>Настройка</b>",
f"<b>Название:</b> {summary['name']}",
f"<b>Ключ:</b> <code>{summary['key']}</code>",
f"<b>Категория:</b> {summary['category_label']}",
f"<b>Тип:</b> {summary['type']}",
f"<b>Текущее значение:</b> {summary['current']}",
f"<b>Значение по умолчанию:</b> {summary['original']}",
@@ -194,9 +361,9 @@ async def show_bot_config_menu(
db_user: User,
db: AsyncSession,
):
keyboard = _build_categories_keyboard(db_user.language)
keyboard = _build_groups_keyboard()
await callback.message.edit_text(
"🧩 <b>Конфигурация бота</b>\n\nВыберите категорию настроек:",
"🧩 <b>Конфигурация бота</b>\n\nВыберите раздел настроек:",
reply_markup=keyboard,
)
await callback.answer()
@@ -204,20 +371,23 @@ async def show_bot_config_menu(
@admin_required
@error_handler
async def show_bot_config_categories_page(
async def show_bot_config_group(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
):
parts = callback.data.split(":")
try:
page = int(parts[1])
except (IndexError, ValueError):
page = 1
group_key, page = _parse_group_payload(callback.data)
grouped = _get_grouped_categories()
group_lookup = {key: (title, items) for key, title, items in grouped}
keyboard = _build_categories_keyboard(db_user.language, page)
if group_key not in group_lookup:
await callback.answer("Эта группа больше недоступна", show_alert=True)
return
group_title, items = group_lookup[group_key]
keyboard = _build_categories_keyboard(group_key, group_title, items, page)
await callback.message.edit_text(
"🧩 <b>Конфигурация бота</b>\n\nВыберите категорию настроек:",
f"🧩 <b>{group_title}</b>\n\nВыберите категорию настроек:",
reply_markup=keyboard,
)
await callback.answer()
@@ -230,7 +400,9 @@ async def show_bot_config_category(
db_user: User,
db: AsyncSession,
):
category_key, page = _parse_category_payload(callback.data)
group_key, category_key, category_page, settings_page = _parse_category_payload(
callback.data
)
definitions = bot_configuration_service.get_settings_for_category(category_key)
if not definitions:
@@ -238,7 +410,13 @@ async def show_bot_config_category(
return
category_label = definitions[0].category_label
keyboard = _build_settings_keyboard(category_key, db_user.language, page)
keyboard = _build_settings_keyboard(
category_key,
group_key,
category_page,
db_user.language,
settings_page,
)
await callback.message.edit_text(
f"🧩 <b>{category_label}</b>\n\nВыберите настройку для просмотра:",
reply_markup=keyboard,
@@ -253,9 +431,19 @@ async def show_bot_config_setting(
db_user: User,
db: AsyncSession,
):
key = callback.data.split(":", 1)[1]
parts = callback.data.split(":", 4)
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
try:
category_page = max(1, int(parts[2])) if len(parts) > 2 else 1
except ValueError:
category_page = 1
try:
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
except ValueError:
settings_page = 1
key = parts[4] if len(parts) > 4 else ""
text = _render_setting_text(key)
keyboard = _build_setting_keyboard(key)
keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page)
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
@@ -268,7 +456,17 @@ async def start_edit_setting(
db: AsyncSession,
state: FSMContext,
):
key = callback.data.split(":", 1)[1]
parts = callback.data.split(":", 4)
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
try:
category_page = max(1, int(parts[2])) if len(parts) > 2 else 1
except ValueError:
category_page = 1
try:
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
except ValueError:
settings_page = 1
key = parts[4] if len(parts) > 4 else ""
definition = bot_configuration_service.get_definition(key)
summary = bot_configuration_service.get_setting_summary(key)
@@ -276,6 +474,7 @@ async def start_edit_setting(
instructions = [
"✏️ <b>Редактирование настройки</b>",
f"Название: {summary['name']}",
f"Ключ: <code>{summary['key']}</code>",
f"Тип: {summary['type']}",
f"Текущее значение: {summary['current']}",
@@ -293,14 +492,22 @@ async def start_edit_setting(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.BACK, callback_data=f"botcfg_setting:{key}"
text=texts.BACK,
callback_data=(
f"botcfg_setting:{group_key}:{category_page}:{settings_page}:{key}"
),
)
]
]
),
)
await state.update_data(setting_key=key)
await state.update_data(
setting_key=key,
setting_group_key=group_key,
setting_category_page=category_page,
setting_settings_page=settings_page,
)
await state.set_state(BotConfigStates.waiting_for_value)
await callback.answer()
@@ -315,6 +522,9 @@ async def handle_edit_setting(
):
data = await state.get_data()
key = data.get("setting_key")
group_key = data.get("setting_group_key", CATEGORY_FALLBACK_KEY)
category_page = data.get("setting_category_page", 1)
settings_page = data.get("setting_settings_page", 1)
if not key:
await message.answer("Не удалось определить редактируемую настройку. Попробуйте снова.")
@@ -331,7 +541,7 @@ async def handle_edit_setting(
await db.commit()
text = _render_setting_text(key)
keyboard = _build_setting_keyboard(key)
keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page)
await message.answer("✅ Настройка обновлена")
await message.answer(text, reply_markup=keyboard)
await state.clear()
@@ -344,12 +554,22 @@ async def reset_setting(
db_user: User,
db: AsyncSession,
):
key = callback.data.split(":", 1)[1]
parts = callback.data.split(":", 4)
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
try:
category_page = max(1, int(parts[2])) if len(parts) > 2 else 1
except ValueError:
category_page = 1
try:
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
except ValueError:
settings_page = 1
key = parts[4] if len(parts) > 4 else ""
await bot_configuration_service.reset_value(db, key)
await db.commit()
text = _render_setting_text(key)
keyboard = _build_setting_keyboard(key)
keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page)
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer("Сброшено к значению по умолчанию")
@@ -361,14 +581,24 @@ async def toggle_setting(
db_user: User,
db: AsyncSession,
):
key = callback.data.split(":", 1)[1]
parts = callback.data.split(":", 4)
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
try:
category_page = max(1, int(parts[2])) if len(parts) > 2 else 1
except ValueError:
category_page = 1
try:
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
except ValueError:
settings_page = 1
key = parts[4] if len(parts) > 4 else ""
current = bot_configuration_service.get_current_value(key)
new_value = not bool(current)
await bot_configuration_service.set_value(db, key, new_value)
await db.commit()
text = _render_setting_text(key)
keyboard = _build_setting_keyboard(key)
keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page)
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer("Обновлено")
@@ -379,9 +609,8 @@ def register_handlers(dp: Dispatcher) -> None:
F.data == "admin_bot_config",
)
dp.callback_query.register(
show_bot_config_categories_page,
F.data.startswith("botcfg_categories:")
& (~F.data.endswith(":noop")),
show_bot_config_group,
F.data.startswith("botcfg_group:") & (~F.data.endswith(":noop")),
)
dp.callback_query.register(
show_bot_config_category,