Compare commits

...

16 Commits

Author SHA1 Message Date
Egor 37a905f056 Update states.py 2025-08-24 16:58:17 +03:00
Egor bce9061401 Update users.py 2025-08-24 16:57:29 +03:00
Egor 118b282a2b Update states.py 2025-08-24 16:52:11 +03:00
Egor 6c250ed36f Update users.py 2025-08-24 16:51:31 +03:00
Egor 3842aeacca Update subscription_service.py 2025-08-24 16:23:04 +03:00
Egor 1d1afbe1da Update remnawave_service.py 2025-08-24 16:21:54 +03:00
Egor d4db55ea66 Update subscription.py 2025-08-24 16:19:32 +03:00
Egor 354964060c Update README.md 2025-08-23 18:25:49 +03:00
Egor 5bc8d8e52c Update README.md 2025-08-23 18:25:08 +03:00
Egor 8f3e8bacc4 Update README.md 2025-08-23 18:24:38 +03:00
Egor 61c94e17a4 Update promocode_service.py 2025-08-23 16:04:54 +03:00
Egor 77c52c1197 Update README.md 2025-08-23 14:38:43 +03:00
Egor c65c703e00 Update README.md 2025-08-23 12:56:42 +03:00
Egor 7560e802cb Update README.md 2025-08-23 12:53:05 +03:00
Egor a5a90a806b Update README.md 2025-08-23 12:35:37 +03:00
Egor aa0a271d14 Update README.md 2025-08-23 12:31:25 +03:00
7 changed files with 845 additions and 55 deletions
+16 -5
View File
@@ -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
View File
@@ -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
)
+8 -1
View File
@@ -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(
+36 -9
View File
@@ -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 "✅ Промокод активирован"
+49 -35
View File
@@ -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()
+64 -1
View File
@@ -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
View File
@@ -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()