Merge pull request #708 from Fr1ngg/bedolaga-hw3yna

feat: enhance admin settings control panel
This commit is contained in:
Egor
2025-10-04 04:49:29 +03:00
committed by GitHub
3 changed files with 1370 additions and 152 deletions
File diff suppressed because it is too large Load Diff
+282 -67
View File
@@ -60,92 +60,158 @@ class BotConfigurationService:
EXCLUDED_KEYS: set[str] = {"BOT_TOKEN", "ADMIN_IDS"}
CATEGORY_TITLES: Dict[str, str] = {
"SUPPORT": "💬 Ссылка на поддержку",
"LOCALIZATION": "🌍 Языковые настройки",
"MAINTENANCE": "🛠️ Режим обслуживания",
"CORE": "🤖 Основные настройки",
"SUPPORT": "💬 Поддержка и тикеты",
"LOCALIZATION": "🌍 Языки интерфейса",
"CHANNEL": "📣 Обязательная подписка",
"ADMIN_NOTIFICATIONS": "🔔 Уведомления админам",
"ADMIN_REPORTS": "📊 Автоматические отчеты",
"TRIAL": "🎁 Триальная подписка",
"PAID_SUBSCRIPTION": "💰 Платные подписки",
"PERIODS": "📅 Периоды подписки",
"SUBSCRIPTION_PRICES": "💵 Цены за периоды",
"TRAFFIC": "🚦 Настройки трафика",
"TRAFFIC_PACKAGES": "📦 Пакеты трафика",
"DISCOUNTS": "🎯 Промо и скидки",
"PAYMENT": "⚙️ Общие настройки платежей",
"PAYMENT": "💳 Общие платежные настройки",
"TELEGRAM": "⭐ Telegram Stars",
"CRYPTOBOT": "💎 CryptoBot",
"YOOKASSA": "💸 YooKassa",
"CRYPTOBOT": "🪙 CryptoBot",
"YOOKASSA": "🟣 YooKassa",
"TRIBUTE": "🎁 Tribute",
"MULENPAY": "💰 MulenPay",
"PAL24": "🏦 Pal24/PayPalych",
"REMNAWAVE": "🔗 RemnaWave API",
"REFERRAL": "🤝 Реферальная система",
"PAL24": "🏦 PAL24 / PayPalych",
"SUBSCRIPTIONS_CORE": "📅 Подписки и лимиты",
"PERIODS": "📆 Периоды подписок",
"SUBSCRIPTION_PRICES": "💵 Стоимость тарифов",
"TRAFFIC": "📊 Трафик",
"TRAFFIC_PACKAGES": "📦 Пакеты трафика",
"TRIAL": "🎁 Пробный период",
"REFERRAL": "👥 Реферальная программа",
"AUTOPAY": "🔄 Автопродление",
"INTERFACE_BRANDING": "🖼️ Визуальные настройки",
"INTERFACE_SUBSCRIPTION": "🔗 Скрыть ссылку подписки",
"CONNECT_BUTTON": "🚀 Кнопка «Подключиться»",
"HAPP": "🅷 Happ настройки",
"SKIP": "⚡ Быстрый старт",
"ADDITIONAL": "📱 Приложения и DeepLinks",
"MINIAPP": "📱 Mini App",
"DATABASE": "🗄️ Режим БД",
"POSTGRES": "🐘 PostgreSQL",
"SQLITE": "💾 SQLite",
"REDIS": "🧠 Redis",
"MONITORING": "📈 Общий мониторинг",
"NOTIFICATIONS": "🔔 Уведомления пользователям",
"SERVER": "🖥️ Статус серверов",
"BACKUP": "💾 Система бэкапов",
"VERSION": "🔄 Обновления",
"LOG": "📝 Логирование",
"ADMIN_NOTIFICATIONS": "📣 Оповещения администраторам",
"ADMIN_REPORTS": "🗂 Автоматические отчеты",
"INTERFACE": "🎨 Интерфейс и брендинг",
"INTERFACE_BRANDING": "🖼️ Брендинг",
"INTERFACE_SUBSCRIPTION": "🔗 Ссылка на подписку",
"CONNECT_BUTTON": "🚀 Кнопка подключения",
"MINIAPP": "📱 Mini App",
"HAPP": "🅷 Happ",
"SKIP": "⚡ Быстрый старт",
"ADDITIONAL": "📱 Дополнительные приложения",
"DATABASE": "💾 База данных",
"POSTGRES": "🐘 PostgreSQL",
"SQLITE": "🧱 SQLite",
"REDIS": "🧠 Redis",
"REMNAWAVE": "🌐 RemnaWave API",
"SERVER_STATUS": "📊 Статус серверов",
"MONITORING": "📈 Мониторинг",
"MAINTENANCE": "🔧 Обслуживание",
"BACKUP": "💾 Резервные копии",
"VERSION": "🔄 Проверка версий",
"WEB_API": "⚡ Web API",
"WEBHOOK": "🌐 Webhook",
"WEB_API": "🌐 Web API",
"DEBUG": "🔧 Режим разработки",
"LOG": "📝 Логирование",
"DEBUG": "🧪 Режим разработки",
}
CATEGORY_DESCRIPTIONS: Dict[str, str] = {
"CORE": "Базовые параметры работы бота и обязательные ссылки.",
"SUPPORT": "Контакты поддержки, SLA и режимы обработки обращений.",
"LOCALIZATION": "Доступные языки, локализация интерфейса и выбор языка.",
"CHANNEL": "Настройки обязательной подписки на канал или группу.",
"PAYMENT": "Общие тексты платежей, описания чеков и шаблоны.",
"YOOKASSA": "Интеграция с YooKassa: идентификаторы магазина и вебхуки.",
"CRYPTOBOT": "CryptoBot и криптоплатежи через Telegram.",
"MULENPAY": "Платежи MulenPay и параметры магазина.",
"PAL24": "PAL24 / PayPalych подключения и лимиты.",
"TRIBUTE": "Tribute и донат-сервисы.",
"TELEGRAM": "Telegram Stars и их стоимость.",
"SUBSCRIPTIONS_CORE": "Лимиты устройств, трафика и базовые цены подписок.",
"PERIODS": "Доступные периоды подписок и продлений.",
"SUBSCRIPTION_PRICES": "Стоимость подписок по периодам в копейках.",
"TRAFFIC": "Лимиты трафика и стратегии сброса.",
"TRAFFIC_PACKAGES": "Цены пакетов трафика и конфигурация предложений.",
"TRIAL": "Длительность и ограничения пробного периода.",
"REFERRAL": "Бонусы и пороги реферальной программы.",
"AUTOPAY": "Настройки автопродления и минимальный баланс.",
"NOTIFICATIONS": "Пользовательские уведомления и кэширование сообщений.",
"ADMIN_NOTIFICATIONS": "Оповещения админам о событиях и тикетах.",
"ADMIN_REPORTS": "Автоматические отчеты для команды.",
"INTERFACE": "Глобальные параметры интерфейса и брендирования.",
"INTERFACE_BRANDING": "Логотип и фирменный стиль.",
"INTERFACE_SUBSCRIPTION": "Отображение ссылок и кнопок подписок.",
"CONNECT_BUTTON": "Поведение кнопки «Подключиться» и miniapp.",
"MINIAPP": "Mini App и кастомные ссылки.",
"HAPP": "Интеграция Happ и связанные ссылки.",
"SKIP": "Настройки быстрого старта и гайд по подключению.",
"ADDITIONAL": "Конфигурация app-config.json, deep links и кеша.",
"DATABASE": "Режим работы базы данных и пути до файлов.",
"POSTGRES": "Параметры подключения к PostgreSQL.",
"SQLITE": "Файл SQLite и резервные параметры.",
"REDIS": "Подключение к Redis для кэша.",
"REMNAWAVE": "Параметры авторизации и интеграция с RemnaWave API.",
"SERVER_STATUS": "Отображение статуса серверов и external URL.",
"MONITORING": "Интервалы мониторинга и хранение логов.",
"MAINTENANCE": "Режим обслуживания, сообщения и интервалы.",
"BACKUP": "Резервное копирование и расписание.",
"VERSION": "Отслеживание обновлений репозитория.",
"WEB_API": "Web API, токены и права доступа.",
"WEBHOOK": "Пути и секреты вебхуков.",
"LOG": "Уровни логирования и ротация.",
"DEBUG": "Отладочные функции и безопасный режим.",
}
CATEGORY_KEY_OVERRIDES: Dict[str, str] = {
"DATABASE_URL": "DATABASE",
"DATABASE_MODE": "DATABASE",
"LOCALES_PATH": "DATABASE",
"DEFAULT_DEVICE_LIMIT": "PAID_SUBSCRIPTION",
"DEFAULT_TRAFFIC_LIMIT_GB": "PAID_SUBSCRIPTION",
"MAX_DEVICES_LIMIT": "PAID_SUBSCRIPTION",
"PRICE_PER_DEVICE": "PAID_SUBSCRIPTION",
"LOCALES_PATH": "LOCALIZATION",
"CHANNEL_SUB_ID": "CHANNEL",
"CHANNEL_LINK": "CHANNEL",
"CHANNEL_IS_REQUIRED_SUB": "CHANNEL",
"DEFAULT_LANGUAGE": "LOCALIZATION",
"AVAILABLE_LANGUAGES": "LOCALIZATION",
"LANGUAGE_SELECTION_ENABLED": "LOCALIZATION",
"DEFAULT_DEVICE_LIMIT": "SUBSCRIPTIONS_CORE",
"DEFAULT_TRAFFIC_LIMIT_GB": "SUBSCRIPTIONS_CORE",
"MAX_DEVICES_LIMIT": "SUBSCRIPTIONS_CORE",
"PRICE_PER_DEVICE": "SUBSCRIPTIONS_CORE",
"BASE_SUBSCRIPTION_PRICE": "SUBSCRIPTIONS_CORE",
"DEFAULT_TRAFFIC_RESET_STRATEGY": "TRAFFIC",
"RESET_TRAFFIC_ON_PAYMENT": "TRAFFIC",
"TRAFFIC_SELECTION_MODE": "TRAFFIC",
"FIXED_TRAFFIC_LIMIT_GB": "TRAFFIC",
"AVAILABLE_SUBSCRIPTION_PERIODS": "PERIODS",
"AVAILABLE_RENEWAL_PERIODS": "PERIODS",
"BASE_SUBSCRIPTION_PRICE": "SUBSCRIPTION_PRICES",
"PRICE_14_DAYS": "SUBSCRIPTION_PRICES",
"PRICE_30_DAYS": "SUBSCRIPTION_PRICES",
"PRICE_60_DAYS": "SUBSCRIPTION_PRICES",
"PRICE_90_DAYS": "SUBSCRIPTION_PRICES",
"PRICE_180_DAYS": "SUBSCRIPTION_PRICES",
"PRICE_360_DAYS": "SUBSCRIPTION_PRICES",
"PRICE_TRAFFIC_5GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_10GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_25GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_50GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_100GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_250GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_500GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_1000GB": "TRAFFIC_PACKAGES",
"PRICE_TRAFFIC_UNLIMITED": "TRAFFIC_PACKAGES",
"TRAFFIC_PACKAGES_CONFIG": "TRAFFIC_PACKAGES",
"BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED": "DISCOUNTS",
"BASE_PROMO_GROUP_PERIOD_DISCOUNTS": "DISCOUNTS",
"BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED": "SUBSCRIPTIONS_CORE",
"BASE_PROMO_GROUP_PERIOD_DISCOUNTS": "SUBSCRIPTIONS_CORE",
"REFERRED_USER_REWARD": "REFERRAL",
"DEFAULT_AUTOPAY_DAYS_BEFORE": "AUTOPAY",
"MIN_BALANCE_FOR_AUTOPAY_KOPEKS": "AUTOPAY",
"TRIAL_WARNING_HOURS": "NOTIFICATIONS",
"TRIAL_WARNING_HOURS": "TRIAL",
"SUPPORT_USERNAME": "SUPPORT",
"SUPPORT_MENU_ENABLED": "SUPPORT",
"SUPPORT_SYSTEM_MODE": "SUPPORT",
"SUPPORT_TICKET_SLA_ENABLED": "SUPPORT",
"SUPPORT_TICKET_SLA_MINUTES": "SUPPORT",
"SUPPORT_TICKET_SLA_CHECK_INTERVAL_SECONDS": "SUPPORT",
"SUPPORT_TICKET_SLA_REMINDER_COOLDOWN_MINUTES": "SUPPORT",
"ADMIN_NOTIFICATIONS_ENABLED": "ADMIN_NOTIFICATIONS",
"ADMIN_NOTIFICATIONS_CHAT_ID": "ADMIN_NOTIFICATIONS",
"ADMIN_NOTIFICATIONS_TOPIC_ID": "ADMIN_NOTIFICATIONS",
"ADMIN_NOTIFICATIONS_TICKET_TOPIC_ID": "ADMIN_NOTIFICATIONS",
"ADMIN_REPORTS_ENABLED": "ADMIN_REPORTS",
"ADMIN_REPORTS_CHAT_ID": "ADMIN_REPORTS",
"ADMIN_REPORTS_TOPIC_ID": "ADMIN_REPORTS",
"ADMIN_REPORTS_SEND_TIME": "ADMIN_REPORTS",
"PAYMENT_SERVICE_NAME": "PAYMENT",
"PAYMENT_BALANCE_DESCRIPTION": "PAYMENT",
"PAYMENT_SUBSCRIPTION_DESCRIPTION": "PAYMENT",
"PAYMENT_BALANCE_TEMPLATE": "PAYMENT",
"PAYMENT_SUBSCRIPTION_TEMPLATE": "PAYMENT",
"ENABLE_NOTIFICATIONS": "NOTIFICATIONS",
"NOTIFICATION_RETRY_ATTEMPTS": "NOTIFICATIONS",
"NOTIFICATION_CACHE_HOURS": "NOTIFICATIONS",
"MONITORING_LOGS_RETENTION_DAYS": "MONITORING",
"MONITORING_INTERVAL": "MONITORING",
"ENABLE_LOGO_MODE": "INTERFACE_BRANDING",
"LOGO_FILE": "INTERFACE_BRANDING",
"HIDE_SUBSCRIPTION_LINK": "INTERFACE_SUBSCRIPTION",
@@ -154,15 +220,17 @@ class BotConfigurationService:
"APP_CONFIG_PATH": "ADDITIONAL",
"ENABLE_DEEP_LINKS": "ADDITIONAL",
"APP_CONFIG_CACHE_TTL": "ADDITIONAL",
"DEFAULT_LANGUAGE": "LOCALIZATION",
"AVAILABLE_LANGUAGES": "LOCALIZATION",
"PAYMENT_SERVICE_NAME": "PAYMENT",
"PAYMENT_BALANCE_DESCRIPTION": "PAYMENT",
"PAYMENT_SUBSCRIPTION_DESCRIPTION": "PAYMENT",
"PAYMENT_BALANCE_TEMPLATE": "PAYMENT",
"PAYMENT_SUBSCRIPTION_TEMPLATE": "PAYMENT",
"INACTIVE_USER_DELETE_MONTHS": "MONITORING",
"LANGUAGE_SELECTION_ENABLED": "LOCALIZATION",
"INACTIVE_USER_DELETE_MONTHS": "MAINTENANCE",
"MAINTENANCE_MESSAGE": "MAINTENANCE",
"MAINTENANCE_CHECK_INTERVAL": "MAINTENANCE",
"MAINTENANCE_AUTO_ENABLE": "MAINTENANCE",
"WEBHOOK_URL": "WEBHOOK",
"WEBHOOK_SECRET": "WEBHOOK",
"VERSION_CHECK_ENABLED": "VERSION",
"VERSION_CHECK_REPO": "VERSION",
"VERSION_CHECK_INTERVAL_HOURS": "VERSION",
"TELEGRAM_STARS_RATE_RUB": "TELEGRAM",
"REMNAWAVE_USER_DESCRIPTION_TEMPLATE": "REMNAWAVE",
}
CATEGORY_PREFIX_OVERRIDES: Dict[str, str] = {
@@ -193,7 +261,7 @@ class BotConfigurationService:
"MINIAPP_": "MINIAPP",
"MONITORING_": "MONITORING",
"NOTIFICATION_": "NOTIFICATIONS",
"SERVER_STATUS": "SERVER",
"SERVER_STATUS": "SERVER_STATUS",
"MAINTENANCE_": "MAINTENANCE",
"VERSION_CHECK": "VERSION",
"BACKUP_": "BACKUP",
@@ -290,6 +358,153 @@ class BotConfigurationService:
],
}
SETTING_HINTS: Dict[str, Dict[str, str]] = {
"YOOKASSA_ENABLED": {
"description": "Включает оплату через YooKassa. Требует корректных идентификаторов магазина и секретного ключа.",
"format": "Булево значение: выберите \"Включить\" или \"Выключить\".",
"example": "Включено при полностью настроенной интеграции.",
"warning": "При включении без Shop ID и Secret Key пользователи увидят ошибки при оплате.",
"dependencies": "YOOKASSA_SHOP_ID, YOOKASSA_SECRET_KEY, YOOKASSA_RETURN_URL",
},
"CRYPTOBOT_ENABLED": {
"description": "Разрешает принимать криптоплатежи через CryptoBot.",
"format": "Булево значение.",
"example": "Включите после указания токена API и секрета вебхука.",
"warning": "Пустой токен или неверный вебхук приведут к отказам платежей.",
"dependencies": "CRYPTOBOT_API_TOKEN, CRYPTOBOT_WEBHOOK_SECRET",
},
"SUPPORT_TICKET_SLA_MINUTES": {
"description": "Лимит времени для ответа модераторов на тикет в минутах.",
"format": "Целое число от 1 до 1440.",
"example": "5",
"warning": "Слишком низкое значение может вызвать частые напоминания, слишком высокое — ухудшить SLA.",
"dependencies": "SUPPORT_TICKET_SLA_ENABLED, SUPPORT_TICKET_SLA_REMINDER_COOLDOWN_MINUTES",
},
"MAINTENANCE_MODE": {
"description": "Переводит бота в режим технического обслуживания и скрывает действия для пользователей.",
"format": "Булево значение.",
"example": "Включено на время плановых работ.",
"warning": "Не забудьте отключить после завершения работ, иначе бот останется недоступен.",
"dependencies": "MAINTENANCE_MESSAGE, MAINTENANCE_CHECK_INTERVAL",
},
"REMNAWAVE_API_URL": {
"description": "Базовый адрес панели RemnaWave, с которой синхронизируется бот.",
"format": "Полный URL вида https://panel.example.com.",
"example": "https://panel.remnawave.net",
"warning": "Недоступный адрес приведет к ошибкам при управлении VPN-учетками.",
"dependencies": "REMNAWAVE_API_KEY или REMNAWAVE_USERNAME/REMNAWAVE_PASSWORD",
},
}
@classmethod
def get_category_description(cls, category_key: str) -> str:
return cls.CATEGORY_DESCRIPTIONS.get(category_key, "")
@classmethod
def is_toggle(cls, key: str) -> bool:
definition = cls.get_definition(key)
return definition.python_type is bool
@classmethod
def _format_numeric_with_unit(cls, key: str, value: Union[int, float]) -> Optional[str]:
if isinstance(value, bool):
return None
upper_key = key.upper()
if any(suffix in upper_key for suffix in ("PRICE", "_KOPEKS", "AMOUNT")):
try:
return settings.format_price(int(value))
except Exception:
return f"{value}"
if upper_key.endswith("_PERCENT") or "PERCENT" in upper_key:
return f"{value}%"
if upper_key.endswith("_HOURS"):
return f"{value} ч"
if upper_key.endswith("_MINUTES"):
return f"{value} мин"
if upper_key.endswith("_SECONDS"):
return f"{value} сек"
if upper_key.endswith("_DAYS"):
return f"{value} дн"
if upper_key.endswith("_GB"):
return f"{value} ГБ"
if upper_key.endswith("_MB"):
return f"{value} МБ"
return None
@classmethod
def _split_comma_values(cls, text: str) -> Optional[List[str]]:
raw = (text or "").strip()
if not raw or "," not in raw:
return None
parts = [segment.strip() for segment in raw.split(",") if segment.strip()]
return parts or None
@classmethod
def format_value_human(cls, key: str, value: Any) -> str:
if value is None:
return ""
if isinstance(value, bool):
return "✅ ВКЛЮЧЕНО" if value else "❌ ВЫКЛЮЧЕНО"
if isinstance(value, (int, float)):
formatted = cls._format_numeric_with_unit(key, value)
return formatted or str(value)
if isinstance(value, str):
cleaned = value.strip()
if not cleaned:
return ""
if any(keyword in key.upper() for keyword in ("TOKEN", "SECRET", "PASSWORD", "KEY")):
return "••••••••"
items = cls._split_comma_values(cleaned)
if items:
return ", ".join(items)
return cleaned
if isinstance(value, (list, tuple, set)):
return ", ".join(str(item) for item in value)
if isinstance(value, dict):
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return str(value)
return str(value)
@classmethod
def get_setting_guidance(cls, key: str) -> Dict[str, str]:
definition = cls.get_definition(key)
original = cls.get_original_value(key)
type_label = definition.type_label
hints = dict(cls.SETTING_HINTS.get(key, {}))
base_description = (
hints.get("description")
or f"Параметр <b>{definition.display_name}</b> управляет категорией «{definition.category_label}»."
)
base_format = hints.get("format") or (
"Булево значение (да/нет)." if definition.python_type is bool
else "Введите значение соответствующего типа (число или строку)."
)
example = hints.get("example") or (
cls.format_value_human(key, original) if original is not None else ""
)
warning = hints.get("warning") or (
"Неверные значения могут привести к некорректной работе бота."
)
dependencies = hints.get("dependencies") or definition.category_label
return {
"description": base_description,
"format": base_format,
"example": example,
"warning": warning,
"dependencies": dependencies,
"type": type_label,
}
_definitions: Dict[str, SettingDefinition] = {}
_original_values: Dict[str, Any] = settings.model_dump()
_overrides_raw: Dict[str, Optional[str]] = {}
@@ -443,7 +658,7 @@ class BotConfigurationService:
@classmethod
def format_value_for_list(cls, key: str) -> str:
value = cls.get_current_value(key)
formatted = cls.format_value(value)
formatted = cls.format_value_human(key, value)
if formatted == "":
return formatted
return _truncate(formatted)
@@ -693,8 +908,8 @@ class BotConfigurationService:
return {
"key": key,
"name": definition.display_name,
"current": cls.format_value(current),
"original": cls.format_value(original),
"current": cls.format_value_human(key, current),
"original": cls.format_value_human(key, original),
"type": definition.type_label,
"category_key": definition.category_key,
"category_label": definition.category_label,
+2
View File
@@ -132,6 +132,8 @@ class SupportSettingsStates(StatesGroup):
class BotConfigStates(StatesGroup):
waiting_for_value = State()
waiting_for_search_query = State()
waiting_for_import_file = State()
class AutoPayStates(StatesGroup):
setting_autopay_days = State()