diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index b9518499..fe410d8b 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -3190,12 +3190,27 @@ async def handle_toggle_daily_subscription_pause( "DAILY_SUBSCRIPTION_RESUMED", "▶️ Подписка возобновлена!" ) + # Синхронизируем с Remnawave - активируем пользователя + try: + from app.services.subscription_service import SubscriptionService + subscription_service = SubscriptionService() + await subscription_service.create_remnawave_user( + db, + subscription, + reset_traffic=False, + reset_reason=None, + ) + logger.info(f"✅ Синхронизировано с Remnawave после возобновления суточной подписки {subscription.id}") + except Exception as e: + logger.error(f"Ошибка синхронизации с Remnawave при возобновлении: {e}") else: # Была активна, теперь на паузе message = texts.t( "DAILY_SUBSCRIPTION_PAUSED", "⏸️ Подписка приостановлена!" ) + # При паузе можно отключить пользователя в Remnawave (опционально) + # Пока оставляем активным, т.к. пауза - это только остановка списания await callback.answer(message, show_alert=True) diff --git a/app/handlers/subscription/tariff_purchase.py b/app/handlers/subscription/tariff_purchase.py index 266821ab..dfbf8352 100644 --- a/app/handlers/subscription/tariff_purchase.py +++ b/app/handlers/subscription/tariff_purchase.py @@ -318,6 +318,151 @@ def get_daily_tariff_insufficient_balance_keyboard( ]) +# ==================== Кастомные дни/трафик ==================== + + +def get_custom_tariff_keyboard( + tariff_id: int, + language: str, + days: int, + traffic_gb: int, + can_custom_days: bool, + can_custom_traffic: bool, + min_days: int = 1, + max_days: int = 365, + min_traffic: int = 1, + max_traffic: int = 1000, +) -> InlineKeyboardMarkup: + """Создает клавиатуру для настройки кастомных дней и трафика.""" + texts = get_texts(language) + buttons = [] + + # Кнопки изменения дней + if can_custom_days: + days_row = [] + # -30 / -7 / -1 + if days > min_days: + if days - 30 >= min_days: + days_row.append(InlineKeyboardButton(text="-30", callback_data=f"custom_days:{tariff_id}:-30")) + if days - 7 >= min_days: + days_row.append(InlineKeyboardButton(text="-7", callback_data=f"custom_days:{tariff_id}:-7")) + days_row.append(InlineKeyboardButton(text="-1", callback_data=f"custom_days:{tariff_id}:-1")) + + # Текущее значение + days_row.append(InlineKeyboardButton(text=f"📅 {days} дн.", callback_data="noop")) + + # +1 / +7 / +30 + if days < max_days: + days_row.append(InlineKeyboardButton(text="+1", callback_data=f"custom_days:{tariff_id}:1")) + if days + 7 <= max_days: + days_row.append(InlineKeyboardButton(text="+7", callback_data=f"custom_days:{tariff_id}:7")) + if days + 30 <= max_days: + days_row.append(InlineKeyboardButton(text="+30", callback_data=f"custom_days:{tariff_id}:30")) + + if days_row: + buttons.append(days_row) + + # Кнопки изменения трафика + if can_custom_traffic: + traffic_row = [] + # -100 / -10 / -1 + if traffic_gb > min_traffic: + if traffic_gb - 100 >= min_traffic: + traffic_row.append(InlineKeyboardButton(text="-100", callback_data=f"custom_traffic:{tariff_id}:-100")) + if traffic_gb - 10 >= min_traffic: + traffic_row.append(InlineKeyboardButton(text="-10", callback_data=f"custom_traffic:{tariff_id}:-10")) + traffic_row.append(InlineKeyboardButton(text="-1", callback_data=f"custom_traffic:{tariff_id}:-1")) + + # Текущее значение + traffic_row.append(InlineKeyboardButton(text=f"📊 {traffic_gb} ГБ", callback_data="noop")) + + # +1 / +10 / +100 + if traffic_gb < max_traffic: + traffic_row.append(InlineKeyboardButton(text="+1", callback_data=f"custom_traffic:{tariff_id}:1")) + if traffic_gb + 10 <= max_traffic: + traffic_row.append(InlineKeyboardButton(text="+10", callback_data=f"custom_traffic:{tariff_id}:10")) + if traffic_gb + 100 <= max_traffic: + traffic_row.append(InlineKeyboardButton(text="+100", callback_data=f"custom_traffic:{tariff_id}:100")) + + if traffic_row: + buttons.append(traffic_row) + + # Кнопка подтверждения + buttons.append([ + InlineKeyboardButton( + text="✅ Подтвердить покупку", + callback_data=f"custom_confirm:{tariff_id}" + ) + ]) + + # Кнопка назад + buttons.append([ + InlineKeyboardButton(text=texts.BACK, callback_data="tariff_list") + ]) + + return InlineKeyboardMarkup(inline_keyboard=buttons) + + +def format_custom_tariff_preview( + tariff: Tariff, + days: int, + traffic_gb: int, + user_balance: int, + discount_percent: int = 0, +) -> str: + """Форматирует предпросмотр покупки с кастомными параметрами.""" + # Рассчитываем цену + days_price = 0 + traffic_price = 0 + + if tariff.can_purchase_custom_days(): + days_price = tariff.get_price_for_custom_days(days) or 0 + + if tariff.can_purchase_custom_traffic(): + traffic_price = tariff.get_price_for_custom_traffic(traffic_gb) or 0 + + total_price = days_price + traffic_price + + # Применяем скидку + if discount_percent > 0: + total_price = _apply_promo_discount(total_price, discount_percent) + + traffic_display = f"{traffic_gb} ГБ" if traffic_gb > 0 else _format_traffic(tariff.traffic_limit_gb) + + text = f"""📦 {tariff.name} + +Настройте параметры: +""" + + if tariff.can_purchase_custom_days(): + text += f"📅 Дней: {days} (от {tariff.min_days} до {tariff.max_days})\n" + text += f" 💰 {_format_price_kopeks(days_price)}\n" + + if tariff.can_purchase_custom_traffic(): + text += f"📊 Трафик: {traffic_gb} ГБ (от {tariff.min_traffic_gb} до {tariff.max_traffic_gb})\n" + text += f" 💰 {_format_price_kopeks(traffic_price)}\n" + else: + text += f"📊 Трафик: {traffic_display}\n" + + text += f"📱 Устройств: {tariff.device_limit}\n" + + if discount_percent > 0: + text += f"\n🎁 Скидка: {discount_percent}%\n" + + text += f""" +💰 Итого: {_format_price_kopeks(total_price)} + +💳 Ваш баланс: {_format_price_kopeks(user_balance)}""" + + if user_balance < total_price: + missing = total_price - user_balance + text += f"\n⚠️ Не хватает: {_format_price_kopeks(missing)}" + else: + text += f"\nПосле оплаты: {_format_price_kopeks(user_balance - total_price)}" + + return text + + @error_handler async def show_tariffs_list( callback: types.CallbackQuery, @@ -416,17 +561,317 @@ async def select_tariff( parse_mode="HTML" ) else: - # Для обычного тарифа показываем выбор периода - await callback.message.edit_text( - format_tariff_info_for_user(tariff, db_user.language), - reply_markup=get_tariff_periods_keyboard(tariff, db_user.language, db_user=db_user), - parse_mode="HTML" - ) + # Проверяем, есть ли кастомные дни или трафик + can_custom_days = tariff.can_purchase_custom_days() + can_custom_traffic = tariff.can_purchase_custom_traffic() + + if can_custom_days or can_custom_traffic: + # Показываем экран настройки кастомных параметров + user_balance = db_user.balance_kopeks or 0 + + # Начальные значения - минимальные + initial_days = tariff.min_days if can_custom_days else 30 + initial_traffic = tariff.min_traffic_gb if can_custom_traffic else tariff.traffic_limit_gb + + # Сохраняем в состояние + await state.update_data( + selected_tariff_id=tariff_id, + custom_days=initial_days, + custom_traffic_gb=initial_traffic, + ) + + preview_text = format_custom_tariff_preview( + tariff=tariff, + days=initial_days, + traffic_gb=initial_traffic, + user_balance=user_balance, + ) + + await callback.message.edit_text( + preview_text, + reply_markup=get_custom_tariff_keyboard( + tariff_id=tariff_id, + language=db_user.language, + days=initial_days, + traffic_gb=initial_traffic, + can_custom_days=can_custom_days, + can_custom_traffic=can_custom_traffic, + min_days=tariff.min_days, + max_days=tariff.max_days, + min_traffic=tariff.min_traffic_gb, + max_traffic=tariff.max_traffic_gb, + ), + parse_mode="HTML" + ) + else: + # Для обычного тарифа показываем выбор периода + await callback.message.edit_text( + format_tariff_info_for_user(tariff, db_user.language), + reply_markup=get_tariff_periods_keyboard(tariff, db_user.language, db_user=db_user), + parse_mode="HTML" + ) await state.update_data(selected_tariff_id=tariff_id) await callback.answer() +@error_handler +async def handle_custom_days_change( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + """Обрабатывает изменение количества дней.""" + parts = callback.data.split(":") + tariff_id = int(parts[1]) + delta = int(parts[2]) + + tariff = await get_tariff_by_id(db, tariff_id) + if not tariff or not tariff.is_active: + await callback.answer("Тариф недоступен", show_alert=True) + return + + state_data = await state.get_data() + current_days = state_data.get('custom_days', tariff.min_days) + current_traffic = state_data.get('custom_traffic_gb', tariff.min_traffic_gb) + + # Применяем изменение + new_days = current_days + delta + new_days = max(tariff.min_days, min(tariff.max_days, new_days)) + + await state.update_data(custom_days=new_days) + + user_balance = db_user.balance_kopeks or 0 + + preview_text = format_custom_tariff_preview( + tariff=tariff, + days=new_days, + traffic_gb=current_traffic, + user_balance=user_balance, + ) + + await callback.message.edit_text( + preview_text, + reply_markup=get_custom_tariff_keyboard( + tariff_id=tariff_id, + language=db_user.language, + days=new_days, + traffic_gb=current_traffic, + can_custom_days=tariff.can_purchase_custom_days(), + can_custom_traffic=tariff.can_purchase_custom_traffic(), + min_days=tariff.min_days, + max_days=tariff.max_days, + min_traffic=tariff.min_traffic_gb, + max_traffic=tariff.max_traffic_gb, + ), + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def handle_custom_traffic_change( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + """Обрабатывает изменение количества трафика.""" + parts = callback.data.split(":") + tariff_id = int(parts[1]) + delta = int(parts[2]) + + tariff = await get_tariff_by_id(db, tariff_id) + if not tariff or not tariff.is_active: + await callback.answer("Тариф недоступен", show_alert=True) + return + + state_data = await state.get_data() + current_days = state_data.get('custom_days', tariff.min_days) + current_traffic = state_data.get('custom_traffic_gb', tariff.min_traffic_gb) + + # Применяем изменение + new_traffic = current_traffic + delta + new_traffic = max(tariff.min_traffic_gb, min(tariff.max_traffic_gb, new_traffic)) + + await state.update_data(custom_traffic_gb=new_traffic) + + user_balance = db_user.balance_kopeks or 0 + + preview_text = format_custom_tariff_preview( + tariff=tariff, + days=current_days, + traffic_gb=new_traffic, + user_balance=user_balance, + ) + + await callback.message.edit_text( + preview_text, + reply_markup=get_custom_tariff_keyboard( + tariff_id=tariff_id, + language=db_user.language, + days=current_days, + traffic_gb=new_traffic, + can_custom_days=tariff.can_purchase_custom_days(), + can_custom_traffic=tariff.can_purchase_custom_traffic(), + min_days=tariff.min_days, + max_days=tariff.max_days, + min_traffic=tariff.min_traffic_gb, + max_traffic=tariff.max_traffic_gb, + ), + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def handle_custom_confirm( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + """Подтверждает покупку тарифа с кастомными параметрами.""" + tariff_id = int(callback.data.split(":")[1]) + + tariff = await get_tariff_by_id(db, tariff_id) + if not tariff or not tariff.is_active: + await callback.answer("Тариф недоступен", show_alert=True) + return + + state_data = await state.get_data() + custom_days = state_data.get('custom_days', tariff.min_days) + custom_traffic = state_data.get('custom_traffic_gb', tariff.min_traffic_gb) + + # Рассчитываем цену + days_price = tariff.get_price_for_custom_days(custom_days) or 0 + traffic_price = tariff.get_price_for_custom_traffic(custom_traffic) or 0 + total_price = days_price + traffic_price + + # Проверяем баланс + user_balance = db_user.balance_kopeks or 0 + if user_balance < total_price: + await callback.answer("Недостаточно средств на балансе", show_alert=True) + return + + texts = get_texts(db_user.language) + + try: + # Списываем баланс + success = await subtract_user_balance( + db, db_user, total_price, + f"Покупка тарифа {tariff.name} на {custom_days} дней" + ) + if not success: + await callback.answer("Ошибка списания баланса", show_alert=True) + return + + # Получаем список серверов из тарифа + squads = tariff.allowed_squads or [] + + # Если allowed_squads пустой - значит "все серверы", получаем их + if not squads: + from app.database.crud.server_squad import get_all_server_squads + all_servers, _ = await get_all_server_squads(db, available_only=True) + squads = [s.squad_uuid for s in all_servers if s.squad_uuid] + + # Определяем трафик + traffic_limit = custom_traffic if tariff.can_purchase_custom_traffic() else tariff.traffic_limit_gb + + # Проверяем есть ли уже подписка + existing_subscription = await get_subscription_by_user_id(db, db_user.id) + + if existing_subscription: + # Продлеваем существующую подписку и обновляем параметры тарифа + subscription = await extend_subscription( + db, + existing_subscription, + days=custom_days, + tariff_id=tariff.id, + traffic_limit_gb=traffic_limit, + device_limit=tariff.device_limit, + connected_squads=squads, + ) + else: + # Создаем новую подписку + subscription = await create_paid_subscription( + db=db, + user_id=db_user.id, + duration_days=custom_days, + traffic_limit_gb=traffic_limit, + device_limit=tariff.device_limit, + connected_squads=squads, + tariff_id=tariff.id, + ) + + # Обновляем пользователя в Remnawave + try: + subscription_service = SubscriptionService() + await subscription_service.create_remnawave_user( + db, + subscription, + reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT, + reset_reason="покупка тарифа", + ) + except Exception as e: + logger.error(f"Ошибка обновления Remnawave: {e}") + + # Создаем транзакцию + await create_transaction( + db, + user_id=db_user.id, + type=TransactionType.SUBSCRIPTION_PAYMENT, + amount_kopeks=-total_price, + description=f"Покупка тарифа {tariff.name} на {custom_days} дней", + ) + + # Отправляем уведомление админу + try: + admin_notification_service = AdminNotificationService(callback.bot) + await admin_notification_service.send_subscription_purchase_notification( + db, + db_user, + subscription, + None, + custom_days, + was_trial_conversion=False, + amount_kopeks=total_price, + ) + except Exception as e: + logger.error(f"Ошибка отправки уведомления админу: {e}") + + # Очищаем корзину после успешной покупки + try: + await user_cart_service.delete_user_cart(db_user.id) + except Exception as e: + logger.error(f"Ошибка очистки корзины: {e}") + + await state.clear() + + traffic_display = _format_traffic(traffic_limit) + + await callback.message.edit_text( + f"🎉 Подписка успешно оформлена!\n\n" + f"📦 Тариф: {tariff.name}\n" + f"📊 Трафик: {traffic_display}\n" + f"📱 Устройств: {tariff.device_limit}\n" + f"📅 Период: {_format_period(custom_days)}\n" + f"💰 Списано: {_format_price_kopeks(total_price)}\n\n" + f"Перейдите в раздел «Подписка» для подключения.", + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")], + [InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")] + ]), + parse_mode="HTML" + ) + await callback.answer("Подписка оформлена!", show_alert=True) + + except Exception as e: + logger.error(f"Ошибка при покупке тарифа с кастомными параметрами: {e}", exc_info=True) + await callback.answer("Произошла ошибка при оформлении подписки", show_alert=True) + + @error_handler async def select_tariff_period( callback: types.CallbackQuery, @@ -2460,6 +2905,11 @@ def register_tariff_purchase_handlers(dp: Dispatcher): # Подтверждение покупки суточного тарифа dp.callback_query.register(confirm_daily_tariff_purchase, F.data.startswith("daily_tariff_confirm:")) + # Кастомные дни/трафик + dp.callback_query.register(handle_custom_days_change, F.data.startswith("custom_days:")) + dp.callback_query.register(handle_custom_traffic_change, F.data.startswith("custom_traffic:")) + dp.callback_query.register(handle_custom_confirm, F.data.startswith("custom_confirm:")) + # Продление по тарифу dp.callback_query.register(select_tariff_extend_period, F.data.startswith("tariff_extend:")) dp.callback_query.register(confirm_tariff_extend, F.data.startswith("tariff_ext_confirm:")) diff --git a/app/services/ban_notification_service.py b/app/services/ban_notification_service.py index c3c8f8e9..1e521282 100644 --- a/app/services/ban_notification_service.py +++ b/app/services/ban_notification_service.py @@ -229,6 +229,65 @@ class BanNotificationService: ) return False, f"Ошибка Telegram API: {str(e)}", user.telegram_id + async def send_network_wifi_notification( + self, + db: AsyncSession, + user_identifier: str, + username: str, + ban_minutes: int, + network_type: Optional[str] = None, + node_name: Optional[str] = None + ) -> Tuple[bool, str, Optional[int]]: + """ + Отправить уведомление о блокировке за использование WiFi сети + + Returns: + (success, message, telegram_id) + """ + if not self._bot: + return False, "Бот не инициализирован", None + + # Находим пользователя + user = await self._find_user_by_identifier(db, user_identifier) + if not user: + logger.warning(f"Пользователь {user_identifier} не найден в базе данных") + return False, f"Пользователь не найден: {user_identifier}", None + + # Формируем сообщение + network_info = f"Тип сети: {network_type}\n" if network_type else "" + node_info = f"Сервер: {node_name}\n" if node_name else "" + + message_text = ( + "📶 Блокировка за использование WiFi сети\n\n" + f"Ваш аккаунт временно заблокирован из-за использования WiFi сети.\n\n" + f"{network_info}" + f"{node_info}" + f"⏱ Блокировка на: {ban_minutes} мин\n\n" + f"ℹ️ Использование VPN через WiFi запрещено правилами сервиса.\n" + f"Пожалуйста, используйте мобильную сеть для подключения к VPN.\n\n" + f"После окончания блокировки ваш доступ будет восстановлен автоматически." + ) + + # Отправляем сообщение + try: + await self._bot.send_message( + chat_id=user.telegram_id, + text=message_text, + parse_mode="HTML" + ) + logger.info( + f"Уведомление о WiFi бане отправлено пользователю {username} " + f"(telegram_id: {user.telegram_id})" + ) + return True, "Уведомление отправлено", user.telegram_id + + except TelegramAPIError as e: + logger.error( + f"Ошибка отправки WiFi уведомления пользователю {username} " + f"(telegram_id: {user.telegram_id}): {e}" + ) + return False, f"Ошибка Telegram API: {str(e)}", user.telegram_id + # Глобальный экземпляр сервиса ban_notification_service = BanNotificationService() diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index 50cc3e0f..f5eaef5d 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -1893,7 +1893,60 @@ class RemnaWaveService: except Exception as e: logger.error(f"Ошибка получения статистики трафика для пользователя {telegram_id}: {e}") return None - + + async def get_telegram_id_by_email(self, user_identifier: str) -> Optional[int]: + """ + Получить telegram_id пользователя по email или username из панели RemnaWave. + + Args: + user_identifier: Email или username пользователя + + Returns: + telegram_id если найден, иначе None + """ + if not self.is_configured: + logger.warning("RemnaWave API не настроен для поиска пользователя") + return None + + try: + async with self.get_api_client() as api: + # Сначала пробуем найти по username (часто username == email) + try: + user = await api.get_user_by_username(user_identifier) + if user and user.telegram_id: + logger.info( + f"Найден пользователь по username '{user_identifier}': " + f"telegram_id={user.telegram_id}" + ) + return user.telegram_id + except Exception as e: + logger.debug(f"Пользователь не найден по username '{user_identifier}': {e}") + + # Если не нашли по username, ищем по email среди всех пользователей + try: + all_users_response = await api.get_all_users(start=0, size=10000) + users_list = all_users_response.get('users', []) + + for panel_user in users_list: + panel_email = panel_user.email if hasattr(panel_user, 'email') else None + if panel_email and panel_email.lower() == user_identifier.lower(): + panel_telegram_id = panel_user.telegram_id if hasattr(panel_user, 'telegram_id') else None + if panel_telegram_id: + logger.info( + f"Найден пользователь по email '{user_identifier}': " + f"telegram_id={panel_telegram_id}" + ) + return panel_telegram_id + except Exception as e: + logger.warning(f"Ошибка поиска пользователя по email '{user_identifier}': {e}") + + logger.warning(f"Пользователь с идентификатором '{user_identifier}' не найден в панели") + return None + + except Exception as e: + logger.error(f"Ошибка получения telegram_id для '{user_identifier}': {e}") + return None + async def test_api_connection(self) -> Dict[str, Any]: if not self.is_configured: return { diff --git a/app/states.py b/app/states.py index 7e608051..98b5d4b7 100644 --- a/app/states.py +++ b/app/states.py @@ -12,17 +12,22 @@ class SubscriptionStates(StatesGroup): selecting_countries = State() selecting_devices = State() confirming_purchase = State() - + adding_countries = State() adding_traffic = State() adding_devices = State() extending_subscription = State() confirming_traffic_reset = State() cart_saved_for_topup = State() - + # Состояния для простой подписки waiting_for_simple_subscription_payment_method = State() + # Состояния для кастомных дней/трафика при покупке тарифа + selecting_custom_days = State() + selecting_custom_traffic = State() + confirming_custom_purchase = State() + class BalanceStates(StatesGroup): waiting_for_amount = State() waiting_for_pal24_method = State() diff --git a/app/webapi/routes/ban_notifications.py b/app/webapi/routes/ban_notifications.py index 69cb08f0..69e45eea 100644 --- a/app/webapi/routes/ban_notifications.py +++ b/app/webapi/routes/ban_notifications.py @@ -87,6 +87,22 @@ async def send_ban_notification( warning_message=request.warning_message, ) + elif request.notification_type == "network_wifi": + if request.ban_minutes is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Для типа 'network_wifi' требуется поле: ban_minutes" + ) + + success, message, telegram_id = await ban_notification_service.send_network_wifi_notification( + db=db, + user_identifier=request.user_identifier, + username=request.username, + ban_minutes=request.ban_minutes, + network_type=request.network_type, + node_name=request.node_name, + ) + else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/app/webapi/schemas/ban_notifications.py b/app/webapi/schemas/ban_notifications.py index 1e3800de..5d88253a 100644 --- a/app/webapi/schemas/ban_notifications.py +++ b/app/webapi/schemas/ban_notifications.py @@ -7,8 +7,8 @@ from pydantic import BaseModel, Field class BanNotificationRequest(BaseModel): """Запрос на отправку уведомления о бане пользователю""" - notification_type: Literal["punishment", "enabled", "warning"] = Field( - description="Тип уведомления: punishment (бан), enabled (разбан), warning (предупреждение)" + notification_type: Literal["punishment", "enabled", "warning", "network_wifi"] = Field( + description="Тип уведомления: punishment (бан за устройства), enabled (разбан), warning (предупреждение), network_wifi (бан за WiFi)" ) user_identifier: str = Field( description="Email или user_id пользователя из Remnawave Panel" @@ -25,6 +25,10 @@ class BanNotificationRequest(BaseModel): # Данные для warning warning_message: Optional[str] = Field(None, description="Текст предупреждения") + # Данные для network_wifi + network_type: Optional[str] = Field(None, description="Тип сети (WiFi/Mobile)") + node_name: Optional[str] = Field(None, description="Название ноды") + class Config: json_schema_extra = { "example": {