mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-22 12:21:26 +00:00
feat(monitoring): добавить настройки мониторинга трафика в админку
- Добавлена кнопка "⚙️ Настройки трафика" в меню мониторинга
- Добавлен UI для управления быстрой и суточной проверками трафика
- Можно включать/выключать проверки, менять пороги и интервалы
- Настройки сохраняются в БД через BotConfigurationService
- Добавлены SETTING_HINTS с описаниями параметров
This commit is contained in:
@@ -1794,5 +1794,318 @@ async def process_notification_value_input(message: Message, state: FSMContext):
|
||||
await state.clear()
|
||||
|
||||
|
||||
# ============== Настройки мониторинга трафика ==============
|
||||
|
||||
def _format_traffic_toggle(enabled: bool) -> str:
|
||||
return "🟢 Вкл" if enabled else "🔴 Выкл"
|
||||
|
||||
|
||||
def _build_traffic_settings_keyboard() -> InlineKeyboardMarkup:
|
||||
"""Строит клавиатуру настроек мониторинга трафика."""
|
||||
fast_enabled = settings.TRAFFIC_FAST_CHECK_ENABLED
|
||||
daily_enabled = settings.TRAFFIC_DAILY_CHECK_ENABLED
|
||||
|
||||
fast_interval = settings.TRAFFIC_FAST_CHECK_INTERVAL_MINUTES
|
||||
fast_threshold = settings.TRAFFIC_FAST_CHECK_THRESHOLD_GB
|
||||
daily_time = settings.TRAFFIC_DAILY_CHECK_TIME
|
||||
daily_threshold = settings.TRAFFIC_DAILY_THRESHOLD_GB
|
||||
cooldown = settings.TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text=f"{_format_traffic_toggle(fast_enabled)} Быстрая проверка",
|
||||
callback_data="admin_traffic_toggle_fast"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"⏱ Интервал: {fast_interval} мин",
|
||||
callback_data="admin_traffic_edit_fast_interval"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"📊 Порог дельты: {fast_threshold} ГБ",
|
||||
callback_data="admin_traffic_edit_fast_threshold"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"{_format_traffic_toggle(daily_enabled)} Суточная проверка",
|
||||
callback_data="admin_traffic_toggle_daily"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"🕐 Время проверки: {daily_time}",
|
||||
callback_data="admin_traffic_edit_daily_time"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"📈 Суточный порог: {daily_threshold} ГБ",
|
||||
callback_data="admin_traffic_edit_daily_threshold"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"⏳ Кулдаун: {cooldown} мин",
|
||||
callback_data="admin_traffic_edit_cooldown"
|
||||
)],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_monitoring")],
|
||||
])
|
||||
|
||||
|
||||
def _build_traffic_settings_text() -> str:
|
||||
"""Строит текст настроек мониторинга трафика."""
|
||||
fast_enabled = settings.TRAFFIC_FAST_CHECK_ENABLED
|
||||
daily_enabled = settings.TRAFFIC_DAILY_CHECK_ENABLED
|
||||
|
||||
fast_status = _format_traffic_toggle(fast_enabled)
|
||||
daily_status = _format_traffic_toggle(daily_enabled)
|
||||
|
||||
text = (
|
||||
"⚙️ <b>Настройки мониторинга трафика</b>\n\n"
|
||||
f"<b>Быстрая проверка:</b> {fast_status}\n"
|
||||
f"• Интервал: {settings.TRAFFIC_FAST_CHECK_INTERVAL_MINUTES} мин\n"
|
||||
f"• Порог дельты: {settings.TRAFFIC_FAST_CHECK_THRESHOLD_GB} ГБ\n\n"
|
||||
f"<b>Суточная проверка:</b> {daily_status}\n"
|
||||
f"• Время: {settings.TRAFFIC_DAILY_CHECK_TIME} UTC\n"
|
||||
f"• Порог: {settings.TRAFFIC_DAILY_THRESHOLD_GB} ГБ\n\n"
|
||||
f"<b>Общие:</b>\n"
|
||||
f"• Кулдаун уведомлений: {settings.TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES} мин\n"
|
||||
)
|
||||
|
||||
# Информация о фильтрах
|
||||
monitored_nodes = settings.get_traffic_monitored_nodes()
|
||||
ignored_nodes = settings.get_traffic_ignored_nodes()
|
||||
excluded_uuids = settings.get_traffic_excluded_user_uuids()
|
||||
|
||||
if monitored_nodes:
|
||||
text += f"• Мониторим только: {len(monitored_nodes)} нод(ы)\n"
|
||||
if ignored_nodes:
|
||||
text += f"• Игнорируем: {len(ignored_nodes)} нод(ы)\n"
|
||||
if excluded_uuids:
|
||||
text += f"• Исключено юзеров: {len(excluded_uuids)}\n"
|
||||
|
||||
return text
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_mon_traffic_settings")
|
||||
@admin_required
|
||||
async def admin_traffic_settings(callback: CallbackQuery):
|
||||
"""Показывает настройки мониторинга трафика."""
|
||||
try:
|
||||
text = _build_traffic_settings_text()
|
||||
keyboard = _build_traffic_settings_keyboard()
|
||||
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отображения настроек трафика: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки настроек", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_traffic_toggle_fast")
|
||||
@admin_required
|
||||
async def toggle_fast_check(callback: CallbackQuery):
|
||||
"""Переключает быструю проверку трафика."""
|
||||
try:
|
||||
from app.services.system_settings_service import BotConfigurationService
|
||||
|
||||
current = settings.TRAFFIC_FAST_CHECK_ENABLED
|
||||
new_value = not current
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await BotConfigurationService.set_value(db, "TRAFFIC_FAST_CHECK_ENABLED", new_value)
|
||||
await db.commit()
|
||||
|
||||
await callback.answer("✅ Включено" if new_value else "⏸️ Отключено")
|
||||
|
||||
# Обновляем отображение
|
||||
text = _build_traffic_settings_text()
|
||||
keyboard = _build_traffic_settings_keyboard()
|
||||
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка переключения быстрой проверки: {e}")
|
||||
await callback.answer("❌ Ошибка", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_traffic_toggle_daily")
|
||||
@admin_required
|
||||
async def toggle_daily_check(callback: CallbackQuery):
|
||||
"""Переключает суточную проверку трафика."""
|
||||
try:
|
||||
from app.services.system_settings_service import BotConfigurationService
|
||||
|
||||
current = settings.TRAFFIC_DAILY_CHECK_ENABLED
|
||||
new_value = not current
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await BotConfigurationService.set_value(db, "TRAFFIC_DAILY_CHECK_ENABLED", new_value)
|
||||
await db.commit()
|
||||
|
||||
await callback.answer("✅ Включено" if new_value else "⏸️ Отключено")
|
||||
|
||||
text = _build_traffic_settings_text()
|
||||
keyboard = _build_traffic_settings_keyboard()
|
||||
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка переключения суточной проверки: {e}")
|
||||
await callback.answer("❌ Ошибка", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_traffic_edit_fast_interval")
|
||||
@admin_required
|
||||
async def edit_fast_interval(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начинает редактирование интервала быстрой проверки."""
|
||||
await state.set_state(AdminStates.editing_traffic_setting)
|
||||
await state.update_data(
|
||||
traffic_setting_key="TRAFFIC_FAST_CHECK_INTERVAL_MINUTES",
|
||||
traffic_setting_type="int",
|
||||
settings_message_chat=callback.message.chat.id,
|
||||
settings_message_id=callback.message.message_id,
|
||||
)
|
||||
await callback.answer()
|
||||
await callback.message.answer(
|
||||
"⏱ Введите интервал быстрой проверки в минутах (минимум 1):"
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_traffic_edit_fast_threshold")
|
||||
@admin_required
|
||||
async def edit_fast_threshold(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начинает редактирование порога быстрой проверки."""
|
||||
await state.set_state(AdminStates.editing_traffic_setting)
|
||||
await state.update_data(
|
||||
traffic_setting_key="TRAFFIC_FAST_CHECK_THRESHOLD_GB",
|
||||
traffic_setting_type="float",
|
||||
settings_message_chat=callback.message.chat.id,
|
||||
settings_message_id=callback.message.message_id,
|
||||
)
|
||||
await callback.answer()
|
||||
await callback.message.answer(
|
||||
"📊 Введите порог дельты трафика в ГБ (например: 5.0):"
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_traffic_edit_daily_time")
|
||||
@admin_required
|
||||
async def edit_daily_time(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начинает редактирование времени суточной проверки."""
|
||||
await state.set_state(AdminStates.editing_traffic_setting)
|
||||
await state.update_data(
|
||||
traffic_setting_key="TRAFFIC_DAILY_CHECK_TIME",
|
||||
traffic_setting_type="time",
|
||||
settings_message_chat=callback.message.chat.id,
|
||||
settings_message_id=callback.message.message_id,
|
||||
)
|
||||
await callback.answer()
|
||||
await callback.message.answer(
|
||||
"🕐 Введите время суточной проверки в формате HH:MM (UTC):\n"
|
||||
"Например: 00:00, 03:00, 12:30"
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_traffic_edit_daily_threshold")
|
||||
@admin_required
|
||||
async def edit_daily_threshold(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начинает редактирование суточного порога."""
|
||||
await state.set_state(AdminStates.editing_traffic_setting)
|
||||
await state.update_data(
|
||||
traffic_setting_key="TRAFFIC_DAILY_THRESHOLD_GB",
|
||||
traffic_setting_type="float",
|
||||
settings_message_chat=callback.message.chat.id,
|
||||
settings_message_id=callback.message.message_id,
|
||||
)
|
||||
await callback.answer()
|
||||
await callback.message.answer(
|
||||
"📈 Введите суточный порог трафика в ГБ (например: 50.0):"
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_traffic_edit_cooldown")
|
||||
@admin_required
|
||||
async def edit_cooldown(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начинает редактирование кулдауна уведомлений."""
|
||||
await state.set_state(AdminStates.editing_traffic_setting)
|
||||
await state.update_data(
|
||||
traffic_setting_key="TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES",
|
||||
traffic_setting_type="int",
|
||||
settings_message_chat=callback.message.chat.id,
|
||||
settings_message_id=callback.message.message_id,
|
||||
)
|
||||
await callback.answer()
|
||||
await callback.message.answer(
|
||||
"⏳ Введите кулдаун уведомлений в минутах (минимум 1):"
|
||||
)
|
||||
|
||||
|
||||
@router.message(AdminStates.editing_traffic_setting)
|
||||
async def process_traffic_setting_input(message: Message, state: FSMContext):
|
||||
"""Обрабатывает ввод настройки мониторинга трафика."""
|
||||
from app.services.system_settings_service import BotConfigurationService
|
||||
|
||||
data = await state.get_data()
|
||||
if not data:
|
||||
await state.clear()
|
||||
await message.answer("ℹ️ Контекст утерян, попробуйте снова из меню настроек.")
|
||||
return
|
||||
|
||||
raw_value = (message.text or "").strip()
|
||||
setting_key = data.get("traffic_setting_key")
|
||||
setting_type = data.get("traffic_setting_type")
|
||||
|
||||
# Валидация и парсинг значения
|
||||
try:
|
||||
if setting_type == "int":
|
||||
value = int(raw_value)
|
||||
if value < 1:
|
||||
raise ValueError("Значение должно быть >= 1")
|
||||
elif setting_type == "float":
|
||||
value = float(raw_value.replace(",", "."))
|
||||
if value <= 0:
|
||||
raise ValueError("Значение должно быть > 0")
|
||||
elif setting_type == "time":
|
||||
# Валидация формата HH:MM
|
||||
import re
|
||||
if not re.match(r"^\d{1,2}:\d{2}$", raw_value):
|
||||
raise ValueError("Неверный формат времени. Используйте HH:MM")
|
||||
parts = raw_value.split(":")
|
||||
hours, minutes = int(parts[0]), int(parts[1])
|
||||
if hours < 0 or hours > 23 or minutes < 0 or minutes > 59:
|
||||
raise ValueError("Неверное время")
|
||||
value = f"{hours:02d}:{minutes:02d}"
|
||||
else:
|
||||
value = raw_value
|
||||
except ValueError as e:
|
||||
await message.answer(f"❌ {str(e)}")
|
||||
return
|
||||
|
||||
# Сохраняем значение
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
await BotConfigurationService.set_value(db, setting_key, value)
|
||||
await db.commit()
|
||||
|
||||
back_keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="⬅️ К настройкам трафика", callback_data="admin_mon_traffic_settings")]
|
||||
]
|
||||
)
|
||||
await message.answer("✅ Настройка сохранена!", reply_markup=back_keyboard)
|
||||
|
||||
# Обновляем исходное сообщение с настройками
|
||||
chat_id = data.get("settings_message_chat")
|
||||
message_id = data.get("settings_message_id")
|
||||
if chat_id and message_id:
|
||||
try:
|
||||
text = _build_traffic_settings_text()
|
||||
keyboard = _build_traffic_settings_keyboard()
|
||||
await message.bot.edit_message_text(
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
except Exception:
|
||||
pass # Игнорируем если сообщение уже удалено
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сохранения настройки трафика: {e}")
|
||||
await message.answer(f"❌ Ошибка сохранения: {str(e)}")
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
def register_handlers(dp):
|
||||
dp.include_router(router)
|
||||
|
||||
@@ -1779,6 +1779,10 @@ def get_monitoring_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_MONITORING_TEST_NOTIFICATIONS", "🧪 Тест уведомлений"),
|
||||
callback_data="admin_mon_test_notifications"
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="⚙️ Настройки трафика",
|
||||
callback_data="admin_mon_traffic_settings"
|
||||
)
|
||||
],
|
||||
[
|
||||
|
||||
@@ -276,6 +276,18 @@ class BotConfigurationService:
|
||||
"TRAFFIC_MONITORING_INTERVAL_HOURS": "MONITORING",
|
||||
"TRAFFIC_MONITORED_NODES": "MONITORING",
|
||||
"TRAFFIC_SNAPSHOT_TTL_HOURS": "MONITORING",
|
||||
"TRAFFIC_FAST_CHECK_ENABLED": "MONITORING",
|
||||
"TRAFFIC_FAST_CHECK_INTERVAL_MINUTES": "MONITORING",
|
||||
"TRAFFIC_FAST_CHECK_THRESHOLD_GB": "MONITORING",
|
||||
"TRAFFIC_DAILY_CHECK_ENABLED": "MONITORING",
|
||||
"TRAFFIC_DAILY_CHECK_TIME": "MONITORING",
|
||||
"TRAFFIC_DAILY_THRESHOLD_GB": "MONITORING",
|
||||
"TRAFFIC_IGNORED_NODES": "MONITORING",
|
||||
"TRAFFIC_EXCLUDED_USER_UUIDS": "MONITORING",
|
||||
"TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES": "MONITORING",
|
||||
"SUSPICIOUS_NOTIFICATIONS_TOPIC_ID": "MONITORING",
|
||||
"TRAFFIC_CHECK_BATCH_SIZE": "MONITORING",
|
||||
"TRAFFIC_CHECK_CONCURRENCY": "MONITORING",
|
||||
"ENABLE_LOGO_MODE": "INTERFACE_BRANDING",
|
||||
"LOGO_FILE": "INTERFACE_BRANDING",
|
||||
"HIDE_SUBSCRIPTION_LINK": "INTERFACE_SUBSCRIPTION",
|
||||
@@ -780,6 +792,57 @@ class BotConfigurationService:
|
||||
),
|
||||
"dependencies": "TRAFFIC_MONITORING_ENABLED, Redis",
|
||||
},
|
||||
"TRAFFIC_FAST_CHECK_ENABLED": {
|
||||
"description": (
|
||||
"Включает быструю проверку трафика. "
|
||||
"Система сравнивает текущий трафик со snapshot и уведомляет о превышениях дельты."
|
||||
),
|
||||
"format": "Булево значение.",
|
||||
"example": "true",
|
||||
"warning": "Требует Redis для хранения snapshot. При отключении проверки не выполняются.",
|
||||
"dependencies": "Redis, TRAFFIC_FAST_CHECK_INTERVAL_MINUTES, TRAFFIC_FAST_CHECK_THRESHOLD_GB",
|
||||
},
|
||||
"TRAFFIC_FAST_CHECK_INTERVAL_MINUTES": {
|
||||
"description": "Интервал быстрой проверки трафика в минутах.",
|
||||
"format": "Целое число минут (минимум 1).",
|
||||
"example": "10",
|
||||
"warning": "Слишком малый интервал создаёт нагрузку на Remnawave API.",
|
||||
"dependencies": "TRAFFIC_FAST_CHECK_ENABLED",
|
||||
},
|
||||
"TRAFFIC_FAST_CHECK_THRESHOLD_GB": {
|
||||
"description": "Порог дельты трафика в ГБ для быстрой проверки. При превышении отправляется уведомление.",
|
||||
"format": "Число с плавающей точкой.",
|
||||
"example": "5.0",
|
||||
"warning": "Слишком низкий порог приведёт к частым уведомлениям.",
|
||||
"dependencies": "TRAFFIC_FAST_CHECK_ENABLED",
|
||||
},
|
||||
"TRAFFIC_DAILY_CHECK_ENABLED": {
|
||||
"description": "Включает суточную проверку трафика через bandwidth-stats API.",
|
||||
"format": "Булево значение.",
|
||||
"example": "true",
|
||||
"warning": "Проверка выполняется в указанное время (TRAFFIC_DAILY_CHECK_TIME).",
|
||||
"dependencies": "TRAFFIC_DAILY_CHECK_TIME, TRAFFIC_DAILY_THRESHOLD_GB",
|
||||
},
|
||||
"TRAFFIC_DAILY_CHECK_TIME": {
|
||||
"description": "Время суточной проверки трафика в формате HH:MM (UTC).",
|
||||
"format": "Строка времени HH:MM.",
|
||||
"example": "00:00",
|
||||
"warning": "Время указывается в UTC.",
|
||||
"dependencies": "TRAFFIC_DAILY_CHECK_ENABLED",
|
||||
},
|
||||
"TRAFFIC_DAILY_THRESHOLD_GB": {
|
||||
"description": "Порог суточного трафика в ГБ. При превышении за 24 часа отправляется уведомление.",
|
||||
"format": "Число с плавающей точкой.",
|
||||
"example": "50.0",
|
||||
"warning": "Учитывается весь трафик за последние 24 часа.",
|
||||
"dependencies": "TRAFFIC_DAILY_CHECK_ENABLED",
|
||||
},
|
||||
"TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES": {
|
||||
"description": "Кулдаун уведомлений по одному пользователю в минутах.",
|
||||
"format": "Целое число минут.",
|
||||
"example": "60",
|
||||
"warning": "Защита от спама уведомлениями по одному и тому же пользователю.",
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -134,6 +134,7 @@ class AdminStates(StatesGroup):
|
||||
editing_faq_title = State()
|
||||
editing_faq_content = State()
|
||||
editing_notification_value = State()
|
||||
editing_traffic_setting = State()
|
||||
|
||||
confirming_sync = State()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user