mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Implement custom days and traffic handling in subscription purchase flow
- Added new states for selecting custom days and traffic in the subscription process. - Enhanced the tariff purchase handler to support custom days and traffic adjustments. - Introduced new functions for formatting and displaying custom tariff previews. - Updated the ban notification service to include a new notification type for WiFi bans. - Modified API routes and schemas to accommodate the new notification type and its parameters.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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"""📦 <b>{tariff.name}</b>
|
||||
|
||||
<b>Настройте параметры:</b>
|
||||
"""
|
||||
|
||||
if tariff.can_purchase_custom_days():
|
||||
text += f"📅 Дней: <b>{days}</b> (от {tariff.min_days} до {tariff.max_days})\n"
|
||||
text += f" 💰 {_format_price_kopeks(days_price)}\n"
|
||||
|
||||
if tariff.can_purchase_custom_traffic():
|
||||
text += f"📊 Трафик: <b>{traffic_gb} ГБ</b> (от {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🎁 <b>Скидка: {discount_percent}%</b>\n"
|
||||
|
||||
text += f"""
|
||||
<b>💰 Итого: {_format_price_kopeks(total_price)}</b>
|
||||
|
||||
💳 Ваш баланс: {_format_price_kopeks(user_balance)}"""
|
||||
|
||||
if user_balance < total_price:
|
||||
missing = total_price - user_balance
|
||||
text += f"\n⚠️ <b>Не хватает: {_format_price_kopeks(missing)}</b>"
|
||||
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"🎉 <b>Подписка успешно оформлена!</b>\n\n"
|
||||
f"📦 Тариф: <b>{tariff.name}</b>\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:"))
|
||||
|
||||
@@ -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"Тип сети: <b>{network_type}</b>\n" if network_type else ""
|
||||
node_info = f"Сервер: <b>{node_name}</b>\n" if node_name else ""
|
||||
|
||||
message_text = (
|
||||
"📶 <b>Блокировка за использование WiFi сети</b>\n\n"
|
||||
f"Ваш аккаунт временно заблокирован из-за использования WiFi сети.\n\n"
|
||||
f"{network_info}"
|
||||
f"{node_info}"
|
||||
f"⏱ Блокировка на: <b>{ban_minutes} мин</b>\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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user