Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a0a2dccff | |||
| 66a6f32d4c | |||
| 02b3f61953 | |||
| dc08526255 | |||
| 2ca339610b | |||
| ffe65937c1 | |||
| 4b6f03eef5 | |||
| d2f530a7fe | |||
| 44ea4b6b13 | |||
| ebc8421a70 | |||
| 526f142dea | |||
| ca431d36e6 | |||
| 68a81bdd89 | |||
| 68a3d713ef | |||
| 50d3d08e38 | |||
| 9a2fec4d8f | |||
| e68b1d2092 | |||
| 6dfedef81e | |||
| 317e39c823 | |||
| 0072a3cb71 | |||
| 8d021c1530 | |||
| 23606dc2d4 | |||
| 0838c4196d | |||
| 65605a0de2 | |||
| a20ac80281 | |||
| 02e83fb0d0 | |||
| eddad0db6e | |||
| ffad0afb61 | |||
| 83eed8e1cd | |||
| 0c7c8abade | |||
| a064890ed8 | |||
| f310f4a35e | |||
| 0268e09882 | |||
| 449814d0df | |||
| a3d3577103 | |||
| a3086fdd2c | |||
| daafc6a41e | |||
| c27ee116f7 | |||
| 5167cde443 | |||
| 34542d3a9c | |||
| e73d1df4cc | |||
| f9a95c3d48 | |||
| 2dd2b5fdbf | |||
| 896f1db9d4 | |||
| a1bd2d775f | |||
| 8cbe23f9ed |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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 для перехода в меню управления..."
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
@@ -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")],
|
||||
|
||||
@@ -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(
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 211 KiB |
@@ -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 == "Старая подписка":
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||