From fd41d22322dca8e2f475cef806d3e507bb4b54cd Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 15 Oct 2025 02:36:11 +0300 Subject: [PATCH] refactor: regroup config categories --- app/handlers/admin/bot_configuration.py | 183 ++++++++++++++++++------ app/services/system_settings_service.py | 7 + 2 files changed, 146 insertions(+), 44 deletions(-) diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index a6531fab..d381e23b 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -42,13 +42,22 @@ CATEGORY_GROUP_METADATA: Dict[str, Dict[str, object]] = { "title": "💬 Поддержка", "description": "Контакты, режимы тикетов, SLA и уведомления модераторов.", "icon": "💬", - "categories": ("SUPPORT",), + "categories": ("SUPPORT", "MODERATION"), }, "payments": { "title": "💳 Платежные системы", "description": "YooKassa, CryptoBot, MulenPay, PAL24, Tribute и Telegram Stars.", "icon": "💳", - "categories": ("PAYMENT", "YOOKASSA", "CRYPTOBOT", "MULENPAY", "PAL24", "TRIBUTE", "TELEGRAM"), + "categories": ( + "PAYMENT", + "YOOKASSA", + "CRYPTOBOT", + "MULENPAY", + "PAL24", + "TRIBUTE", + "TELEGRAM", + "WATA", + ), }, "subscriptions": { "title": "📅 Подписки и цены", @@ -78,7 +87,17 @@ CATEGORY_GROUP_METADATA: Dict[str, Dict[str, object]] = { "title": "🎨 Интерфейс и брендинг", "description": "Логотип, тексты, языки, miniapp и deep links.", "icon": "🎨", - "categories": ("INTERFACE_BRANDING", "INTERFACE_SUBSCRIPTION", "CONNECT_BUTTON", "MINIAPP", "HAPP", "SKIP", "LOCALIZATION", "ADDITIONAL"), + "categories": ( + "INTERFACE", + "INTERFACE_BRANDING", + "INTERFACE_SUBSCRIPTION", + "CONNECT_BUTTON", + "MINIAPP", + "HAPP", + "SKIP", + "LOCALIZATION", + "ADDITIONAL", + ), }, "database": { "title": "💾 База данных", @@ -120,11 +139,11 @@ CATEGORY_GROUP_METADATA: Dict[str, Dict[str, object]] = { CATEGORY_GROUP_ORDER: Tuple[str, ...] = ( "core", - "support", "payments", "subscriptions", "trial", "referral", + "support", "notifications", "interface", "database", @@ -212,6 +231,21 @@ def _get_group_description(group_key: str) -> str: return str(meta.get("description", "")) +def _format_days(count: int) -> str: + remainder = count % 100 + if 11 <= remainder <= 14: + suffix = "дней" + else: + last_digit = count % 10 + if last_digit == 1: + suffix = "день" + elif last_digit in {2, 3, 4}: + suffix = "дня" + else: + suffix = "дней" + return f"{count} {suffix}" + + def _get_group_icon(group_key: str) -> str: meta = _get_group_meta(group_key) return str(meta.get("icon", "⚙️")) @@ -226,6 +260,7 @@ def _get_group_status(group_key: str) -> Tuple[str, str]: "MulenPay": settings.is_mulenpay_enabled(), "PAL24": settings.is_pal24_enabled(), "Tribute": settings.TRIBUTE_ENABLED, + "Wata": getattr(settings, "WATA_ENABLED", False), "Stars": settings.TELEGRAM_STARS_ENABLED, } active = sum(1 for value in payment_statuses.values() if value) @@ -244,7 +279,7 @@ def _get_group_status(group_key: str) -> Tuple[str, str]: or (settings.REMNAWAVE_USERNAME and settings.REMNAWAVE_PASSWORD) ) ) - return ("🟢", "API подключено") if api_ready else ("🟡", "Нужно указать URL и ключи") + return ("🟢", "Подключено") if api_ready else ("🟡", "Нужно указать URL и ключи") if key == "server": mode = (settings.SERVER_STATUS_MODE or "").lower() @@ -264,14 +299,14 @@ def _get_group_status(group_key: str) -> Tuple[str, str]: user_on = settings.is_notifications_enabled() admin_on = settings.is_admin_notifications_enabled() if user_on and admin_on: - return "🟢", "Все уведомления включены" + return "🟢", "Все включены" if user_on or admin_on: return "🟡", "Часть уведомлений включена" return "⚪", "Уведомления отключены" if key == "trial": if settings.TRIAL_DURATION_DAYS > 0: - return "🟢", f"{settings.TRIAL_DURATION_DAYS} дней пробного периода" + return "🟢", f"{_format_days(settings.TRIAL_DURATION_DAYS)}" return "⚪", "Триал отключен" if key == "referral": @@ -281,7 +316,14 @@ def _get_group_status(group_key: str) -> Tuple[str, str]: or settings.REFERRAL_INVITER_BONUS_KOPEKS or settings.get_referred_user_reward_kopeks() ) - return ("🟢", "Программа активна") if active else ("⚪", "Бонусы не заданы") + return ("🟢", "Активна") if active else ("⚪", "Бонусы не заданы") + + if key == "support": + if not settings.SUPPORT_MENU_ENABLED: + return "⚪", "Меню отключено" + if settings.SUPPORT_USERNAME and settings.SUPPORT_USERNAME != "@support": + return "🟢", "Команда готова" + return "🟡", "Требует настройки" if key == "core": token_ok = bool(getattr(settings, "BOT_TOKEN", "")) @@ -306,7 +348,12 @@ def _get_group_status(group_key: str) -> Tuple[str, str]: branding = bool(settings.ENABLE_LOGO_MODE or settings.MINIAPP_CUSTOM_URL) return ("🟢", "Брендинг настроен") if branding else ("⚪", "Настройки по умолчанию") - return "🟢", "Готово к работе" + if key == "external_admin": + if getattr(settings, "WEB_API_DEFAULT_TOKEN", ""): + return "🟢", "Настроено" + return "⚪", "Требуется токен" + + return "🟢", "Готово" def _get_setting_icon(definition, current_value: object) -> str: @@ -353,22 +400,27 @@ def _render_dashboard_overview() -> str: ) lines: List[str] = [ - "⚙️ Панель управления ботом", + "⚙️ ПАНЕЛЬ УПРАВЛЕНИЯ БОТОМ", "", f"Всего параметров: {total_settings} • Переопределено: {total_overrides}", "", - "Выберите категорию ниже или используйте быстрые действия:", + "Группы настроек:", "", ] for group_key, title, items in grouped: status_icon, status_text = _get_group_status(group_key) - description = _get_group_description(group_key) if group_key != CATEGORY_FALLBACK_KEY else "Настройки без категории." + description = ( + _get_group_description(group_key) + if group_key != CATEGORY_FALLBACK_KEY + else "Настройки без категории." + ) total = sum(count for _, _, count in items) - lines.append(f"{status_icon} {title} — {status_text}") + icon = _get_group_icon(group_key) + lines.append(f"{status_icon} {icon} {title} — {status_text}") if description: lines.append(f" {description}") - lines.append(f" Настроек: {total}") + lines.append(f"└ Настроек: {total}") lines.append("") lines.append("🔍 Кнопка поиска поможет найти параметр по названию, описанию или ключу.") @@ -461,7 +513,15 @@ def _build_search_results_keyboard(results: List[Dict[str, object]]) -> types.In rows.append( [ types.InlineKeyboardButton( - text="⬅️ В главное меню", + text="⬅️ Попробовать снова", + callback_data="botcfg_action:search", + ) + ] + ) + rows.append( + [ + types.InlineKeyboardButton( + text="🏠 Главное меню", callback_data="admin_bot_config", ) ] @@ -505,7 +565,7 @@ async def start_settings_search( await callback.message.edit_text( "🔍 Поиск по настройкам\n\n" - "Отправьте часть ключа или названия настройки. \n" + "Отправьте часть ключа или названия.\n" "Например: yookassa или уведомления.", reply_markup=keyboard, parse_mode="HTML", @@ -542,6 +602,8 @@ async def handle_search_query( lines.append( f"{index}. {item['name']} — {item['value']} ({item['category_label']})" ) + lines.append("") + lines.append("Нажмите на нужную настройку, чтобы открыть её.") text = "\n".join(lines) else: keyboard = types.InlineKeyboardMarkup( @@ -580,7 +642,7 @@ async def show_presets( lines = [ "🎯 Готовые пресеты", "", - "Выберите набор параметров, чтобы быстро применить его к боту.", + "Выберите набор параметров:", "", ] for key, meta in PRESET_METADATA.items(): @@ -714,12 +776,14 @@ async def apply_preset( title = PRESET_METADATA.get(preset_key, {}).get("title", preset_key) summary_lines = [ - f"✅ Пресет {title} применен", + f'✅ Пресет "{title}" применен', "", f"Изменено параметров: {len(applied)}", ] if applied: - summary_lines.append("\n".join(f"• {key}" for key in applied)) + summary_lines.extend( + ["", "\n".join(f"• {key}" for key in applied)] + ) keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ @@ -806,7 +870,7 @@ async def start_import_settings( await callback.message.edit_text( "📥 Импорт настроек\n\n" - "Прикрепите .env файл или отправьте текстом пары KEY=value.\n" + "Прикрепите .env файл или отправьте текстом пары KEY=value.\n\n" "Неизвестные параметры будут проигнорированы.", parse_mode="HTML", reply_markup=keyboard, @@ -886,15 +950,30 @@ async def handle_import_message( f"Обновлено параметров: {len(applied)}", ] if applied: - summary_lines.append("\n".join(f"• {key}" for key in applied)) + summary_lines.extend( + [ + "", + "\n".join(f"• {key}" for key in applied), + ] + ) if skipped: - summary_lines.append("\nПропущено (неизвестные ключи):") - summary_lines.append("\n".join(f"• {key}" for key in skipped)) + summary_lines.extend( + [ + "", + "Пропущено (неизвестные ключи):", + "\n".join(f"• {key}" for key in skipped), + ] + ) if errors: - summary_lines.append("\nОшибки разбора:") - summary_lines.append("\n".join(f"• {html.escape(err)}" for err in errors)) + summary_lines.extend( + [ + "", + "Ошибки разбора:", + "\n".join(f"• {html.escape(err)}" for err in errors), + ] + ) keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ @@ -937,7 +1016,9 @@ async def show_settings_history( ) except Exception: formatted_value = row.value or "—" - lines.append(f"{ts_text} • {row.key} = {formatted_value}") + lines.append( + f"{ts_text} • {row.key} = {html.escape(str(formatted_value))}" + ) else: lines.append("История изменений пуста.") @@ -1109,8 +1190,12 @@ def _build_groups_keyboard() -> types.InlineKeyboardMarkup: for group_key, title, items in grouped: total = sum(count for _, _, count in items) - status_icon, _ = _get_group_status(group_key) - button_text = f"{status_icon} {title} ({total})" + status_icon, status_text = _get_group_status(group_key) + icon = _get_group_icon(group_key) + button_text = ( + f"{status_icon} {icon} {title} — {status_text}\n" + f"└ Настроек: {total}" + ) rows.append( [ types.InlineKeyboardButton( @@ -1162,7 +1247,7 @@ def _build_groups_keyboard() -> types.InlineKeyboardMarkup: rows.append( [ types.InlineKeyboardButton( - text="⬅️ Назад", + text="⬅️ Назад в админку", callback_data="admin_submenu_settings", ) ] @@ -1193,7 +1278,10 @@ def _build_categories_keyboard( rows.append( [ types.InlineKeyboardButton( - text=f"{status_icon} {group_title}", + text=( + f"{status_icon} {_get_group_icon(group_key)} {group_title}\n" + f"└ Статус: {_status_text}" + ), callback_data="botcfg_group:noop", ) ] @@ -1206,7 +1294,7 @@ def _build_categories_keyboard( if bot_configuration_service.has_override(definition.key): overrides += 1 badge = "✳️" if overrides else "•" - button_text = f"{badge} {label} ({count})" + button_text = f"{badge} • {label} ({count})" buttons.append( types.InlineKeyboardButton( text=button_text, @@ -1228,7 +1316,7 @@ def _build_categories_keyboard( ) nav_row.append( types.InlineKeyboardButton( - text=f"{page}/{total_pages}", + text=f"[{page}/{total_pages}]", callback_data="botcfg_group:noop", ) ) @@ -1353,7 +1441,7 @@ def _build_settings_keyboard( ) nav_row.append( types.InlineKeyboardButton( - text=f"{page}/{total_pages}", callback_data="botcfg_cat_page:noop" + text=f"[{page}/{total_pages}]", callback_data="botcfg_cat_page:noop" ) ) if page < total_pages: @@ -1464,6 +1552,11 @@ def _build_setting_keyboard( def _render_setting_text(key: str) -> str: summary = bot_configuration_service.get_setting_summary(key) guidance = bot_configuration_service.get_setting_guidance(key) + description = guidance.get("description") or "—" + format_hint = guidance.get("format") or "—" + example = guidance.get("example") or "—" + warning = guidance.get("warning") or "—" + dependencies = guidance.get("dependencies") or "—" lines = [ f"🧩 {summary['name']}", @@ -1479,11 +1572,11 @@ def _render_setting_text(key: str) -> str: else [] ), "", - f"📘 Описание: {guidance['description']}", - f"📐 Формат: {guidance['format']}", - f"💡 Пример: {guidance['example']}", - f"⚠️ Важно: {guidance['warning']}", - f"🔗 Связанные настройки: {guidance['dependencies']}", + f"📘 Описание: {description}", + f"📐 Формат: {format_hint}", + f"💡 Пример: {example}", + f"⚠️ Важно: {warning}", + f"🔗 Связанные настройки: {dependencies}", ] choices = bot_configuration_service.get_choice_options(key) @@ -1543,11 +1636,13 @@ async def show_bot_config_group( keyboard = _build_categories_keyboard(group_key, group_title, items, page) status_icon, status_text = _get_group_status(group_key) description = _get_group_description(group_key) - lines = [f"{status_icon} {group_title}"] + icon = _get_group_icon(group_key) + lines = [f"{icon} {group_title}"] + lines.append(f"Хлебные крошки: 🏠 → {group_title}") + if status_text: + lines.append(f"Статус: {status_icon} {status_text}") if description: lines.append(description) - if status_text: - lines.append(f"Статус: {status_text}") lines.append("") lines.append("📂 Выберите категорию настроек:") await callback.message.edit_text( @@ -1587,12 +1682,12 @@ async def show_bot_config_category( ) text_lines = [ f"🗂 {category_label}", - f"Навигация: 🏠 Главное → {group_title} → {category_label}", + f"Хлебные крошки: 🏠 → {group_title} → {category_label}", ] if category_description: text_lines.append(category_description) text_lines.append("") - text_lines.append("📋 Выберите настройку для просмотра или редактирования:") + text_lines.append("📋 Список настроек категории:") await callback.message.edit_text( "\n".join(text_lines), reply_markup=keyboard, @@ -2071,7 +2166,7 @@ async def show_bot_config_setting( return text = _render_setting_text(key) keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page) - await callback.message.edit_text(text, reply_markup=keyboard) + await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") await _store_setting_context( state, key=key, diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index 02cf22ee..341aec6b 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -78,6 +78,7 @@ class BotConfigurationService: "TRIBUTE": "🎁 Tribute", "MULENPAY": "💰 MulenPay", "PAL24": "🏦 PAL24 / PayPalych", + "WATA": "💠 Wata", "EXTERNAL_ADMIN": "🛡️ Внешняя админка", "SUBSCRIPTIONS_CORE": "📅 Подписки и лимиты", "PERIODS": "📆 Периоды подписок", @@ -127,6 +128,7 @@ class BotConfigurationService: "PAL24": "PAL24 / PayPalych подключения и лимиты.", "TRIBUTE": "Tribute и донат-сервисы.", "TELEGRAM": "Telegram Stars и их стоимость.", + "WATA": "Wata: токены доступа, редиректы и лимиты платежей.", "EXTERNAL_ADMIN": "Токен внешней админки для проверки запросов.", "SUBSCRIPTIONS_CORE": "Лимиты устройств, трафика и базовые цены подписок.", "PERIODS": "Доступные периоды подписок и продлений.", @@ -165,6 +167,8 @@ class BotConfigurationService: } CATEGORY_KEY_OVERRIDES: Dict[str, str] = { + "BOT_TOKEN": "CORE", + "BOT_USERNAME": "CORE", "DATABASE_URL": "DATABASE", "DATABASE_MODE": "DATABASE", "LOCALES_PATH": "LOCALIZATION", @@ -198,6 +202,7 @@ class BotConfigurationService: "DEFAULT_AUTOPAY_ENABLED": "AUTOPAY", "DEFAULT_AUTOPAY_DAYS_BEFORE": "AUTOPAY", "MIN_BALANCE_FOR_AUTOPAY_KOPEKS": "AUTOPAY", + "DISABLE_TOPUP_BUTTONS": "PAYMENT", "TRIAL_WARNING_HOURS": "TRIAL", "SUPPORT_USERNAME": "SUPPORT", "SUPPORT_MENU_ENABLED": "SUPPORT", @@ -247,6 +252,7 @@ class BotConfigurationService: } CATEGORY_PREFIX_OVERRIDES: Dict[str, str] = { + "BOT_": "CORE", "SUPPORT_": "SUPPORT", "ADMIN_NOTIFICATIONS": "ADMIN_NOTIFICATIONS", "ADMIN_REPORTS": "ADMIN_REPORTS", @@ -267,6 +273,7 @@ class BotConfigurationService: "CRYPTOBOT_": "CRYPTOBOT", "MULENPAY_": "MULENPAY", "PAL24_": "PAL24", + "WATA_": "WATA", "PAYMENT_": "PAYMENT", "EXTERNAL_ADMIN_": "EXTERNAL_ADMIN", "CONNECT_BUTTON_HAPP": "HAPP",