Compare commits

...

46 Commits

Author SHA1 Message Date
Egor 2a0a2dccff Update README.md 2025-08-13 06:55:22 +03:00
Egor 66a6f32d4c Update .env.example 2025-08-13 06:51:35 +03:00
Egor 02b3f61953 Update translations.py 2025-08-13 06:37:10 +03:00
Egor dc08526255 Update config.py 2025-08-13 06:36:52 +03:00
Egor 2ca339610b Update subscription_monitor.py 2025-08-13 06:36:32 +03:00
Egor ffe65937c1 Update README.md 2025-08-12 19:16:27 +03:00
Egor 4b6f03eef5 Update README.md 2025-08-12 19:15:02 +03:00
Egor d2f530a7fe Update bedolaga.sh 2025-08-12 19:10:37 +03:00
Egor 44ea4b6b13 Update .env.example 2025-08-12 18:55:22 +03:00
Egor ebc8421a70 Update README.md 2025-08-12 18:52:20 +03:00
Egor 526f142dea Update remnawave_api.py 2025-08-12 18:12:34 +03:00
Egor ca431d36e6 Add files via upload 2025-08-12 03:49:13 +03:00
Egor 68a81bdd89 Delete screens/уведомления/Снимок экрана 2025-08-12 в 01.45.14.png 2025-08-12 03:48:59 +03:00
Fr1ngg 68a3d713ef Update README.md 2025-08-12 03:19:42 +03:00
Fr1ngg 50d3d08e38 Update README.md 2025-08-12 03:14:42 +03:00
Fr1ngg 9a2fec4d8f Update README.md 2025-08-12 03:10:34 +03:00
Fr1ngg e68b1d2092 Add files via upload 2025-08-12 02:48:15 +03:00
Fr1ngg 6dfedef81e Update README.md 2025-08-12 02:36:41 +03:00
Fr1ngg 317e39c823 Update keyboards.py 2025-08-12 02:34:19 +03:00
Fr1ngg 0072a3cb71 Update translations.py 2025-08-12 02:34:03 +03:00
Fr1ngg 8d021c1530 Update database.py 2025-08-12 02:33:44 +03:00
Fr1ngg 23606dc2d4 Update admin_handlers.py 2025-08-12 02:33:23 +03:00
Fr1ngg 0838c4196d Add files via upload 2025-08-12 01:47:52 +03:00
Fr1ngg 65605a0de2 Create 1 2025-08-12 01:47:38 +03:00
Fr1ngg a20ac80281 Delete screens/уведомления 2025-08-12 01:47:25 +03:00
Fr1ngg 02e83fb0d0 Create уведомления 2025-08-12 01:47:13 +03:00
Fr1ngg eddad0db6e Add files via upload 2025-08-12 01:46:00 +03:00
Fr1ngg ffad0afb61 Delete screens/Balance/Снимок экрана 2025-08-09 в 07.54.52.png 2025-08-12 01:44:40 +03:00
Fr1ngg 83eed8e1cd Add files via upload 2025-08-12 01:44:17 +03:00
Fr1ngg 0c7c8abade Add files via upload 2025-08-12 01:42:18 +03:00
Fr1ngg a064890ed8 Delete screens/adminpanel/Снимок экрана 2025-08-09 в 08.09.05.png 2025-08-12 01:41:33 +03:00
Fr1ngg f310f4a35e Delete screens/sub/Снимок экрана 2025-08-09 в 08.05.47.png 2025-08-12 01:41:14 +03:00
Fr1ngg 0268e09882 Delete screens/Главное меню/Mainmenu.png 2025-08-12 01:40:23 +03:00
Fr1ngg 449814d0df Add files via upload 2025-08-12 01:40:09 +03:00
Fr1ngg a3d3577103 Update README.md 2025-08-12 01:37:43 +03:00
Fr1ngg a3086fdd2c Update README.md 2025-08-12 01:34:57 +03:00
Fr1ngg daafc6a41e Update README.md 2025-08-12 01:31:42 +03:00
Fr1ngg c27ee116f7 Update README.md 2025-08-12 01:28:56 +03:00
Fr1ngg 5167cde443 Update bedolaga.sh 2025-08-12 01:25:30 +03:00
Fr1ngg 34542d3a9c Update bedolaga.sh 2025-08-12 01:09:56 +03:00
Fr1ngg e73d1df4cc Update logo2.svg 2025-08-12 01:05:39 +03:00
Fr1ngg f9a95c3d48 Update README.md 2025-08-12 01:04:53 +03:00
Fr1ngg 2dd2b5fdbf Update README.md 2025-08-12 01:04:37 +03:00
Fr1ngg 896f1db9d4 Update README.md 2025-08-11 23:09:23 +03:00
Fr1ngg a1bd2d775f Create emergency_fix.py 2025-08-11 22:54:05 +03:00
Fr1ngg 8cbe23f9ed Update database.py 2025-08-11 22:31:17 +03:00
30 changed files with 2245 additions and 694 deletions
+4 -1
View File
@@ -10,7 +10,6 @@ REFERRAL_PERCENTAGE=0.2
# RemnaWave API Configuration
REMNAWAVE_URL=https://example.com
REMNAWAVE_MODE=remote
REMNAWAVE_TOKEN=
# Admin Configuration (comma-separated list of Telegram user IDs)
@@ -24,6 +23,9 @@ TRIAL_DURATION_DAYS=3 # Дней триала
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
TRIAL_PRICE=0.0 # Оставить 0!
TRIAL_NOTIFICATION_ENABLED=true # Уведомление об истекшей триальной подписке
TRIAL_NOTIFICATION_HOURS_AFTER=1 # Через сколько отсылать сообщение
TRIAL_NOTIFICATION_HOURS_WINDOW=23 # Через сколько выслать повторно
# Monitor Service Settings (дополнительные настройки)
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
@@ -33,6 +35,7 @@ DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после и
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
+446 -608
View File
File diff suppressed because it is too large Load Diff
+564
View File
@@ -7739,3 +7739,567 @@ async def autopay_user_detail_callback(callback: CallbackQuery, user: User, **kw
except Exception as e:
logger.error(f"Error showing autopay user detail: {e}")
await callback.answer("❌ Ошибка получения информации")
@admin_router.callback_query(F.data == "admin_user_subscriptions_all")
async def admin_user_subscriptions_all_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
await show_user_subscriptions_admin(callback, user, page=0, filter_type="all", **kwargs)
@admin_router.callback_query(F.data == "admin_user_subscriptions_filters")
async def admin_user_subscriptions_filters_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
stats = await db.get_user_subscriptions_stats_admin()
text = f"🔍 Фильтры подписок пользователей\n\n"
text += f"📊 Статистика:\n"
text += f"• Всего подписок: {stats['total_subscriptions']}\n"
text += f"• 🟢 Активных: {stats['active_subscriptions']}\n"
text += f"• 🔴 Истекших: {stats['expired_subscriptions']}\n"
text += f"• ⏰ Истекают скоро: {stats['expiring_subscriptions']}\n"
text += f"• 🔄 С автоплатежом: {stats['autopay_subscriptions']}\n"
text += f"• 🆓 Триальных: {stats['trial_subscriptions']}\n"
text += f"• 📦 Импортированных: {stats['imported_subscriptions']}\n\n"
text += f"Выберите фильтр для просмотра:"
try:
await callback.message.edit_text(
text,
reply_markup=admin_user_subscriptions_filters_keyboard(user.language)
)
except Exception as edit_error:
if "message is not modified" in str(edit_error).lower():
await callback.answer("✅ Фильтры обновлены", show_alert=False)
else:
logger.error(f"Error editing filters message: {edit_error}")
await callback.answer("❌ Ошибка отображения фильтров", show_alert=True)
except Exception as e:
logger.error(f"Error showing subscriptions filters: {e}")
await callback.answer("❌ Ошибка загрузки фильтров", show_alert=True)
@admin_router.callback_query(F.data.startswith("filter_subs_"))
async def filter_subscriptions_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
filter_type = callback.data.replace("filter_subs_", "")
await show_user_subscriptions_admin(callback, user, page=0, filter_type=filter_type, **kwargs)
@admin_router.callback_query(F.data.startswith("user_subs_page_"))
async def user_subscriptions_page_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
parts = callback.data.split("_")
page = int(parts[3])
filter_type = parts[4] if len(parts) > 4 else "all"
await show_user_subscriptions_admin(callback, user, page=page, filter_type=filter_type, **kwargs)
except Exception as e:
logger.error(f"Error in user subscriptions pagination: {e}")
await callback.answer("❌ Ошибка навигации", show_alert=True)
@admin_router.callback_query(F.data.startswith("refresh_user_subs_"))
async def refresh_user_subscriptions_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
filter_type = callback.data.replace("refresh_user_subs_", "")
await callback.answer("🔄 Обновляю список...")
await show_user_subscriptions_admin(callback, user, page=0, filter_type=filter_type, **kwargs)
@admin_router.callback_query(F.data.startswith("admin_user_sub_detail_"))
async def admin_user_subscription_detail_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
subscription_id = int(callback.data.replace("admin_user_sub_detail_", ""))
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
if not subscription_detail:
await callback.answer("❌ Подписка не найдена", show_alert=True)
return
def clean_text(text):
if not text:
return "N/A"
return str(text).replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
user_first_name = clean_text(subscription_detail['user_first_name'])
user_username = clean_text(subscription_detail['user_username'])
subscription_name = clean_text(subscription_detail['subscription_name'])
short_uuid = clean_text(subscription_detail['short_uuid'])
text = f"📋 Детали подписки пользователя\n\n"
text += f"👤 Пользователь:\n"
text += f"├ Имя: {user_first_name}\n"
text += f"├ Username: @{user_username}\n"
text += f"├ Telegram ID: {subscription_detail['user_id']}\n"
text += f"└ Баланс: {subscription_detail['user_balance']:.2f}\n\n"
text += f"📦 Подписка:\n"
text += f"├ Название: {subscription_name}\n"
text += f"├ Цена: {subscription_detail['subscription_price']}\n"
text += f"├ Длительность: {subscription_detail['subscription_duration']} дн.\n"
text += f"└ Short UUID: {short_uuid}\n\n"
status_emoji = subscription_detail['status_emoji']
text += f"🔘 Статус: {status_emoji} "
if subscription_detail['status'] == "active":
text += f"Активна (осталось {subscription_detail['days_left']} дн.)\n"
elif subscription_detail['status'] == "expiring_soon":
text += f"Истекает через {subscription_detail['days_left']} дн.\n"
elif subscription_detail['status'] == "expired":
text += "Истекла\n"
elif subscription_detail['status'] == "inactive":
text += "Приостановлена\n"
text += f"📅 Временные рамки:\n"
text += f"├ Создана: {format_datetime(subscription_detail['created_at'], user.language)}\n"
text += f"├ Истекает: {format_datetime(subscription_detail['expires_at'], user.language)}\n"
if subscription_detail['updated_at']:
text += f"└ Обновлена: {format_datetime(subscription_detail['updated_at'], user.language)}\n"
else:
text += f"└ Обновлена: Никогда\n"
text += f"\n🔄 Автоплатеж:\n"
if subscription_detail['auto_pay_enabled']:
text += f"├ Статус: ✅ Включен\n"
text += f"└ Продлять за: {subscription_detail['auto_pay_days_before']} дн. до истечения\n"
if subscription_detail['user_balance'] < subscription_detail['subscription_price']:
needed = subscription_detail['subscription_price'] - subscription_detail['user_balance']
text += f"⚠️ Недостаточно средств! Нужно еще {needed:.2f}\n"
else:
text += f"└ Статус: ❌ Отключен\n"
if subscription_detail['is_trial']:
text += f"\n🆓 Тип: Триальная подписка\n"
elif subscription_detail['is_imported']:
text += f"\n📦 Тип: Импортированная подписка\n"
try:
await callback.message.edit_text(
text,
reply_markup=admin_user_subscription_detail_keyboard(
subscription_id, subscription_detail['user_id'], user.language
)
)
except Exception as edit_error:
if "message is not modified" in str(edit_error).lower():
await callback.answer("✅ Информация актуальна", show_alert=False)
else:
logger.error(f"Error editing detail message: {edit_error}")
await callback.answer("❌ Ошибка отображения деталей", show_alert=True)
except Exception as e:
logger.error(f"Error showing user subscription detail: {e}")
await callback.answer("❌ Ошибка загрузки деталей", show_alert=True)
async def show_user_subscriptions_admin(callback: CallbackQuery, user: User, page: int = 0,
filter_type: str = "all", **kwargs):
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
page_size = 10
offset = page * page_size
subscriptions_data, total_count = await db.get_all_user_subscriptions_admin(
offset=offset, limit=page_size, filter_type=filter_type
)
if not subscriptions_data and page == 0:
filter_names = {
"all": "подписок",
"active": "активных подписок",
"expired": "истекших подписок",
"expiring": "истекающих подписок",
"autopay": "подписок с автоплатежом",
"trial": "триальных подписок",
"imported": "импортированных подписок"
}
await callback.message.edit_text(
f"📋 Список {filter_names.get(filter_type, 'подписок')} пуст",
reply_markup=admin_user_subscriptions_filters_keyboard(user.language)
)
return
if not subscriptions_data and page > 0:
await show_user_subscriptions_admin(callback, user, page - 1, filter_type, **kwargs)
return
filter_titles = {
"all": "Все подписки пользователей",
"active": "Активные подписки",
"expired": "Истекшие подписки",
"expiring": "Истекающие подписки",
"autopay": "Подписки с автоплатежом",
"trial": "Триальные подписки",
"imported": "Импортированные подписки"
}
total_pages = (total_count + page_size - 1) // page_size
text = f"📋 {filter_titles.get(filter_type, 'Подписки')}\n"
text += f"📄 Страница {page + 1} из {total_pages} • Всего: {total_count}\n\n"
for i, sub_data in enumerate(subscriptions_data, start=offset + 1):
status_emojis = {
"active": "🟢",
"expiring": "🟡",
"expiring_soon": "🚨",
"expired": "",
"inactive": ""
}
status_emoji = status_emojis.get(sub_data['status'], "")
user_display = sub_data['user_first_name'] or "Unknown"
user_display = user_display.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
if sub_data['user_username'] != 'N/A':
clean_username = sub_data['user_username'].replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
user_display += f" (@{clean_username})"
subscription_name = sub_data['subscription_name'].replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
text += f"{i}. {status_emoji} {user_display}\n"
text += f" 📋 {subscription_name}{sub_data['subscription_price']}\n"
text += f" 📅 Создана: {format_datetime(sub_data['created_at'], user.language)}\n"
if sub_data['status'] == "active":
text += f" ⏰ Истекает через {sub_data['days_left']} дн.\n"
elif sub_data['status'] in ["expiring", "expiring_soon"]:
text += f" ⚠️ Истекает через {sub_data['days_left']} дн.\n"
elif sub_data['status'] == "expired":
text += f" ❌ Истекла\n"
elif sub_data['status'] == "inactive":
text += f" ⏸ Приостановлена\n"
if sub_data['auto_pay_enabled']:
text += f" 🔄 Автоплатеж: за {sub_data['auto_pay_days_before']} дн.\n"
labels = []
if sub_data['is_trial']:
labels.append("🆓 Trial")
if sub_data['is_imported']:
labels.append("📦 Import")
if labels:
text += f" 🏷 {''.join(labels)}\n"
text += "\n"
additional_buttons = []
if len(subscriptions_data) <= 5:
for sub_data in subscriptions_data:
user_name = (sub_data['user_first_name'] or "User")[:10]
user_name = user_name.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
if len(user_name) > 10:
user_name = user_name[:7] + "..."
additional_buttons.append([
InlineKeyboardButton(
text=f"👤 {user_name}",
callback_data=f"admin_user_sub_detail_{sub_data['id']}"
)
])
if additional_buttons:
text += "👆 Нажмите на кнопку для просмотра деталей:"
keyboard = user_subscriptions_pagination_keyboard(page, total_pages, filter_type, user.language)
if additional_buttons:
nav_buttons = keyboard.inline_keyboard[0] if keyboard.inline_keyboard else []
other_buttons = keyboard.inline_keyboard[1:] if len(keyboard.inline_keyboard) > 1 else []
new_keyboard_buttons = []
if nav_buttons:
new_keyboard_buttons.append(nav_buttons)
for i in range(0, len(additional_buttons), 2):
row = []
for j in range(2):
if i + j < len(additional_buttons):
row.extend(additional_buttons[i + j])
if row:
new_keyboard_buttons.append(row)
new_keyboard_buttons.extend(other_buttons)
keyboard = InlineKeyboardMarkup(inline_keyboard=new_keyboard_buttons)
try:
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as edit_error:
if "message is not modified" in str(edit_error).lower():
await callback.answer("✅ Список обновлен", show_alert=False)
else:
logger.error(f"Error editing message: {edit_error}")
try:
await callback.message.answer(
text,
reply_markup=keyboard
)
except Exception as send_error:
logger.error(f"Error sending new message: {send_error}")
await callback.answer("❌ Ошибка отображения", show_alert=True)
except Exception as e:
logger.error(f"Error showing user subscriptions admin: {e}")
try:
await callback.message.edit_text(
"❌ Ошибка загрузки подписок",
reply_markup=admin_user_subscriptions_filters_keyboard(user.language)
)
except:
await callback.answer("❌ Ошибка загрузки подписок", show_alert=True)
@admin_router.callback_query(F.data.startswith("edit_user_sub_"))
async def edit_user_subscription_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
subscription_id = int(callback.data.replace("edit_user_sub_", ""))
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
if not subscription_detail:
await callback.answer("❌ Подписка не найдена", show_alert=True)
return
text = f"✏️ **Редактирование подписки**\n\n"
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
text += f"Что вы хотите изменить?"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="📅 Срок действия", callback_data=f"edit_sub_expiry_{subscription_id}"),
InlineKeyboardButton(text="🔘 Статус", callback_data=f"toggle_sub_status_{subscription_id}")
],
[
InlineKeyboardButton(text="🔄 Автоплатеж", callback_data=f"edit_sub_autopay_{subscription_id}"),
InlineKeyboardButton(text="📊 Трафик", callback_data=f"edit_sub_traffic_{subscription_id}")
],
[InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error editing user subscription: {e}")
await callback.answer("❌ Ошибка редактирования", show_alert=True)
@admin_router.callback_query(F.data.startswith("refresh_user_sub_"))
async def refresh_user_subscription_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
subscription_id = callback.data.replace("refresh_user_sub_", "")
await callback.answer("🔄 Обновляю информацию...")
new_callback_data = f"admin_user_sub_detail_{subscription_id}"
callback.data = new_callback_data
await admin_user_subscription_detail_callback(callback, user, **kwargs)
@admin_router.callback_query(F.data.startswith("edit_sub_traffic_"))
async def edit_sub_traffic_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
subscription_id = int(callback.data.split("_")[-1])
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
if not subscription_detail:
await callback.answer("❌ Подписка не найдена", show_alert=True)
return
text = f"📊 **Изменение лимита трафика**\n\n"
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
text += f"Введите новый лимит трафика в ГБ (0 = безлимит):"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")]
]),
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error editing subscription traffic: {e}")
await callback.answer("❌ Ошибка редактирования", show_alert=True)
@admin_router.callback_query(F.data.startswith("edit_sub_expiry_"))
async def edit_sub_expiry_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
subscription_id = int(callback.data.split("_")[-1])
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
if not subscription_detail:
await callback.answer("❌ Подписка не найдена", show_alert=True)
return
text = f"📅 **Изменение срока действия**\n\n"
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
text += f"Введите новую дату истечения (YYYY-MM-DD) или количество дней:"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")]
]),
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error editing subscription expiry: {e}")
await callback.answer("❌ Ошибка редактирования", show_alert=True)
@admin_router.callback_query(F.data.startswith("toggle_sub_status_"))
async def toggle_subscription_status_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
subscription_id = int(callback.data.split("_")[-1])
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
async with db.session_factory() as session:
from sqlalchemy import select, update
from database import UserSubscription
result = await session.execute(
select(UserSubscription).where(UserSubscription.id == subscription_id)
)
user_subscription = result.scalar_one_or_none()
if not user_subscription:
await callback.answer("❌ Подписка не найдена", show_alert=True)
return
new_status = not user_subscription.is_active
await session.execute(
update(UserSubscription)
.where(UserSubscription.id == subscription_id)
.values(is_active=new_status)
)
await session.commit()
status_text = "активирована" if new_status else "деактивирована"
await callback.answer(f"✅ Подписка {status_text}")
log_user_action(user.telegram_id, "subscription_status_toggled",
f"SubID: {subscription_id}, Active: {new_status}")
await admin_user_subscription_detail_callback(callback, user, **kwargs)
except Exception as e:
logger.error(f"Error toggling subscription status: {e}")
await callback.answer("❌ Ошибка изменения статуса", show_alert=True)
@admin_router.callback_query(F.data.startswith("edit_sub_autopay_"))
async def edit_sub_autopay_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
subscription_id = int(callback.data.split("_")[-1])
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
if not subscription_detail:
await callback.answer("❌ Подписка не найдена", show_alert=True)
return
current_autopay = subscription_detail['auto_pay_enabled']
autopay_days = subscription_detail['auto_pay_days_before']
text = f"🔄 **Настройки автоплатежа**\n\n"
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
text += f"Текущее состояние: {'✅ Включен' if current_autopay else '❌ Отключен'}\n"
if current_autopay:
text += f"Продлевать за: {autopay_days} дней до истечения\n\n"
buttons = []
if current_autopay:
buttons.append([InlineKeyboardButton(text="❌ Отключить автоплатеж", callback_data=f"disable_autopay_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="📅 Изменить дни", callback_data=f"change_autopay_days_{subscription_id}")])
else:
buttons.append([InlineKeyboardButton(text="✅ Включить автоплатеж", callback_data=f"enable_autopay_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error editing subscription autopay: {e}")
await callback.answer("❌ Ошибка редактирования", show_alert=True)
+1 -1
View File
@@ -261,7 +261,7 @@
text-anchor="middle"
fill="url(#mainGradient)"
filter="url(#glow)">
RemnaWave
Remnawave
</text>
<text x="200" y="345"

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+512 -43
View File
@@ -101,18 +101,23 @@ create_project_structure() {
create_docker_compose() {
log "Создание docker-compose.yml..."
echo "Выберите конфигурацию Docker Compose:"
echo "1) Минимальная (рекомендуется) - только PostgreSQL и бот"
echo "2) Расширенная - с Redis и Nginx"
echo "Выберите конфигурацию установки:"
echo "1) Только бот (панель RemnaWave на другом сервере)"
echo "2) Панель + бот на одном сервере (рекомендуется)"
echo "3) Расширенная - с Redis и Nginx"
while true; do
read -p "Ваш выбор (1-2): " choice
read -p "Ваш выбор (1-3): " choice
case $choice in
1)
create_minimal_compose
create_standalone_compose
break
;;
2)
create_panel_bot_compose
break
;;
3)
create_full_compose
break
;;
@@ -123,7 +128,8 @@ create_docker_compose() {
done
}
create_minimal_compose() {
# Создание конфигурации только для бота (внешняя панель)
create_standalone_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
@@ -180,7 +186,72 @@ networks:
driver: bridge
EOF
log "Минимальная конфигурация Docker Compose создана"
log "Конфигурация только для бота создана"
export COMPOSE_TYPE="standalone"
}
# Создание конфигурации панель + бот
create_panel_bot_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- remnawave-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- remnawave-network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres_data:
driver: local
networks:
remnawave-network:
name: remnawave-network
external: true
EOF
log "Конфигурация панель + бот на одном сервере создана"
log "ВАЖНО: Убедитесь что панель RemnaWave уже установлена и создала сеть remnawave-network"
export COMPOSE_TYPE="panel_bot"
}
create_full_compose() {
@@ -287,6 +358,7 @@ networks:
EOF
log "Расширенная конфигурация Docker Compose создана"
export COMPOSE_TYPE="full"
}
# Создание .env файла
@@ -298,29 +370,17 @@ create_env_file() {
read -p "Введите BOT_USERNAME (без @): " BOT_USERNAME
read -p "Введите ADMIN_IDS (через запятую): " ADMIN_IDS
# Настройки RemnaWave
echo "Выберите режим RemnaWave:"
echo "1) remote"
echo "2) local"
while true; do
read -p "Ваш выбор (1-2): " remna_choice
case $remna_choice in
1)
REMNAWAVE_MODE="remote"
break
;;
2)
REMNAWAVE_MODE="local"
break
;;
*)
error "Неверный выбор. Попробуйте снова."
;;
esac
done
# Настройки RemnaWave в зависимости от типа установки
if [ "$COMPOSE_TYPE" = "panel_bot" ]; then
log "Настройка для панель + бот на одном сервере"
REMNAWAVE_URL="http://remnawave:3000"
echo "URL панели будет: $REMNAWAVE_URL (внутренний адрес контейнера)"
else
read -p "Введите REMNAWAVE_URL (например: https://your-panel.com): " REMNAWAVE_URL
fi
read -p "Введите REMNAWAVE_URL: " REMNAWAVE_URL
read -p "Введите REMNAWAVE_TOKEN: " REMNAWAVE_TOKEN
read -p "Введите SUBSCRIPTION_BASE_URL (например: https://sub.your-domain.com): " SUBSCRIPTION_BASE_URL
# Настройки триала
while true; do
@@ -352,18 +412,62 @@ create_env_file() {
read -p "Введите REFERRAL_THRESHOLD (сумму с .0 на конце): " REFERRAL_THRESHOLD
read -p "Введите REFERRAL_PERCENTAGE (с 0. в начале): " REFERRAL_PERCENTAGE
# Настройки оплаты звездами Telegram
echo ""
echo -e "${YELLOW}=== Настройка оплаты звездами Telegram ===${NC}"
while true; do
read -p "Включить оплату звездами Telegram? (y/n): " stars_enabled
case $stars_enabled in
[Yy]*)
STARS_ENABLED="true"
echo ""
echo -e "${BLUE}Настройка курсов обмена звезд на рубли:${NC}"
echo -e "${YELLOW}Введите курс обмена для каждого пакета звезд${NC}"
echo -e "${YELLOW}(например, если 100 звезд = 150 рублей, введите 150)${NC}"
echo ""
read -p "Курс для 100 звезд (в рублях): " STARS_100_RATE
read -p "Курс для 150 звезд (в рублях): " STARS_150_RATE
read -p "Курс для 250 звезд (в рублях): " STARS_250_RATE
read -p "Курс для 350 звезд (в рублях): " STARS_350_RATE
read -p "Курс для 500 звезд (в рублях): " STARS_500_RATE
break
;;
[Nn]*)
STARS_ENABLED="false"
STARS_100_RATE=""
STARS_150_RATE=""
STARS_250_RATE=""
STARS_350_RATE=""
STARS_500_RATE=""
break
;;
*)
error "Пожалуйста, ответьте y или n."
;;
esac
done
# Настройки мониторинга
read -p "Введите DELETE_EXPIRED_TRIAL_DAYS: " DELETE_EXPIRED_TRIAL_DAYS
read -p "Введите DELETE_EXPIRED_REGULAR_DAYS: " DELETE_EXPIRED_REGULAR_DAYS
# Создание .env файла
cat > "$ENV_FILE" << EOF
# Bot Configuration
BOT_TOKEN=$BOT_TOKEN
BOT_USERNAME=$BOT_USERNAME
ADMIN_IDS=$ADMIN_IDS
REMNAWAVE_MODE=$REMNAWAVE_MODE
# RemnaWave API Configuration
REMNAWAVE_URL=$REMNAWAVE_URL
REMNAWAVE_TOKEN=$REMNAWAVE_TOKEN
SUBSCRIPTION_BASE_URL=$SUBSCRIPTION_BASE_URL
# Admin Configuration
ADMIN_IDS=$ADMIN_IDS
SUPPORT_USERNAME=support
# Trial Configuration
TRIAL_ENABLED=$TRIAL_ENABLED
EOF
@@ -376,14 +480,31 @@ EOF
fi
cat >> "$ENV_FILE" << EOF
TRIAL_PRICE=0.0
# Referral Configuration
REFERRAL_FIRST_REWARD=$REFERRAL_FIRST_REWARD
REFERRAL_REFERRED_BONUS=$REFERRAL_REFERRED_BONUS
REFERRAL_THRESHOLD=$REFERRAL_THRESHOLD
REFERRAL_PERCENTAGE=$REFERRAL_PERCENTAGE
# Monitor Service Settings
# Telegram Stars Configuration
STARS_ENABLED=$STARS_ENABLED
EOF
# Добавляем курсы звезд только если включена оплата звездами
if [ "$STARS_ENABLED" = "true" ]; then
cat >> "$ENV_FILE" << EOF
STARS_100_RATE=$STARS_100_RATE
STARS_150_RATE=$STARS_150_RATE
STARS_250_RATE=$STARS_250_RATE
STARS_350_RATE=$STARS_350_RATE
STARS_500_RATE=$STARS_500_RATE
EOF
fi
cat >> "$ENV_FILE" << EOF
# Monitor Configuration
MONITOR_ENABLED=true
MONITOR_CHECK_INTERVAL=21600
MONITOR_DAILY_CHECK_HOUR=12
@@ -391,6 +512,8 @@ MONITOR_WARNING_DAYS=2
DELETE_EXPIRED_TRIAL_DAYS=$DELETE_EXPIRED_TRIAL_DAYS
DELETE_EXPIRED_REGULAR_DAYS=$DELETE_EXPIRED_REGULAR_DAYS
AUTO_DELETE_ENABLED=true
# Lucky Game Configuration
LUCKY_GAME_ENABLED=true
LUCKY_GAME_REWARD=50.0
LUCKY_GAME_NUMBERS=30
@@ -398,6 +521,17 @@ LUCKY_GAME_WINNING_COUNT=5
EOF
log ".env файл создан успешно"
# Показываем специальные инструкции для панель + бот
if [ "$COMPOSE_TYPE" = "panel_bot" ]; then
echo ""
echo -e "${YELLOW}=== ВАЖНЫЕ ИНСТРУКЦИИ ДЛЯ ПАНЕЛЬ + БОТ ===${NC}"
echo -e "${GREEN}1. Убедитесь что панель RemnaWave уже запущена${NC}"
echo -e "${GREEN}2. URL панели установлен как: $REMNAWAVE_URL${NC}"
echo -e "${GREEN}3. Бот будет подключаться к панели через внутреннюю Docker сеть${NC}"
echo -e "${YELLOW}4. Если панель не запущена, сначала запустите её!${NC}"
echo ""
fi
}
# Создание службы systemd
@@ -456,10 +590,23 @@ check_remnawave_connection() {
if [ -f "$ENV_FILE" ]; then
source "$ENV_FILE"
if [ ! -z "$REMNAWAVE_URL" ]; then
if curl -s --connect-timeout 5 "$REMNAWAVE_URL/api/auth/status" > /dev/null; then
return 0 # Подключен
# Для внутренних URL (панель+бот) проверка API отключена
# так как панель может блокировать HTTP запросы через ProxyCheckMiddleware
if [[ "$REMNAWAVE_URL" == *"remnawave:3000"* ]]; then
# Для локальной установки просто проверяем что бот запущен
# Если бот работает - значит скорее всего API тоже доступен
if docker compose -f "$COMPOSE_FILE" ps bot | grep -q "Up"; then
return 0 # Считаем что подключен если бот запущен
else
return 1
fi
else
return 1 # Не подключен
# Внешний URL - проверяем напрямую
if curl -s --connect-timeout 5 "$REMNAWAVE_URL/api/system/stats" > /dev/null 2>&1; then
return 0
else
return 1
fi
fi
else
return 1
@@ -473,8 +620,36 @@ check_remnawave_connection() {
start_bot() {
log "Запуск бота..."
cd "$BOT_DIR"
# Проверяем тип установки
if grep -q "remnawave-network" "$COMPOSE_FILE"; then
log "Обнаружена конфигурация панель + бот"
# Проверяем существование сети
if ! docker network ls | grep -q "remnawave-network"; then
error "Сеть remnawave-network не найдена!"
error "Убедитесь что панель RemnaWave запущена и создала сеть"
echo ""
echo "Для проверки выполните:"
echo " docker network ls | grep remnawave"
echo ""
echo "Если сети нет, сначала запустите панель RemnaWave"
return 1
fi
log "Сеть remnawave-network найдена ✓"
fi
docker compose up -d
log "Бот запущен"
# Ждем немного и проверяем статус
sleep 5
if check_bot_status; then
log "✅ Бот успешно запущен и работает"
else
warn "⚠️ Бот запущен но возможны проблемы. Проверьте логи: docker compose logs bot"
fi
}
# Остановка бота
@@ -759,6 +934,250 @@ diagnose_database() {
echo -e "${RED}Не удается получить размер базы данных${NC}"
}
}
# Функция экстренного исправления базы данных
emergency_fix_database() {
log "Экстренное исправление базы данных..."
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер бота
if ! docker compose ps bot | grep -q "Up"; then
warn "Контейнер бота не запущен. Запускаем бота..."
docker compose up -d bot
# Ждем готовности бота
log "Ожидание готовности бота..."
for i in {1..60}; do
if docker compose logs bot 2>/dev/null | grep -q "Bot started successfully\|Application startup complete\|Bot is running"; then
log "Бот готов к работе"
break
fi
if [ $i -eq 60 ]; then
warn "Бот не запустился полностью, но попробуем выполнить исправление"
break
fi
sleep 2
echo -n "."
done
echo ""
fi
# Создаем скрипт исправления во временном файле
EMERGENCY_SCRIPT="$BOT_DIR/emergency_fix.py"
log "Создание скрипта экстренного исправления..."
cat > "$EMERGENCY_SCRIPT" << 'EOF'
"""
Экстренное исправление проблемы с отображением подписок
Этот патч добавляет недостающие поля в таблицу user_subscriptions
"""
import asyncio
import sys
import os
from pathlib import Path
# Добавляем корневую директорию в путь
sys.path.insert(0, str(Path(__file__).parent))
try:
from config import load_config
from database import Database
except ImportError:
# Если не можем импортировать, попробуем из app
sys.path.insert(0, '/app')
try:
from config import load_config
from database import Database
except ImportError:
print("❌ Не удается импортировать модули. Проверьте структуру проекта.")
sys.exit(1)
from sqlalchemy import text
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def check_and_add_column(db, column_name, column_definition):
"""Проверяет и добавляет колонку в таблицу"""
try:
# Отдельная транзакция для проверки
async with db.engine.begin() as conn:
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
logger.info(f"✅ Поле {column_name} уже существует")
return True
except Exception:
# Отдельная транзакция для добавления колонки
try:
async with db.engine.begin() as conn:
logger.info(f" Добавляю поле {column_name}...")
await conn.execute(text(f"""
ALTER TABLE user_subscriptions
ADD COLUMN {column_name} {column_definition}
"""))
logger.info(f"✅ Поле {column_name} добавлено")
return True
except Exception as e:
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
return False
async def emergency_fix():
"""Экстренное исправление базы данных"""
try:
# Загружаем конфигурацию
config = load_config()
# Подключаемся к базе данных
db = Database(config.DATABASE_URL)
logger.info("🔧 Выполняю экстренное исправление базы данных...")
# Проверяем существование таблицы user_subscriptions
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
count = result.scalar()
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
except Exception as e:
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
await db.close()
return
# Добавляем поля по одному в отдельных транзакциях
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
# Финальная проверка в отдельной транзакции
if success1 and success2:
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("""
SELECT id, auto_pay_enabled, auto_pay_days_before
FROM user_subscriptions LIMIT 1
"""))
row = result.fetchone()
if row:
logger.info("✅ Все поля доступны для чтения")
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
else:
logger.info("✅ Все поля доступны, но таблица пуста")
except Exception as e:
logger.error(f"❌ Поля все еще недоступны: {e}")
else:
logger.error("❌ Не удалось добавить все необходимые поля")
await db.close()
logger.info("🎉 Экстренное исправление завершено!")
except Exception as e:
logger.error(f"💥 Критическая ошибка: {e}")
raise
if __name__ == "__main__":
asyncio.run(emergency_fix())
EOF
# Копируем скрипт в контейнер и запускаем
log "Копирование скрипта в контейнер бота..."
if docker compose exec bot test -d /app; then
# Копируем скрипт в контейнер
docker compose cp "$EMERGENCY_SCRIPT" bot:/app/emergency_fix.py
log "Запуск экстренного исправления в контейнере бота..."
if docker compose exec bot python emergency_fix.py; then
log "✅ Экстренное исправление выполнено успешно!"
# Перезапускаем бота для применения изменений
log "Перезапуск бота для применения изменений..."
docker compose restart bot
log "✅ Бот перезапущен"
else
error "❌ Ошибка при выполнении экстренного исправления"
echo "Проверьте логи бота: docker compose logs bot"
fi
# Удаляем временный скрипт из контейнера
docker compose exec bot rm -f /app/emergency_fix.py 2>/dev/null || true
else
error "❌ Не удается найти директорию /app в контейнере бота"
echo "Проверьте, что контейнер бота запущен правильно"
fi
# Удаляем временный скрипт с хоста
rm -f "$EMERGENCY_SCRIPT"
}
# Альтернативный метод экстренного исправления через SQL
emergency_fix_database_sql() {
log "Экстренное исправление базы данных через SQL..."
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер postgres
if ! docker compose ps postgres | grep -q "Up"; then
log "Контейнер PostgreSQL не запущен. Запускаем PostgreSQL..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
log "PostgreSQL готов к работе"
break
fi
if [ $i -eq 30 ]; then
error "PostgreSQL не запустился в течение 60 секунд"
return 1
fi
sleep 2
echo -n "."
done
echo ""
fi
log "Проверка существования полей в таблице user_subscriptions..."
# Проверяем auto_pay_enabled
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "SELECT auto_pay_enabled FROM user_subscriptions LIMIT 1" &>/dev/null; then
log "✅ Поле auto_pay_enabled уже существует"
else
log "➕ Добавление поля auto_pay_enabled..."
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "ALTER TABLE user_subscriptions ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE" &>/dev/null; then
log "✅ Поле auto_pay_enabled добавлено"
else
error "❌ Ошибка при добавлении поля auto_pay_enabled"
return 1
fi
fi
# Проверяем auto_pay_days_before
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "SELECT auto_pay_days_before FROM user_subscriptions LIMIT 1" &>/dev/null; then
log "✅ Поле auto_pay_days_before уже существует"
else
log "➕ Добавление поля auto_pay_days_before..."
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "ALTER TABLE user_subscriptions ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3" &>/dev/null; then
log "✅ Поле auto_pay_days_before добавлено"
else
error "❌ Ошибка при добавлении поля auto_pay_days_before"
return 1
fi
fi
log "✅ Экстренное исправление через SQL завершено!"
# Перезапускаем бота если он запущен
if docker compose ps bot | grep -q "Up"; then
log "Перезапуск бота для применения изменений..."
docker compose restart bot
log "✅ Бот перезапущен"
fi
}
# Редактирование .env файла
edit_env_file() {
ensure_nano
@@ -840,12 +1259,33 @@ remove_bot() {
fi
}
# Проверка статуса сети RemnaWave
check_remnawave_network() {
if docker network ls | grep -q "remnawave-network"; then
return 0 # Сеть существует
else
return 1 # Сеть не существует
fi
}
# Главное меню
show_menu() {
clear
echo -e "${BLUE}=== RemnaWave Bedolaga Bot Management ===${NC}"
echo ""
# Определяем тип установки
INSTALLATION_TYPE="Неизвестно"
if [ -f "$COMPOSE_FILE" ]; then
if grep -q "remnawave-network" "$COMPOSE_FILE" && grep -q "external: true" "$COMPOSE_FILE"; then
INSTALLATION_TYPE="Панель + Бот на одном сервере"
elif grep -q "bot_network" "$COMPOSE_FILE"; then
INSTALLATION_TYPE="Только бот (внешняя панель)"
fi
fi
echo -e "${YELLOW}Тип установки: ${NC}$INSTALLATION_TYPE"
# Показать статус бота
echo -e "${YELLOW}Статус бота:${NC}"
if check_bot_status; then
@@ -854,6 +1294,16 @@ show_menu() {
echo -e "🔴 Бот: ${RED}ОСТАНОВЛЕН${NC}"
fi
# Показать статус сети (для панель + бот)
if [[ "$INSTALLATION_TYPE" == *"Панель + Бот"* ]]; then
echo -e "${YELLOW}Статус сети RemnaWave:${NC}"
if check_remnawave_network; then
echo -e "🟢 Сеть: ${GREEN}СОЗДАНА${NC}"
else
echo -e "🔴 Сеть: ${RED}НЕ НАЙДЕНА${NC} (запустите панель RemnaWave)"
fi
fi
# Показать статус подключения к API
echo -e "${YELLOW}Подключение к RemnaWave API:${NC}"
if check_remnawave_connection; then
@@ -876,8 +1326,10 @@ show_menu() {
echo "7) Восстановить базу данных"
echo "8) Редактировать .env файл"
echo "9) Диагностика базы данных"
echo "10) Удалить базу данных"
echo "11) Полностью удалить бота"
echo "10) Экстренное исправление БД (Python)"
echo "11) Экстренное исправление БД (SQL)"
echo "12) Удалить базу данных"
echo "13) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
@@ -891,8 +1343,10 @@ show_menu() {
7) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
8) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
9) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
10) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
11) remove_bot; ;;
10) emergency_fix_database; read -p "Нажмите Enter для продолжения..."; ;;
11) emergency_fix_database_sql; read -p "Нажмите Enter для продолжения..."; ;;
12) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
13) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
@@ -905,8 +1359,10 @@ show_menu() {
echo "5) Восстановить базу данных"
echo "6) Редактировать .env файл"
echo "7) Диагностика базы данных"
echo "8) Удалить базу данных"
echo "9) Полностью удалить бота"
echo "8) Экстренное исправление БД (Python)"
echo "9) Экстренное исправление БД (SQL)"
echo "10) Удалить базу данных"
echo "11) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
@@ -918,8 +1374,10 @@ show_menu() {
5) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
6) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
7) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
8) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
9) remove_bot; ;;
8) emergency_fix_database; read -p "Нажмите Enter для продолжения..."; ;;
9) emergency_fix_database_sql; read -p "Нажмите Enter для продолжения..."; ;;
10) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
11) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
@@ -939,6 +1397,17 @@ install_bot() {
log "Установка завершена!"
log "Бот установлен в: $BOT_DIR"
if [ "$COMPOSE_TYPE" = "panel_bot" ]; then
echo ""
echo -e "${YELLOW}=== ВАЖНО ДЛЯ ПАНЕЛЬ + БОТ УСТАНОВКИ ===${NC}"
echo -e "${GREEN}1. Перед запуском бота убедитесь что панель RemnaWave запущена${NC}"
echo -e "${GREEN}2. Панель должна создать сеть 'remnawave-network'${NC}"
echo -e "${GREEN}3. Проверить сеть: docker network ls | grep remnawave${NC}"
echo -e "${YELLOW}4. Если сети нет - сначала запустите панель RemnaWave!${NC}"
echo ""
fi
log "Для управления ботом используйте это меню или systemctl"
read -p "Нажмите Enter для перехода в меню управления..."
+6
View File
@@ -28,6 +28,9 @@ class Config:
DELETE_EXPIRED_TRIAL_DAYS: int = 1
DELETE_EXPIRED_REGULAR_DAYS: int = 7
AUTO_DELETE_ENABLED: bool = False
TRIAL_NOTIFICATION_ENABLED: bool = True
TRIAL_NOTIFICATION_HOURS_AFTER: int = 2
TRIAL_NOTIFICATION_HOURS_WINDOW: int = 22
LUCKY_GAME_ENABLED: bool = True
LUCKY_GAME_REWARD: float = 50.0
@@ -112,6 +115,9 @@ def load_config() -> Config:
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
TRIAL_NOTIFICATION_ENABLED=get_bool('TRIAL_NOTIFICATION_ENABLED', True),
TRIAL_NOTIFICATION_HOURS_AFTER=get_int('TRIAL_NOTIFICATION_HOURS_AFTER', 2),
TRIAL_NOTIFICATION_HOURS_WINDOW=get_int('TRIAL_NOTIFICATION_HOURS_WINDOW', 22),
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
+315 -23
View File
@@ -1885,32 +1885,38 @@ class Database:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT auto_pay_enabled FROM user_subscriptions LIMIT 1"))
logger.info("Autopay fields already exist")
return
logger.info("auto_pay_enabled field already exists")
except Exception:
pass
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE
"""))
logger.info("Added auto_pay_enabled column")
except Exception as e:
logger.error(f"Error adding auto_pay_enabled column: {e}")
pass
async with self.engine.begin() as conn:
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE
"""))
logger.info("Added auto_pay_enabled column")
except Exception as e:
logger.warning(f"Column auto_pay_enabled may already exist: {e}")
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3
"""))
logger.info("Added auto_pay_days_before column")
except Exception as e:
logger.warning(f"Column auto_pay_days_before may already exist: {e}")
logger.info("Successfully added autopay fields to user_subscriptions table")
await conn.execute(text("SELECT auto_pay_days_before FROM user_subscriptions LIMIT 1"))
logger.info("auto_pay_days_before field already exists")
except Exception:
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3
"""))
logger.info("Added auto_pay_days_before column")
except Exception as e:
logger.error(f"Error adding auto_pay_days_before column: {e}")
pass
logger.info("Successfully completed autopay fields migration")
except Exception as e:
logger.error(f"Error adding autopay fields: {e}")
logger.error(f"Error during autopay migration: {e}")
async def get_autopay_history(self, limit: int = 50) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
@@ -2143,3 +2149,289 @@ class Database:
except Exception as e:
logger.error(f"Error getting users with insufficient autopay balance: {e}")
return []
async def get_all_user_subscriptions_admin(self, offset: int = 0, limit: int = 20,
filter_type: str = "all") -> tuple[List[Dict], int]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, desc, and_, or_
from datetime import datetime, timedelta
base_query = select(
UserSubscription,
Subscription.name.label('subscription_name'),
Subscription.price.label('subscription_price'),
Subscription.is_trial.label('is_trial'),
Subscription.is_imported.label('is_imported'),
User.username.label('user_username'),
User.first_name.label('user_first_name'),
User.telegram_id.label('user_telegram_id')
).select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
).join(
User.__table__, UserSubscription.user_id == User.telegram_id
)
)
current_time = datetime.utcnow()
if filter_type == "active":
base_query = base_query.where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
elif filter_type == "expired":
base_query = base_query.where(
or_(
UserSubscription.is_active == False,
UserSubscription.expires_at <= current_time
)
)
elif filter_type == "expiring":
expiring_date = current_time + timedelta(days=7)
base_query = base_query.where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time,
UserSubscription.expires_at <= expiring_date
)
)
elif filter_type == "autopay":
base_query = base_query.where(UserSubscription.auto_pay_enabled == True)
elif filter_type == "trial":
base_query = base_query.where(Subscription.is_trial == True)
elif filter_type == "imported":
base_query = base_query.where(Subscription.is_imported == True)
count_query = select(func.count()).select_from(base_query.subquery())
total_count_result = await session.execute(count_query)
total_count = total_count_result.scalar() or 0
data_query = base_query.order_by(desc(UserSubscription.created_at)).offset(offset).limit(limit)
result = await session.execute(data_query)
subscriptions_data = []
for row in result.fetchall():
user_sub = row[0]
if user_sub.expires_at <= current_time:
status = "expired"
elif not user_sub.is_active:
status = "inactive"
else:
days_left = (user_sub.expires_at - current_time).days
if days_left <= 3:
status = "expiring_soon"
elif days_left <= 7:
status = "expiring"
else:
status = "active"
subscriptions_data.append({
'id': user_sub.id,
'user_id': row.user_telegram_id,
'user_username': row.user_username or 'N/A',
'user_first_name': row.user_first_name or 'N/A',
'subscription_name': row.subscription_name,
'subscription_price': row.subscription_price,
'short_uuid': user_sub.short_uuid,
'expires_at': user_sub.expires_at,
'is_active': user_sub.is_active,
'auto_pay_enabled': user_sub.auto_pay_enabled,
'auto_pay_days_before': user_sub.auto_pay_days_before,
'created_at': user_sub.created_at,
'is_trial': row.is_trial,
'is_imported': row.is_imported,
'status': status,
'days_left': (user_sub.expires_at - current_time).days if user_sub.expires_at > current_time else 0
})
return subscriptions_data, total_count
except Exception as e:
logger.error(f"Error getting admin user subscriptions: {e}")
return [], 0
async def get_user_subscription_detail_admin(self, subscription_id: int) -> Optional[Dict]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
query = select(
UserSubscription,
Subscription.name.label('subscription_name'),
Subscription.description.label('subscription_description'),
Subscription.price.label('subscription_price'),
Subscription.duration_days.label('subscription_duration'),
Subscription.traffic_limit_gb.label('subscription_traffic_limit'),
Subscription.is_trial.label('is_trial'),
Subscription.is_imported.label('is_imported'),
User.username.label('user_username'),
User.first_name.label('user_first_name'),
User.telegram_id.label('user_telegram_id'),
User.balance.label('user_balance')
).select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
).join(
User.__table__, UserSubscription.user_id == User.telegram_id
)
).where(UserSubscription.id == subscription_id)
result = await session.execute(query)
row = result.fetchone()
if not row:
return None
user_sub = row[0]
current_time = datetime.utcnow()
if user_sub.expires_at <= current_time:
status = "expired"
status_emoji = ""
elif not user_sub.is_active:
status = "inactive"
status_emoji = ""
else:
days_left = (user_sub.expires_at - current_time).days
if days_left <= 1:
status = "expiring_today"
status_emoji = "🔴"
elif days_left <= 3:
status = "expiring_soon"
status_emoji = "🟡"
elif days_left <= 7:
status = "expiring"
status_emoji = "🟠"
else:
status = "active"
status_emoji = "🟢"
return {
'id': user_sub.id,
'user_id': row.user_telegram_id,
'user_username': row.user_username or 'N/A',
'user_first_name': row.user_first_name or 'N/A',
'user_balance': row.user_balance,
'subscription_name': row.subscription_name,
'subscription_description': row.subscription_description,
'subscription_price': row.subscription_price,
'subscription_duration': row.subscription_duration,
'subscription_traffic_limit': row.subscription_traffic_limit,
'short_uuid': user_sub.short_uuid,
'expires_at': user_sub.expires_at,
'is_active': user_sub.is_active,
'auto_pay_enabled': user_sub.auto_pay_enabled,
'auto_pay_days_before': user_sub.auto_pay_days_before,
'created_at': user_sub.created_at,
'updated_at': user_sub.updated_at,
'is_trial': row.is_trial,
'is_imported': row.is_imported,
'status': status,
'status_emoji': status_emoji,
'days_left': (user_sub.expires_at - current_time).days if user_sub.expires_at > current_time else 0
}
except Exception as e:
logger.error(f"Error getting user subscription detail: {e}")
return None
async def get_user_subscriptions_stats_admin(self) -> Dict[str, Any]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_, or_
from datetime import datetime, timedelta
current_time = datetime.utcnow()
total_subs = await session.execute(
select(func.count(UserSubscription.id))
)
total_subscriptions = total_subs.scalar() or 0
active_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
)
active_subscriptions = active_subs.scalar() or 0
expired_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
or_(
UserSubscription.is_active == False,
UserSubscription.expires_at <= current_time
)
)
)
expired_subscriptions = expired_subs.scalar() or 0
autopay_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
UserSubscription.auto_pay_enabled == True
)
)
autopay_subscriptions = autopay_subs.scalar() or 0
expiring_date = current_time + timedelta(days=7)
expiring_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time,
UserSubscription.expires_at <= expiring_date
)
)
)
expiring_subscriptions = expiring_subs.scalar() or 0
trial_subs = await session.execute(
select(func.count(UserSubscription.id))
.select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
)
)
.where(Subscription.is_trial == True)
)
trial_subscriptions = trial_subs.scalar() or 0
imported_subs = await session.execute(
select(func.count(UserSubscription.id))
.select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
)
)
.where(Subscription.is_imported == True)
)
imported_subscriptions = imported_subs.scalar() or 0
return {
'total_subscriptions': total_subscriptions,
'active_subscriptions': active_subscriptions,
'expired_subscriptions': expired_subscriptions,
'autopay_subscriptions': autopay_subscriptions,
'expiring_subscriptions': expiring_subscriptions,
'trial_subscriptions': trial_subscriptions,
'imported_subscriptions': imported_subscriptions
}
except Exception as e:
logger.error(f"Error getting user subscriptions stats: {e}")
return {
'total_subscriptions': 0,
'active_subscriptions': 0,
'expired_subscriptions': 0,
'autopay_subscriptions': 0,
'expiring_subscriptions': 0,
'trial_subscriptions': 0,
'imported_subscriptions': 0
}
+100
View File
@@ -0,0 +1,100 @@
"""
Экстренное исправление проблемы с отображением подписок
Этот патч добавляет недостающие поля в таблицу user_subscriptions
"""
import asyncio
import sys
import os
from pathlib import Path
# Добавляем корневую директорию в путь
sys.path.insert(0, str(Path(__file__).parent))
from config import load_config
from database import Database
from sqlalchemy import text
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def check_and_add_column(db, column_name, column_definition):
"""Проверяет и добавляет колонку в таблицу"""
try:
# Отдельная транзакция для проверки
async with db.engine.begin() as conn:
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
logger.info(f"✅ Поле {column_name} уже существует")
return True
except Exception:
# Отдельная транзакция для добавления колонки
try:
async with db.engine.begin() as conn:
logger.info(f" Добавляю поле {column_name}...")
await conn.execute(text(f"""
ALTER TABLE user_subscriptions
ADD COLUMN {column_name} {column_definition}
"""))
logger.info(f"✅ Поле {column_name} добавлено")
return True
except Exception as e:
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
return False
async def emergency_fix():
"""Экстренное исправление базы данных"""
try:
# Загружаем конфигурацию
config = load_config()
# Подключаемся к базе данных
db = Database(config.DATABASE_URL)
logger.info("🔧 Выполняю экстренное исправление базы данных...")
# Проверяем существование таблицы user_subscriptions
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
count = result.scalar()
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
except Exception as e:
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
await db.close()
return
# Добавляем поля по одному в отдельных транзакциях
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
# Финальная проверка в отдельной транзакции
if success1 and success2:
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("""
SELECT id, auto_pay_enabled, auto_pay_days_before
FROM user_subscriptions LIMIT 1
"""))
row = result.fetchone()
if row:
logger.info("✅ Все поля доступны для чтения")
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
else:
logger.info("✅ Все поля доступны, но таблица пуста")
except Exception as e:
logger.error(f"❌ Поля все еще недоступны: {e}")
else:
logger.error("❌ Не удалось добавить все необходимые поля")
await db.close()
logger.info("🎉 Экстренное исправление завершено!")
except Exception as e:
logger.error(f"💥 Критическая ошибка: {e}")
raise
if __name__ == "__main__":
asyncio.run(emergency_fix())
+74
View File
@@ -2,6 +2,7 @@ from database import Subscription
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from typing import List, Optional, Dict
from translations import t
from datetime import datetime
def language_keyboard() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -218,11 +219,84 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_user_subscriptions_filters_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура фильтров для подписок пользователей"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🟢 Активные", callback_data="filter_subs_active"),
InlineKeyboardButton(text="🔴 Истекшие", callback_data="filter_subs_expired")
],
[
InlineKeyboardButton(text="🔄✅ С автоплатежом", callback_data="filter_subs_autopay"),
InlineKeyboardButton(text="⏰ Истекают скоро", callback_data="filter_subs_expiring")
],
[
InlineKeyboardButton(text="🆓 Триальные", callback_data="filter_subs_trial"),
InlineKeyboardButton(text="📦 Импортированные", callback_data="filter_subs_imported")
],
[
InlineKeyboardButton(text="📋 Все подписки", callback_data="admin_user_subscriptions_all"),
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")
]
])
return keyboard
def admin_user_subscription_detail_keyboard(subscription_id: int, user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура для детального просмотра подписки пользователя"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_user_sub_{subscription_id}"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_sub_{subscription_id}")
],
[
InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_detail_{user_id}"),
InlineKeyboardButton(text="📋 К списку подписок", callback_data="admin_user_subscriptions_all")
],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")]
])
return keyboard
def user_subscriptions_pagination_keyboard(current_page: int, total_pages: int,
filter_type: str = "all", lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"user_subs_page_{current_page - 1}_{filter_type}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"user_subs_page_{current_page + 1}_{filter_type}"))
buttons.append(nav_row)
buttons.append([
InlineKeyboardButton(text="🔍 Фильтры", callback_data="admin_user_subscriptions_filters"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_subs_{filter_type}")
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
+3 -1
View File
@@ -19,7 +19,9 @@ class RemnaWaveAPI:
headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
'Accept': 'application/json',
'X-Forwarded-Proto': 'https',
'X-Forwarded-For': '127.0.0.1'
}
timeout = aiohttp.ClientTimeout(total=30)
self.session = aiohttp.ClientSession(
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+1
View File
@@ -0,0 +1 @@
Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+127 -15
View File
@@ -86,7 +86,7 @@ class SubscriptionMonitorService:
logger.info("Subscription monitor service stopped")
async def _monitor_loop(self):
logger.info("🔄 Starting monitor loop")
logger.info("🔥 Starting monitor loop")
logger.info("⏰ Initial check in 10 seconds...")
await asyncio.sleep(10)
@@ -96,8 +96,10 @@ class SubscriptionMonitorService:
logger.info("🔍 Running periodic subscription check...")
warnings_sent = await self._check_expiring_subscriptions()
if warnings_sent > 0:
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent")
trial_notifications = await self._check_expired_trial_subscriptions()
if warnings_sent > 0 or trial_notifications > 0:
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent, {trial_notifications} trial notifications sent")
else:
logger.info("✅ Monitor check completed: no warnings needed")
@@ -147,7 +149,94 @@ class SubscriptionMonitorService:
break
except Exception as e:
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
await asyncio.sleep(3600)
await asyncio.sleep(3600)
async def _check_expired_trial_subscriptions(self) -> int:
try:
logger.info("🆓 Checking for expired trial subscriptions...")
notifications_sent = 0
now_utc = datetime.utcnow()
all_users = await self.db.get_all_users()
for user in all_users:
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
try:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
continue
if not subscription.is_trial:
continue
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_since_expiry = -time_diff.total_seconds() / 3600
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
logger.info(f"🆓 Sending trial expiry notification to user {user.telegram_id}: "
f"trial '{subscription.name}' expired {hours_since_expiry:.1f} hours ago")
try:
await self._send_trial_expiry_notification(user, subscription)
notifications_sent += 1
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id}")
except Exception as notification_error:
logger.error(f"❌ Failed to send trial notification to user {user.telegram_id}: {notification_error}")
except Exception as sub_error:
logger.error(f"❌ Error checking trial subscription {user_sub.id}: {sub_error}")
except Exception as user_error:
logger.error(f"❌ Error checking trial subscriptions for user {user.telegram_id}: {user_error}")
if notifications_sent > 0:
logger.info(f"🆓 Trial expiry check completed: {notifications_sent} notifications sent")
return notifications_sent
except Exception as e:
logger.error(f"❌ Critical error in check_expired_trial_subscriptions: {e}", exc_info=True)
return 0
async def _send_trial_expiry_notification(self, user, subscription):
try:
if not self.bot:
logger.error("❌ Bot instance is None, cannot send trial notification")
return
from translations import t
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
message = t('trial_subscription_expired', user.language, name=subscription.name)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=t('buy_subscription_btn', user.language),
callback_data="buy_subscription"
)],
[InlineKeyboardButton(
text=t('my_subscriptions_btn', user.language),
callback_data="my_subscriptions"
)]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard)
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id} for subscription '{subscription.name}'")
except Exception as e:
logger.error(f"❌ Error sending trial expiry notification to user {user.telegram_id}: {e}", exc_info=True)
raise
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
try:
@@ -388,11 +477,11 @@ class SubscriptionMonitorService:
f"threshold={self.config.MONITOR_WARNING_DAYS}")
if subscription.is_trial:
logger.debug(f" Skipping trial subscription '{subscription.name}'")
logger.debug(f" Skipping trial subscription '{subscription.name}'")
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
logger.debug(f" Skipping imported subscription '{subscription.name}'")
logger.debug(f" Skipping imported subscription '{subscription.name}'")
continue
should_warn = (
@@ -517,9 +606,13 @@ class SubscriptionMonitorService:
warnings_sent = await self._check_expiring_subscriptions()
logger.info(f"📢 Sent {warnings_sent} expiry warnings")
logger.info("🔄 Deactivating expired subscriptions...")
logger.info("🆓 Checking expired trial subscriptions...")
trial_notifications = await self._check_expired_trial_subscriptions()
logger.info(f"🆓 Sent {trial_notifications} trial expiry notifications")
logger.info("🔥 Deactivating expired subscriptions...")
deactivated_count = await self.deactivate_expired_subscriptions()
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
logger.info(f"🔥 Deactivated {deactivated_count} expired subscriptions")
deleted_trials = 0
deleted_regular = 0
@@ -538,7 +631,7 @@ class SubscriptionMonitorService:
await self._send_final_expiry_notifications()
logger.info("📩 Final notifications sent")
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}, "
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Trial notifications: {trial_notifications}, Deactivated: {deactivated_count}, "
f"Deleted trials: {deleted_trials}, Deleted regular: {deleted_regular}")
return deactivated_count
@@ -590,7 +683,7 @@ class SubscriptionMonitorService:
user_data = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if user_data and user_data.get('uuid'):
await self.api.update_user(user_data['uuid'], {'status': 'EXPIRED'})
logger.debug(f"🔄 Also deactivated user {user_data['uuid']} in RemnaWave")
logger.debug(f"🔥 Also deactivated user {user_data['uuid']} in RemnaWave")
except Exception as api_error:
logger.warning(f"⚠️ Could not deactivate user in RemnaWave: {api_error}")
else:
@@ -704,11 +797,30 @@ class SubscriptionMonitorService:
days_left = int(hours_left / 24)
if subscription.is_trial:
results.append({
'success': True,
'message': f'Trial subscription "{subscription.name}" skipped (no warnings for trials)',
'error': None
})
hours_since_expiry = -hours_left
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
test_message = f"🧪 [ТЕСТОВОЕ УВЕДОМЛЕНИЕ]\n\n🆓 Ваша триальная подписка '{subscription.name}' истекла! Купите новый тариф чтобы продолжить использование VPN."
if self.bot:
try:
await self.bot.send_message(user_id, test_message)
results.append({
'success': True,
'message': f'✅ Sent test trial expiry notification for "{subscription.name}" (expired {hours_since_expiry:.1f} hours ago)',
'error': None
})
except Exception as send_error:
results.append({
'success': False,
'message': f'❌ Failed to send test trial notification for "{subscription.name}"',
'error': str(send_error)
})
else:
results.append({
'success': True,
'message': f'Trial subscription "{subscription.name}" - no notification needed (expired {hours_since_expiry:.1f} hours ago)',
'error': None
})
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
+92 -2
View File
@@ -97,6 +97,48 @@ TRANSLATIONS = {
'manage_balance': 'Управление балансом',
'manage_promocodes': 'Управление промокодами',
'statistics': 'Статистика',
'admin_user_subscriptions': 'Подписки пользователей',
'all_user_subscriptions': 'Все подписки пользователей',
'user_subscription_filters': 'Фильтры подписок',
'active_subscriptions': 'Активные подписки',
'expired_subscriptions': 'Истекшие подписки',
'expiring_subscriptions': 'Истекающие подписки',
'autopay_subscriptions': 'Подписки с автоплатежом',
'trial_subscriptions': 'Триальные подписки',
'imported_subscriptions': 'Импортированные подписки',
'subscription_details': 'Детали подписки',
'user_info': 'Информация о пользователе',
'subscription_info': 'Информация о подписке',
'subscription_status': 'Статус подписки',
'subscription_dates': 'Временные рамки',
'autopay_settings': 'Настройки автоплатежа',
'subscription_type': 'Тип подписки',
'edit_subscription': 'Редактировать подписку',
'toggle_status': 'Изменить статус',
'edit_expiry': 'Изменить срок',
'edit_autopay': 'Настроить автоплатеж',
'subscription_activated': 'Подписка активирована',
'subscription_deactivated': 'Подписка деактивирована',
'insufficient_funds': 'Недостаточно средств',
'funds_needed': 'Нужно еще',
'created_date': 'Дата создания',
'expires_date': 'Дата истечения',
'updated_date': 'Дата обновления',
'days_left': 'Дней осталось',
'autopay_enabled': 'Автоплатеж включен',
'autopay_disabled': 'Автоплатеж отключен',
'renew_before_days': 'Продлять за дней',
'view_details': 'Просмотр деталей',
'back_to_list': 'К списку',
'refresh_data': 'Обновить данные',
'no_subscriptions_found': 'Подписки не найдены',
'subscription_not_found': 'Подписка не найдена',
'database_unavailable': 'База данных недоступна',
'error_loading_subscriptions': 'Ошибка загрузки подписок',
'error_loading_details': 'Ошибка загрузки деталей',
'pagination_error': 'Ошибка навигации',
'edit_error': 'Ошибка редактирования',
'status_toggle_error': 'Ошибка изменения статуса',
'not_admin': '❌ У вас нет прав администратора',
'create_subscription': 'Создать подписку',
'enter_sub_name': 'Введите название подписки:',
@@ -152,7 +194,10 @@ TRANSLATIONS = {
'lucky_game_games_played': 'Игр сыграно: {count}',
'lucky_game_wins': 'Выигрышей: {count}',
'lucky_game_total_won': 'Всего выиграно: {amount}',
'lucky_game_win_rate': 'Процент побед: {rate}%'
'lucky_game_win_rate': 'Процент побед: {rate}%',
'trial_subscription_expired': '🆓 Ваша триальная подписка "{name}" истекла!\n\n'
'💡 Чтобы продолжить использование VPN, купите полный тариф.\n\n'
'✨ Доступны различные планы подписки с выгодными ценами!'
},
'en': {
@@ -204,6 +249,9 @@ TRANSLATIONS = {
'subscription_expires_day_after_tomorrow': '⏰ Your subscription \'{name}\' expires in {days} days!\n\nWe recommend renewing it in advance in \'My Subscriptions\'.\n\n💰 Check your balance - you might want to top it up.',
'subscription_expires_in_days': '⏳ Your subscription \'{name}\' expires in {days} days!\n\nYou can renew it in \'My Subscriptions\'.\n\n💡 Early renewal extends from current expiration date.',
'extend_subscription_btn': '🔄 Extend Subscription',
'trial_subscription_expired': '🆓 Your trial subscription "{name}" has expired!\n\n'
'💡 To continue using VPN, please purchase a full plan.\n\n'
'✨ Various subscription plans with great prices are available!',
'my_subscriptions_btn': '📋 My Subscriptions',
'buy_new_subscription_btn': '🛒 Buy New Subscription',
'restore_subscription_btn': '🔄 Restore Subscription',
@@ -294,7 +342,49 @@ TRANSLATIONS = {
'lucky_game_games_played': 'Games played: {count}',
'lucky_game_wins': 'Wins: {count}',
'lucky_game_total_won': 'Total won: ${amount}',
'lucky_game_win_rate': 'Win rate: {rate}%'
'lucky_game_win_rate': 'Win rate: {rate}%',
'admin_user_subscriptions': 'User Subscriptions',
'all_user_subscriptions': 'All User Subscriptions',
'user_subscription_filters': 'Subscription Filters',
'active_subscriptions': 'Active Subscriptions',
'expired_subscriptions': 'Expired Subscriptions',
'expiring_subscriptions': 'Expiring Subscriptions',
'autopay_subscriptions': 'Autopay Subscriptions',
'trial_subscriptions': 'Trial Subscriptions',
'imported_subscriptions': 'Imported Subscriptions',
'subscription_details': 'Subscription Details',
'user_info': 'User Information',
'subscription_info': 'Subscription Information',
'subscription_status': 'Subscription Status',
'subscription_dates': 'Time Frames',
'autopay_settings': 'Autopay Settings',
'subscription_type': 'Subscription Type',
'edit_subscription': 'Edit Subscription',
'toggle_status': 'Toggle Status',
'edit_expiry': 'Edit Expiry',
'edit_autopay': 'Configure Autopay',
'subscription_activated': 'Subscription activated',
'subscription_deactivated': 'Subscription deactivated',
'insufficient_funds': 'Insufficient funds',
'funds_needed': 'Need more',
'created_date': 'Created Date',
'expires_date': 'Expiry Date',
'updated_date': 'Updated Date',
'days_left': 'Days Left',
'autopay_enabled': 'Autopay Enabled',
'autopay_disabled': 'Autopay Disabled',
'renew_before_days': 'Renew Before Days',
'view_details': 'View Details',
'back_to_list': 'Back to List',
'refresh_data': 'Refresh Data',
'no_subscriptions_found': 'No subscriptions found',
'subscription_not_found': 'Subscription not found',
'database_unavailable': 'Database unavailable',
'error_loading_subscriptions': 'Error loading subscriptions',
'error_loading_details': 'Error loading details',
'pagination_error': 'Navigation error',
'edit_error': 'Edit error',
'status_toggle_error': 'Status toggle error'
}
}