Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37a905f056 | |||
| bce9061401 | |||
| 118b282a2b | |||
| 6c250ed36f | |||
| 3842aeacca | |||
| 1d1afbe1da | |||
| d4db55ea66 | |||
| 354964060c | |||
| 5bc8d8e52c | |||
| 8f3e8bacc4 | |||
| 61c94e17a4 | |||
| 77c52c1197 | |||
| c65c703e00 | |||
| 7560e802cb | |||
| a5a90a806b | |||
| aa0a271d14 |
@@ -20,19 +20,30 @@
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Почему выбрать Bedolaga Bot?
|
||||
## 🧪 ([Тестирование бота](https://t.me/FringVPN_bot))
|
||||
|
||||
## 💬 **[Bedolaga Chat](https://t.me/+wTdMtSWq8YdmZmVi)** - Для общения, вопросов, предложений
|
||||
|
||||
## 🌟 Почему Bedolagа?
|
||||
Бот Бедолага не добрый и не милый.
|
||||
Он просто делает вашу работу вместо вас, принимает оплату, выдаёт подписки, интегрируется с Remnawave и тихо ненавидит всех, кто ещё не подключил его.
|
||||
|
||||
Вы хотите продавать VPN — Бедолага позволит это делать.
|
||||
Вы хотите спать — он позволит и это.
|
||||
|
||||
### ⚡ **Полная автоматизация VPN бизнеса**
|
||||
- 🎯 **Готовое решение** - разверни за 5 минут, начни продавать сегодня
|
||||
- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + планы на ЮKassa
|
||||
- 🔄 **Автоматизация 99%** - от регистрации до продления подписок
|
||||
- 📊 **Детальная аналитика**
|
||||
- 📊 **Детальная аналитика**
|
||||
|
||||
### 🎛️ **Гибкость конфигурации**
|
||||
- 🌍 **Выбор стран** - пользователи сами выбирают нужные локации
|
||||
- 📱 **Управление устройствами** - от 1 до неограниченного количества
|
||||
- 📱 **Управление устройствами** - от 1 до 10 шт
|
||||
- 📊 **Гибкие тарифы** - от 5GB до безлимита, от 14 дней до года
|
||||
- 🎁 **Промо-система** - коды на деньги, дни подписки, триал-периоды
|
||||
- 3 режима показа ссылки подписки: 1) С гайдом по подключению прямо в боте(тянущий данные приложений и ссылок на скачку из app-config.json) 2) Обычное открытие ссылки подписки в миниапе 3) Интеграция сабпейджа мипошки - кастомно прописать ссылку можно
|
||||
- Возможность переключаться между пакетной продажей трафика и фиксированной(Пропуская шаг выбора пакета трафика при оформлении/настройки подписки юзера)
|
||||
|
||||
### 💪 **Enterprise готовность**
|
||||
- 🏗️ **Современная архитектура** - AsyncIO, PostgreSQL, Redis
|
||||
@@ -44,7 +55,7 @@
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### 🐳 Один Docker команда - и ты в бизнесе
|
||||
### 🐳 Docker запуск
|
||||
|
||||
```bash
|
||||
# 1. Скачай репозиторий
|
||||
@@ -132,7 +143,7 @@ CONNECT_BUTTON_MODE=miniapp_subscription
|
||||
# 🎛️ === NEW: Traffic Selection Mode Settings ===
|
||||
# Режим выбора трафика:
|
||||
# "selectable" - пользователи выбирают пакеты трафика (по умолчанию)
|
||||
# "fixed" - фиксированный лимит трафика для всех подписок(БЕЗ ШАГА ВЫБОРА ПАКЕТА ТРАФИКА НА ЭТАПЕ ОФОРМЛЕНИЯ ПОДПИСКИ), доступно 5/10/25/50/100/250/0 (0 безлимит) гб
|
||||
# "fixed" - фиксированный лимит трафика для всех подписок(БЕЗ ШАГА ВЫБОРА ПАКЕТА ТРАФИКА ВО ВРЕМЯ ОФОРМЛЕНИЯ ПОДПИСКИ), доступно 5/10/25/50/100/250/0 (0 безлимит) гб
|
||||
# Фиксированный лимит трафика в ГБ (используется только в режиме "fixed")
|
||||
# 0 = безлимит
|
||||
# для "fixed" обязательно должы быть проставлены цены на пакеты 5/10/25/50/100/250/0 можно постать 0 руб - будет беслпатно
|
||||
|
||||
+668
-3
@@ -860,6 +860,606 @@ async def show_user_statistics(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def extend_user_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(extending_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"⏰ <b>Продление подписки</b>\n\n"
|
||||
"Введите количество дней для продления:\n"
|
||||
"• Например: 30, 7, 90\n"
|
||||
"• Максимум: 365 дней\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="7 дней", callback_data=f"admin_sub_extend_days_{user_id}_7"),
|
||||
types.InlineKeyboardButton(text="30 дней", callback_data=f"admin_sub_extend_days_{user_id}_30")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="90 дней", callback_data=f"admin_sub_extend_days_{user_id}_90"),
|
||||
types.InlineKeyboardButton(text="180 дней", callback_data=f"admin_sub_extend_days_{user_id}_180")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_subscription_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.extending_subscription)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_extension_days(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
days = int(parts[-1])
|
||||
|
||||
success = await _extend_subscription_by_days(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
f"✅ Подписка пользователя продлена на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка продления подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_extension_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("extending_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
days = int(message.text.strip())
|
||||
|
||||
if days <= 0 or days > 365:
|
||||
await message.answer("❌ Количество дней должно быть от 1 до 365")
|
||||
return
|
||||
|
||||
success = await _extend_subscription_by_days(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Подписка пользователя продлена на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка продления подписки")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число дней")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def add_subscription_traffic(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(traffic_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📊 <b>Добавление трафика</b>\n\n"
|
||||
"Введите количество ГБ для добавления:\n"
|
||||
"• Например: 50, 100, 500\n"
|
||||
"• Максимум: 10000 ГБ\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="50 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_50"),
|
||||
types.InlineKeyboardButton(text="100 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_100")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="500 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_500"),
|
||||
types.InlineKeyboardButton(text="1000 ГБ", callback_data=f"admin_sub_traffic_add_{user_id}_1000")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="♾️ Безлимит", callback_data=f"admin_sub_traffic_add_{user_id}_0"),
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_subscription_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.adding_traffic)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_traffic_addition_button(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
gb = int(parts[-1])
|
||||
|
||||
success = await _add_subscription_traffic(db, user_id, gb, db_user.id)
|
||||
|
||||
if success:
|
||||
traffic_text = "♾️ безлимитный" if gb == 0 else f"{gb} ГБ"
|
||||
await callback.message.edit_text(
|
||||
f"✅ К подписке пользователя добавлен трафик: {traffic_text}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка добавления трафика",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_traffic_addition_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("traffic_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
gb = int(message.text.strip())
|
||||
|
||||
if gb < 0 or gb > 10000:
|
||||
await message.answer("❌ Количество ГБ должно быть от 0 до 10000 (0 = безлимит)")
|
||||
return
|
||||
|
||||
success = await _add_subscription_traffic(db, user_id, gb, db_user.id)
|
||||
|
||||
if success:
|
||||
traffic_text = "♾️ безлимитный" if gb == 0 else f"{gb} ГБ"
|
||||
await message.answer(
|
||||
f"✅ К подписке пользователя добавлен трафик: {traffic_text}",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка добавления трафика")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число ГБ")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def deactivate_user_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🚫 <b>Деактивация подписки</b>\n\n"
|
||||
"Вы уверены, что хотите деактивировать подписку этого пользователя?\n"
|
||||
"Пользователь потеряет доступ к сервису.",
|
||||
reply_markup=get_confirmation_keyboard(
|
||||
f"admin_sub_deactivate_confirm_{user_id}",
|
||||
f"admin_user_subscription_{user_id}",
|
||||
db_user.language
|
||||
)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def confirm_subscription_deactivation(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
success = await _deactivate_user_subscription(db, user_id, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Подписка пользователя деактивирована",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка деактивации подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def activate_user_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
success = await _activate_user_subscription(db, user_id, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Подписка пользователя активирована",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка активации подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def grant_trial_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
success = await _grant_trial_subscription(db, user_id, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
"✅ Пользователю выдан триальный период",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка выдачи триального периода",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def grant_paid_subscription(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
await state.update_data(granting_user_id=user_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"💎 <b>Выдача подписки</b>\n\n"
|
||||
"Введите количество дней подписки:\n"
|
||||
"• Например: 30, 90, 180, 365\n"
|
||||
"• Максимум: 730 дней\n\n"
|
||||
"Или нажмите /cancel для отмены",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="30 дней", callback_data=f"admin_sub_grant_days_{user_id}_30"),
|
||||
types.InlineKeyboardButton(text="90 дней", callback_data=f"admin_sub_grant_days_{user_id}_90")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="180 дней", callback_data=f"admin_sub_grant_days_{user_id}_180"),
|
||||
types.InlineKeyboardButton(text="365 дней", callback_data=f"admin_sub_grant_days_{user_id}_365")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_subscription_{user_id}")
|
||||
]
|
||||
])
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.granting_subscription)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_grant_days(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
user_id = int(parts[-2])
|
||||
days = int(parts[-1])
|
||||
|
||||
success = await _grant_paid_subscription(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
f"✅ Пользователю выдана подписка на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка выдачи подписки",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_subscription_grant_text(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
db: AsyncSession
|
||||
):
|
||||
data = await state.get_data()
|
||||
user_id = data.get("granting_user_id")
|
||||
|
||||
if not user_id:
|
||||
await message.answer("❌ Ошибка: пользователь не найден")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
days = int(message.text.strip())
|
||||
|
||||
if days <= 0 or days > 730:
|
||||
await message.answer("❌ Количество дней должно быть от 1 до 730")
|
||||
return
|
||||
|
||||
success = await _grant_paid_subscription(db, user_id, days, db_user.id)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Пользователю выдана подписка на {days} дней",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Ошибка выдачи подписки")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число дней")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
async def _extend_subscription_by_days(db: AsyncSession, user_id: int, days: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, extend_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
await extend_subscription(db, subscription, days)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} продлил подписку пользователя {user_id} на {days} дней")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка продления подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _add_subscription_traffic(db: AsyncSession, user_id: int, gb: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, add_subscription_traffic
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
if gb == 0:
|
||||
subscription.traffic_limit_gb = 0
|
||||
await db.commit()
|
||||
else:
|
||||
await add_subscription_traffic(db, subscription, gb)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
traffic_text = "безлимитный" if gb == 0 else f"{gb} ГБ"
|
||||
logger.info(f"Админ {admin_id} добавил трафик {traffic_text} пользователю {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления трафика: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _deactivate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, deactivate_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
await deactivate_subscription(db, subscription)
|
||||
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if user and user.remnawave_uuid:
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
|
||||
logger.info(f"Админ {admin_id} деактивировал подписку пользователя {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка деактивации подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.database.models import SubscriptionStatus
|
||||
from datetime import datetime
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||
if subscription.end_date <= datetime.utcnow():
|
||||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} активировал подписку пользователя {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка активации подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _grant_trial_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, create_trial_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
existing_subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if existing_subscription:
|
||||
logger.error(f"У пользователя {user_id} уже есть подписка")
|
||||
return False
|
||||
|
||||
subscription = await create_trial_subscription(db, user_id)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} выдал триальную подписку пользователю {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка выдачи триальной подписки: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _grant_paid_subscription(db: AsyncSession, user_id: int, days: int, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id, create_paid_subscription
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.config import settings
|
||||
|
||||
existing_subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if existing_subscription:
|
||||
logger.error(f"У пользователя {user_id} уже есть подписка")
|
||||
return False
|
||||
|
||||
subscription = await create_paid_subscription(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
duration_days=days,
|
||||
traffic_limit_gb=settings.DEFAULT_TRAFFIC_LIMIT_GB,
|
||||
device_limit=settings.DEFAULT_DEVICE_LIMIT,
|
||||
connected_squads=[settings.TRIAL_SQUAD_UUID] if settings.TRIAL_SQUAD_UUID else []
|
||||
)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} выдал платную подписку на {days} дней пользователю {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка выдачи платной подписки: {e}")
|
||||
return False
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def cleanup_inactive_users(
|
||||
@@ -925,7 +1525,7 @@ def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_user_block,
|
||||
F.data.startswith("admin_user_block_")
|
||||
F.data.startswith("admin_user_block_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
@@ -940,7 +1540,7 @@ def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_user_delete,
|
||||
F.data.startswith("admin_user_delete_")
|
||||
F.data.startswith("admin_user_delete_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
@@ -973,7 +1573,6 @@ def register_handlers(dp: Dispatcher):
|
||||
AdminStates.editing_user_balance
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
show_inactive_users,
|
||||
F.data == "admin_users_inactive"
|
||||
@@ -983,3 +1582,69 @@ def register_handlers(dp: Dispatcher):
|
||||
cleanup_inactive_users,
|
||||
F.data == "admin_cleanup_inactive"
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
extend_user_subscription,
|
||||
F.data.startswith("admin_sub_extend_") & ~F.data.contains("days") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_subscription_extension_days,
|
||||
F.data.startswith("admin_sub_extend_days_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_subscription_extension_text,
|
||||
AdminStates.extending_subscription
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
add_subscription_traffic,
|
||||
F.data.startswith("admin_sub_traffic_") & ~F.data.contains("add")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_traffic_addition_button,
|
||||
F.data.startswith("admin_sub_traffic_add_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_traffic_addition_text,
|
||||
AdminStates.adding_traffic
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
deactivate_user_subscription,
|
||||
F.data.startswith("admin_sub_deactivate_") & ~F.data.contains("confirm")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
confirm_subscription_deactivation,
|
||||
F.data.startswith("admin_sub_deactivate_confirm_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
activate_user_subscription,
|
||||
F.data.startswith("admin_sub_activate_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
grant_trial_subscription,
|
||||
F.data.startswith("admin_sub_grant_trial_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
grant_paid_subscription,
|
||||
F.data.startswith("admin_sub_grant_") & ~F.data.contains("trial") & ~F.data.contains("days")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
process_subscription_grant_days,
|
||||
F.data.startswith("admin_sub_grant_days_")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_subscription_grant_text,
|
||||
AdminStates.granting_subscription
|
||||
)
|
||||
|
||||
@@ -1407,9 +1407,16 @@ async def confirm_purchase(
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
|
||||
if existing_subscription and existing_subscription.is_trial == False:
|
||||
if db_user.remnawave_uuid:
|
||||
logger.info(f"📝 Обновляем существующего RemnaWave пользователя {db_user.remnawave_uuid}")
|
||||
remnawave_user = await subscription_service.update_remnawave_user(db, subscription)
|
||||
else:
|
||||
logger.info(f"🆕 Создаем нового RemnaWave пользователя для {db_user.telegram_id}")
|
||||
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
if not remnawave_user:
|
||||
logger.error(f"❌ Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
|
||||
logger.info(f"🔄 Fallback: принудительное создание нового RemnaWave пользователя")
|
||||
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
await create_transaction(
|
||||
|
||||
@@ -86,32 +86,59 @@ class PromoCodeService:
|
||||
effects.append(f"💰 Баланс пополнен на {balance_bonus_rubles}₽")
|
||||
|
||||
if promocode.subscription_days > 0:
|
||||
from app.database.crud.subscription import create_paid_subscription
|
||||
from app.config import settings
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user.id)
|
||||
|
||||
if subscription:
|
||||
await extend_subscription(db, subscription, promocode.subscription_days)
|
||||
|
||||
await self.subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
effects.append(f"⏰ Подписка продлена на {promocode.subscription_days} дней")
|
||||
logger.info(f"✅ Подписка пользователя {user.telegram_id} продлена на {promocode.subscription_days} дней в RemnaWave с текущими сквадами")
|
||||
|
||||
else:
|
||||
await create_paid_subscription(
|
||||
from app.database.crud.subscription import create_paid_subscription
|
||||
|
||||
trial_squads = []
|
||||
if hasattr(settings, 'TRIAL_SQUAD_UUID') and settings.TRIAL_SQUAD_UUID:
|
||||
trial_squads = [settings.TRIAL_SQUAD_UUID]
|
||||
|
||||
new_subscription = await create_paid_subscription(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
duration_days=promocode.subscription_days,
|
||||
traffic_limit_gb=0,
|
||||
traffic_limit_gb=0,
|
||||
device_limit=1,
|
||||
connected_squads=[]
|
||||
connected_squads=trial_squads
|
||||
)
|
||||
|
||||
await self.subscription_service.create_remnawave_user(db, new_subscription)
|
||||
|
||||
effects.append(f"🎉 Получена подписка на {promocode.subscription_days} дней")
|
||||
logger.info(f"✅ Создана новая подписка для пользователя {user.telegram_id} на {promocode.subscription_days} дней с триал сквадом {trial_squads}")
|
||||
|
||||
if promocode.type == PromoCodeType.TRIAL_SUBSCRIPTION.value:
|
||||
from app.database.crud.subscription import create_trial_subscription
|
||||
from app.config import settings
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user.id)
|
||||
if not subscription and not user.has_had_paid_subscription:
|
||||
await create_trial_subscription(db, user.id)
|
||||
effects.append("🎁 Активирована тестовая подписка")
|
||||
|
||||
if not subscription:
|
||||
trial_days = promocode.subscription_days if promocode.subscription_days > 0 else settings.TRIAL_DURATION_DAYS
|
||||
|
||||
trial_subscription = await create_trial_subscription(
|
||||
db,
|
||||
user.id,
|
||||
duration_days=trial_days
|
||||
)
|
||||
|
||||
await self.subscription_service.create_remnawave_user(db, trial_subscription)
|
||||
|
||||
effects.append(f"🎁 Активирована тестовая подписка на {trial_days} дней")
|
||||
logger.info(f"✅ Создана триал подписка для пользователя {user.telegram_id} на {trial_days} дней")
|
||||
else:
|
||||
effects.append("ℹ️ Тестовая подписка уже недоступна")
|
||||
effects.append("ℹ️ У вас уже есть активная подписка")
|
||||
|
||||
return "\n".join(effects) if effects else "✅ Промокод активирован"
|
||||
return "\n".join(effects) if effects else "✅ Промокод активирован"
|
||||
|
||||
@@ -391,34 +391,22 @@ class RemnaWaveService:
|
||||
return False
|
||||
|
||||
async def sync_users_from_panel(self, db: AsyncSession, sync_type: str = "all") -> Dict[str, int]:
|
||||
"""
|
||||
Синхронизация пользователей из панели RemnaWave в бота
|
||||
|
||||
sync_type:
|
||||
- "all": полная синхронизация (создание + обновление + удаление)
|
||||
- "new_only": только создание новых пользователей
|
||||
- "update_only": только обновление существующих
|
||||
"""
|
||||
try:
|
||||
stats = {"created": 0, "updated": 0, "errors": 0, "deleted": 0}
|
||||
|
||||
logger.info(f"🔄 Начинаем синхронизацию типа: {sync_type}")
|
||||
|
||||
async with self.api as api:
|
||||
# Получаем всех пользователей из панели
|
||||
panel_users_data = await api._make_request('GET', '/api/users')
|
||||
panel_users = panel_users_data['response']['users']
|
||||
|
||||
logger.info(f"👥 Найдено пользователей в панели: {len(panel_users)}")
|
||||
|
||||
# Получаем всех пользователей из бота для сравнения
|
||||
bot_users = await get_users_list(db, offset=0, limit=10000)
|
||||
bot_users_by_telegram_id = {user.telegram_id: user for user in bot_users}
|
||||
|
||||
# Множество telegram_id из панели для проверки удаленных
|
||||
panel_telegram_ids = set()
|
||||
|
||||
# Обрабатываем каждого пользователя из панели
|
||||
for i, panel_user in enumerate(panel_users):
|
||||
try:
|
||||
telegram_id = panel_user.get('telegramId')
|
||||
@@ -433,7 +421,6 @@ class RemnaWaveService:
|
||||
db_user = bot_users_by_telegram_id.get(telegram_id)
|
||||
|
||||
if not db_user:
|
||||
# Пользователя нет в боте - создаем
|
||||
if sync_type in ["new_only", "all"]:
|
||||
logger.info(f"🆕 Создание пользователя для telegram_id {telegram_id}")
|
||||
|
||||
@@ -455,7 +442,6 @@ class RemnaWaveService:
|
||||
logger.info(f"✅ Создан пользователь {telegram_id} с подпиской")
|
||||
|
||||
else:
|
||||
# Пользователь есть в боте - обновляем
|
||||
if sync_type in ["update_only", "all"]:
|
||||
logger.debug(f"🔄 Обновление пользователя {telegram_id}")
|
||||
|
||||
@@ -472,25 +458,64 @@ class RemnaWaveService:
|
||||
stats["errors"] += 1
|
||||
continue
|
||||
|
||||
# Удаляем подписки пользователей, которых нет в панели
|
||||
if sync_type == "all":
|
||||
logger.info("🗑️ Удаляем подписки пользователей, отсутствующих в панели...")
|
||||
logger.info("🗑️ Полная очистка подписок пользователей, отсутствующих в панели...")
|
||||
|
||||
for telegram_id, db_user in bot_users_by_telegram_id.items():
|
||||
if telegram_id not in panel_telegram_ids and db_user.subscription:
|
||||
try:
|
||||
logger.info(f"🗑️ Удаляем подписку пользователя {telegram_id} (нет в панели)")
|
||||
logger.info(f"🗑️ Полная очистка данных подписки пользователя {telegram_id} (нет в панели)")
|
||||
|
||||
# Деактивируем подписку
|
||||
from app.database.crud.subscription import deactivate_subscription
|
||||
await deactivate_subscription(db, db_user.subscription)
|
||||
subscription = db_user.subscription
|
||||
|
||||
if db_user.remnawave_uuid:
|
||||
try:
|
||||
devices_reset = await api.reset_user_devices(db_user.remnawave_uuid)
|
||||
if devices_reset:
|
||||
logger.info(f"🔧 Сброшены HWID устройства для пользователя {telegram_id}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Не удалось сбросить HWID устройства для пользователя {telegram_id}")
|
||||
except Exception as hwid_error:
|
||||
logger.error(f"❌ Ошибка сброса HWID устройств для {telegram_id}: {hwid_error}")
|
||||
|
||||
from app.database.models import SubscriptionStatus
|
||||
from datetime import datetime
|
||||
|
||||
subscription.status = SubscriptionStatus.DISABLED.value
|
||||
subscription.is_trial = True
|
||||
subscription.end_date = datetime.utcnow()
|
||||
subscription.traffic_limit_gb = 0
|
||||
subscription.traffic_used_gb = 0.0
|
||||
subscription.device_limit = 1
|
||||
subscription.connected_squads = []
|
||||
subscription.autopay_enabled = False
|
||||
subscription.autopay_days_before = 3
|
||||
|
||||
subscription.remnawave_short_uuid = None
|
||||
subscription.subscription_url = ""
|
||||
|
||||
db_user.remnawave_uuid = None
|
||||
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_server_ids, remove_subscription_servers
|
||||
|
||||
server_ids = await get_subscription_server_ids(db, subscription.id)
|
||||
if server_ids:
|
||||
await remove_subscription_servers(db, subscription.id, server_ids)
|
||||
logger.info(f"🗑️ Удалены серверы подписки ({len(server_ids)}): {server_ids}")
|
||||
except Exception as servers_error:
|
||||
logger.warning(f"⚠️ Не удалось удалить серверы подписки: {servers_error}")
|
||||
|
||||
|
||||
await db.commit()
|
||||
|
||||
stats["deleted"] += 1
|
||||
logger.info(f"✅ Деактивирована подписка пользователя {telegram_id}")
|
||||
logger.info(f"✅ ПОЛНОСТЬЮ очищены ВСЕ данные подписки пользователя {telegram_id}")
|
||||
|
||||
except Exception as delete_error:
|
||||
logger.error(f"❌ Ошибка удаления подписки {telegram_id}: {delete_error}")
|
||||
logger.error(f"❌ Ошибка полной очистки данных подписки {telegram_id}: {delete_error}")
|
||||
stats["errors"] += 1
|
||||
await db.rollback()
|
||||
|
||||
logger.info(f"🎯 Синхронизация завершена: создано {stats['created']}, обновлено {stats['updated']}, удалено {stats['deleted']}, ошибок {stats['errors']}")
|
||||
return stats
|
||||
@@ -599,7 +624,6 @@ class RemnaWaveService:
|
||||
await self._create_subscription_from_panel_data(db, user, panel_user)
|
||||
return
|
||||
|
||||
# Обновляем статус подписки
|
||||
panel_status = panel_user.get('status', 'ACTIVE')
|
||||
expire_at_str = panel_user.get('expireAt', '')
|
||||
|
||||
@@ -611,14 +635,12 @@ class RemnaWaveService:
|
||||
if expire_at.tzinfo is not None:
|
||||
expire_at = expire_at.replace(tzinfo=None)
|
||||
|
||||
# Обновляем дату окончания если она отличается
|
||||
if abs((subscription.end_date - expire_at).total_seconds()) > 60: # больше минуты разницы
|
||||
if abs((subscription.end_date - expire_at).total_seconds()) > 60:
|
||||
subscription.end_date = expire_at
|
||||
logger.debug(f"Обновлена дата окончания подписки до {expire_at}")
|
||||
except Exception as date_error:
|
||||
logger.warning(f"⚠️ Ошибка парсинга даты при обновлении {expire_at_str}: {date_error}")
|
||||
|
||||
# Обновляем статус
|
||||
current_time = datetime.utcnow()
|
||||
if panel_status == 'ACTIVE' and subscription.end_date > current_time:
|
||||
new_status = SubscriptionStatus.ACTIVE.value
|
||||
@@ -627,13 +649,12 @@ class RemnaWaveService:
|
||||
elif panel_status == 'DISABLED':
|
||||
new_status = SubscriptionStatus.DISABLED.value
|
||||
else:
|
||||
new_status = subscription.status # Оставляем текущий статус
|
||||
new_status = subscription.status
|
||||
|
||||
if subscription.status != new_status:
|
||||
subscription.status = new_status
|
||||
logger.debug(f"Обновлен статус подписки: {new_status}")
|
||||
|
||||
# Обновляем использованный трафик
|
||||
used_traffic_bytes = panel_user.get('usedTrafficBytes', 0)
|
||||
traffic_used_gb = used_traffic_bytes / (1024**3)
|
||||
|
||||
@@ -641,7 +662,6 @@ class RemnaWaveService:
|
||||
subscription.traffic_used_gb = traffic_used_gb
|
||||
logger.debug(f"Обновлен использованный трафик: {traffic_used_gb} GB")
|
||||
|
||||
# Обновляем лимит трафика
|
||||
traffic_limit_bytes = panel_user.get('trafficLimitBytes', 0)
|
||||
traffic_limit_gb = traffic_limit_bytes // (1024**3) if traffic_limit_bytes > 0 else 0
|
||||
|
||||
@@ -649,22 +669,18 @@ class RemnaWaveService:
|
||||
subscription.traffic_limit_gb = traffic_limit_gb
|
||||
logger.debug(f"Обновлен лимит трафика: {traffic_limit_gb} GB")
|
||||
|
||||
# Обновляем лимит устройств
|
||||
device_limit = panel_user.get('hwidDeviceLimit', 1) or 1
|
||||
if subscription.device_limit != device_limit:
|
||||
subscription.device_limit = device_limit
|
||||
logger.debug(f"Обновлен лимит устройств: {device_limit}")
|
||||
|
||||
# Обновляем RemnaWave UUID если отсутствует
|
||||
if not subscription.remnawave_short_uuid:
|
||||
subscription.remnawave_short_uuid = panel_user.get('shortUuid')
|
||||
|
||||
# Обновляем URL подписки если отсутствует или изменился
|
||||
panel_url = panel_user.get('subscriptionUrl', '')
|
||||
if not subscription.subscription_url or subscription.subscription_url != panel_url:
|
||||
subscription.subscription_url = panel_url
|
||||
|
||||
# Обновляем подключенные сквады
|
||||
active_squads = panel_user.get('activeInternalSquads', [])
|
||||
squad_uuids = []
|
||||
if isinstance(active_squads, list):
|
||||
@@ -674,7 +690,6 @@ class RemnaWaveService:
|
||||
elif isinstance(squad, str):
|
||||
squad_uuids.append(squad)
|
||||
|
||||
# Сравниваем сквады - обновляем только если есть изменения
|
||||
current_squads = set(subscription.connected_squads or [])
|
||||
new_squads = set(squad_uuids)
|
||||
|
||||
@@ -729,7 +744,6 @@ class RemnaWaveService:
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
# Обновляем UUID в нашей базе
|
||||
await update_user(db, user, remnawave_uuid=new_user.uuid)
|
||||
subscription.remnawave_short_uuid = new_user.short_uuid
|
||||
await db.commit()
|
||||
|
||||
@@ -34,10 +34,23 @@ class SubscriptionService:
|
||||
logger.error(f"Пользователь {subscription.user_id} не найден")
|
||||
return None
|
||||
|
||||
validation_success = await self.validate_and_clean_subscription(db, subscription, user)
|
||||
if not validation_success:
|
||||
logger.error(f"Ошибка валидации подписки для пользователя {user.telegram_id}")
|
||||
return None
|
||||
|
||||
async with self.api as api:
|
||||
existing_users = await api.get_user_by_telegram_id(user.telegram_id)
|
||||
if existing_users:
|
||||
logger.info(f"🔄 Найден существующий пользователь в панели для {user.telegram_id}")
|
||||
remnawave_user = existing_users[0]
|
||||
|
||||
try:
|
||||
await api.reset_user_devices(remnawave_user.uuid)
|
||||
logger.info(f"🔧 Сброшены HWID устройства для пользователя {user.telegram_id}")
|
||||
except Exception as hwid_error:
|
||||
logger.warning(f"⚠️ Не удалось сбросить HWID: {hwid_error}")
|
||||
|
||||
updated_user = await api.update_user(
|
||||
uuid=remnawave_user.uuid,
|
||||
status=UserStatus.ACTIVE,
|
||||
@@ -47,7 +60,9 @@ class SubscriptionService:
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
else:
|
||||
logger.info(f"🆕 Создаем нового пользователя в панели для {user.telegram_id}")
|
||||
username = f"user_{user.telegram_id}"
|
||||
updated_user = await api.create_user(
|
||||
username=username,
|
||||
@@ -67,7 +82,7 @@ class SubscriptionService:
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"✅ Создан RemnaWave пользователь для подписки {subscription.id}")
|
||||
logger.info(f"✅ Создан/обновлен RemnaWave пользователь для подписки {subscription.id}")
|
||||
logger.info(f"🔗 Ссылка на подписку: {updated_user.subscription_url}")
|
||||
logger.info(f"📊 Стратегия сброса трафика: MONTH")
|
||||
return updated_user
|
||||
@@ -267,6 +282,54 @@ class SubscriptionService:
|
||||
logger.error(f"Ошибка расчета стоимости продления: {e}")
|
||||
from app.config import PERIOD_PRICES
|
||||
return PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
async def validate_and_clean_subscription(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
subscription: Subscription,
|
||||
user: User
|
||||
) -> bool:
|
||||
try:
|
||||
needs_cleanup = False
|
||||
|
||||
if user.remnawave_uuid:
|
||||
try:
|
||||
async with self.api as api:
|
||||
remnawave_user = await api.get_user_by_uuid(user.remnawave_uuid)
|
||||
|
||||
if not remnawave_user:
|
||||
logger.warning(f"⚠️ Пользователь {user.telegram_id} имеет UUID {user.remnawave_uuid}, но не найден в панели")
|
||||
needs_cleanup = True
|
||||
else:
|
||||
if remnawave_user.telegram_id != user.telegram_id:
|
||||
logger.warning(f"⚠️ Несоответствие telegram_id для пользователя {user.telegram_id}")
|
||||
needs_cleanup = True
|
||||
except Exception as api_error:
|
||||
logger.error(f"❌ Ошибка проверки пользователя в панели: {api_error}")
|
||||
needs_cleanup = True
|
||||
|
||||
if subscription.remnawave_short_uuid and not user.remnawave_uuid:
|
||||
logger.warning(f"⚠️ У подписки есть short_uuid, но у пользователя нет remnawave_uuid")
|
||||
needs_cleanup = True
|
||||
|
||||
if needs_cleanup:
|
||||
logger.info(f"🧹 Очищаем мусорные данные подписки для пользователя {user.telegram_id}")
|
||||
|
||||
subscription.remnawave_short_uuid = None
|
||||
subscription.subscription_url = ""
|
||||
subscription.connected_squads = []
|
||||
|
||||
user.remnawave_uuid = None
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✅ Мусорные данные очищены для пользователя {user.telegram_id}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка валидации подписки для пользователя {user.telegram_id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
async def get_countries_price_by_uuids(
|
||||
self,
|
||||
|
||||
+4
-1
@@ -35,6 +35,9 @@ class AdminStates(StatesGroup):
|
||||
|
||||
waiting_for_user_search = State()
|
||||
editing_user_balance = State()
|
||||
extending_subscription = State()
|
||||
adding_traffic = State()
|
||||
granting_subscription = State()
|
||||
editing_user_subscription = State()
|
||||
|
||||
creating_promocode = State()
|
||||
@@ -79,4 +82,4 @@ class SquadCreateStates(StatesGroup):
|
||||
selecting_inbounds = State()
|
||||
|
||||
class SquadRenameStates(StatesGroup):
|
||||
waiting_for_new_name = State()
|
||||
waiting_for_new_name = State()
|
||||
|
||||
Reference in New Issue
Block a user