Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 273df23f20 | |||
| a9b0dbf3eb | |||
| 5f56e068d3 | |||
| 92b82132d8 | |||
| 79ae9d3a1c | |||
| 86714a93f9 | |||
| 553f6f8cf6 | |||
| b561f67c12 | |||
| d03f9b80c7 |
@@ -65,7 +65,6 @@ URL и токен RemnaWave API
|
||||
REMNAWAVE_URL=https://your-remnawave-url.ru
|
||||
REMNAWAVE_MODE=remote/local
|
||||
REMNAWAVE_TOKEN=ваш_remnawave_token
|
||||
SUBSCRIPTION_BASE_URL=https://
|
||||
DATABASE_URL=sqlite+aiosqlite:///bot.db
|
||||
ADMIN_IDS=123456789,987654321
|
||||
DEFAULT_LANGUAGE=ru
|
||||
@@ -128,8 +127,6 @@ REMNAWAVE_TOKEN — токен доступа к API RemnaWave.
|
||||
|
||||
DATABASE_URL — строка подключения к базе данных.
|
||||
|
||||
SUBSCRIPTION_BASE_URL=https://sub.example.com (без / на конце)
|
||||
|
||||
ADMIN_IDS — через запятую Telegram ID администраторов.
|
||||
|
||||
SUPPORT_USERNAME — ник поддержки, без @ указывать
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
@@ -13,8 +13,8 @@ class Config:
|
||||
DEFAULT_LANGUAGE: str
|
||||
SUPPORT_USERNAME: str
|
||||
|
||||
# Subscription URL settings
|
||||
SUBSCRIPTION_BASE_URL: str
|
||||
# Subscription URL settings - ТЕПЕРЬ ОПЦИОНАЛЬНЫЙ
|
||||
SUBSCRIPTION_BASE_URL: Optional[str]
|
||||
|
||||
# Trial subscription settings
|
||||
TRIAL_ENABLED: bool
|
||||
@@ -39,12 +39,7 @@ def load_config() -> Config:
|
||||
except ValueError:
|
||||
admin_ids = []
|
||||
|
||||
# Get subscription base URL -
|
||||
subscription_base_url = os.getenv('SUBSCRIPTION_BASE_URL', '')
|
||||
|
||||
# Если SUBSCRIPTION_BASE_URL не установлен, используем значение по умолчанию
|
||||
if not subscription_base_url:
|
||||
subscription_base_url = 'https://sub.fring.tech'
|
||||
subscription_base_url = os.getenv('SUBSCRIPTION_BASE_URL')
|
||||
|
||||
return Config(
|
||||
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
|
||||
@@ -56,7 +51,7 @@ def load_config() -> Config:
|
||||
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
|
||||
SUPPORT_USERNAME=os.getenv('SUPPORT_USERNAME', 'support'),
|
||||
|
||||
# Subscription URL
|
||||
# Subscription URL - ТЕПЕРЬ МОЖЕТ БЫТЬ None
|
||||
SUBSCRIPTION_BASE_URL=subscription_base_url,
|
||||
|
||||
# Trial subscription settings
|
||||
|
||||
+171
-122
@@ -67,7 +67,7 @@ class BotStates(StatesGroup):
|
||||
|
||||
router = Router()
|
||||
|
||||
# Start command
|
||||
# Start command
|
||||
@router.message(Command("start"))
|
||||
async def start_command(message: Message, state: FSMContext, db: Database, **kwargs):
|
||||
"""Handle /start command"""
|
||||
@@ -92,7 +92,7 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
|
||||
else:
|
||||
await show_main_menu(message, user.language, user.is_admin, user.telegram_id, db, config)
|
||||
|
||||
# Language selection
|
||||
# Language selection
|
||||
@router.callback_query(F.data.startswith("lang_"))
|
||||
async def language_callback(callback: CallbackQuery, state: FSMContext, db: Database, **kwargs):
|
||||
"""Handle language selection"""
|
||||
@@ -159,7 +159,7 @@ async def show_main_menu(message: Message, lang: str, is_admin: bool = False, us
|
||||
logger.error(f"Error showing main menu: {e}")
|
||||
await message.answer("❌ Ошибка отображения меню")
|
||||
|
||||
# Main menu handlers
|
||||
# Main menu handlers
|
||||
@router.callback_query(F.data == "main_menu")
|
||||
async def main_menu_callback(callback: CallbackQuery, **kwargs):
|
||||
"""Return to main menu"""
|
||||
@@ -186,6 +186,7 @@ async def main_menu_callback(callback: CallbackQuery, **kwargs):
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial)
|
||||
)
|
||||
|
||||
# Trial subscription handlers
|
||||
@router.callback_query(F.data == "trial_subscription")
|
||||
async def trial_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Show trial subscription info"""
|
||||
@@ -222,7 +223,7 @@ async def trial_subscription_callback(callback: CallbackQuery, db: Database, **k
|
||||
|
||||
@router.callback_query(F.data == "confirm_trial")
|
||||
async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Confirm and create trial subscription"""
|
||||
"""Confirm and create trial subscription - ДОБАВЛЕНА ПОДДЕРЖКА URL ИЗ API"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
config = kwargs.get('config')
|
||||
@@ -312,9 +313,9 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
|
||||
)
|
||||
return
|
||||
|
||||
# Создаем временную тестовую подписку, которая НЕ будет отображаться в админке
|
||||
# Создаем временную тестовую подписку
|
||||
trial_subscription = await db.create_subscription(
|
||||
name=f"Trial_{user.telegram_id}_{int(datetime.utcnow().timestamp())}", # Уникальное имя
|
||||
name=f"Trial_{user.telegram_id}_{int(datetime.utcnow().timestamp())}",
|
||||
description="Автоматически созданная тестовая подписка",
|
||||
price=0,
|
||||
duration_days=config.TRIAL_DURATION_DAYS,
|
||||
@@ -349,9 +350,22 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
|
||||
status='completed'
|
||||
)
|
||||
|
||||
# НОВОЕ: Получаем subscription URL и показываем пользователю
|
||||
success_text = t('trial_success', user.language)
|
||||
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(short_uuid)
|
||||
if subscription_url:
|
||||
success_text += f"\n\n🔗 <a href='{subscription_url}'>Нажмите для подключения</a>"
|
||||
success_text += f"\n📱 Скопируйте ссылку и импортируйте конфигурацию в ваше VPN приложение"
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get trial subscription URL: {e}")
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('trial_success', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
success_text,
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "trial_subscription_activated", "Free trial")
|
||||
@@ -363,6 +377,7 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
# Balance handlers
|
||||
@router.callback_query(F.data == "change_language")
|
||||
async def change_language_callback(callback: CallbackQuery, **kwargs):
|
||||
"""Show language selection for changing language"""
|
||||
@@ -530,7 +545,7 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
logger.error(f"Error getting payment history: {e}")
|
||||
await callback.answer(t('error_occurred', user.language))
|
||||
|
||||
# Subscriptions
|
||||
# Subscription handlers
|
||||
@router.callback_query(F.data == "buy_subscription")
|
||||
async def buy_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Show available subscriptions (excluding trial)"""
|
||||
@@ -607,7 +622,7 @@ async def buy_subscription_detail(callback: CallbackQuery, db: Database, **kwarg
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_buy_"))
|
||||
async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Confirm subscription purchase"""
|
||||
"""Confirm subscription purchase - ДОБАВЛЕНА ПОДДЕРЖКА URL ИЗ API"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
|
||||
@@ -637,9 +652,7 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
)
|
||||
return
|
||||
|
||||
# ИСПРАВЛЕНИЕ: Всегда создавать НОВЫЙ пользователь в RemnaWave для каждой подписки
|
||||
# Это позволит избежать дублирования short_uuid и даст каждой подписке уникальную ссылку
|
||||
|
||||
# Создаем нового пользователя в RemnaWave для каждой подписки
|
||||
username = generate_username()
|
||||
password = generate_password()
|
||||
|
||||
@@ -654,7 +667,7 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
activeInternalSquads=[subscription.squad_uuid]
|
||||
)
|
||||
|
||||
# Handle API response which may use 'data' or 'response' key
|
||||
# Handle API response
|
||||
if remna_user:
|
||||
if 'data' in remna_user and 'uuid' in remna_user['data']:
|
||||
user_uuid = remna_user['data']['uuid']
|
||||
@@ -710,13 +723,13 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user.balance -= subscription.price
|
||||
await db.update_user(user)
|
||||
|
||||
# Create user subscription record with the NEW short_uuid
|
||||
# Create user subscription record
|
||||
expires_at = datetime.utcnow() + timedelta(days=subscription.duration_days)
|
||||
|
||||
await db.create_user_subscription(
|
||||
user_id=user.telegram_id,
|
||||
subscription_id=subscription.id,
|
||||
short_uuid=short_uuid, # Это НОВЫЙ уникальный shortUuid
|
||||
short_uuid=short_uuid,
|
||||
expires_at=expires_at
|
||||
)
|
||||
|
||||
@@ -729,9 +742,29 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
status='completed'
|
||||
)
|
||||
|
||||
# Формируем сообщение с URL из API
|
||||
success_text = f"✅ Подписка успешно создана!\n\n"
|
||||
success_text += f"📋 Подписка: {subscription.name}\n"
|
||||
success_text += f"⏰ Действует до: {format_date(expires_at, user.language)}\n"
|
||||
success_text += f"💰 Стоимость: {subscription.price} руб.\n\n"
|
||||
|
||||
# Получаем subscription URL из API
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(short_uuid)
|
||||
if subscription_url:
|
||||
success_text += f"🔗 <a href='{subscription_url}'>Нажмите для подключения</a>\n\n"
|
||||
success_text += "📱 Скопируйте ссылку и импортируйте конфигурацию в ваше VPN приложение"
|
||||
else:
|
||||
success_text += "⚠️ Ссылка для подключения будет доступна в разделе 'Мои подписки'"
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL: {e}")
|
||||
success_text += "⚠️ Ссылка для подключения будет доступна в разделе 'Мои подписки'"
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('subscription_purchased', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
success_text,
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "subscription_purchased", f"Sub: {subscription.name}")
|
||||
@@ -743,10 +776,13 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
# My subscriptions
|
||||
@router.callback_query(F.data == "my_subscriptions")
|
||||
async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Show user's subscriptions"""
|
||||
"""Show user's subscriptions with URLs from API"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
@@ -761,7 +797,42 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
|
||||
)
|
||||
return
|
||||
|
||||
# Get subscription details
|
||||
text = t('your_subscriptions', user.language) + "\n\n"
|
||||
|
||||
for i, user_sub in enumerate(user_subs, 1):
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
continue
|
||||
|
||||
# Определяем статус
|
||||
now = datetime.utcnow()
|
||||
if user_sub.expires_at < now:
|
||||
status = "❌ Истекла"
|
||||
elif not user_sub.is_active:
|
||||
status = "⏸ Неактивна"
|
||||
else:
|
||||
days_left = (user_sub.expires_at - now).days
|
||||
status = f"✅ Активна ({days_left} дн.)"
|
||||
|
||||
text += f"{i}. {subscription.name}\n"
|
||||
text += f" {status}\n"
|
||||
text += f" До: {format_date(user_sub.expires_at, user.language)}\n"
|
||||
|
||||
# НОВОЕ: Получаем URL из API
|
||||
if user_sub.short_uuid and api:
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
|
||||
if subscription_url:
|
||||
text += f" 🔗 <a href='{subscription_url}'>Подключить</a>\n"
|
||||
else:
|
||||
text += f" 🔗 URL недоступен\n"
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL for {user_sub.short_uuid}: {e}")
|
||||
text += f" 🔗 URL недоступен\n"
|
||||
|
||||
text += "\n"
|
||||
|
||||
# Convert to old format for keyboard
|
||||
sub_list = []
|
||||
for user_sub in user_subs:
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
@@ -772,17 +843,22 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
|
||||
})
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('my_subscriptions', user.language),
|
||||
reply_markup=user_subscriptions_keyboard(sub_list, user.language)
|
||||
text,
|
||||
reply_markup=user_subscriptions_keyboard(sub_list, user.language),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user subscriptions: {e}")
|
||||
await callback.answer(t('error_occurred', user.language))
|
||||
|
||||
@router.callback_query(F.data.startswith("view_sub_"))
|
||||
async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""View subscription details"""
|
||||
"""View subscription details with URL from API"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
@@ -810,29 +886,38 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
'description': subscription.description or ''
|
||||
}
|
||||
|
||||
# Check if subscription is expiring soon (within 3 days)
|
||||
from datetime import datetime, timedelta
|
||||
# Check if subscription is expiring soon
|
||||
now = datetime.utcnow()
|
||||
days_until_expiry = (user_sub.expires_at - now).days
|
||||
|
||||
# ИСПРАВЛЕНИЕ: Запрещаем продление тестовых подписок
|
||||
show_extend = (0 <= days_until_expiry <= 3 and
|
||||
user_sub.is_active and
|
||||
not subscription.is_trial) # Добавили проверку на тестовую подписку
|
||||
not subscription.is_trial)
|
||||
|
||||
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
|
||||
|
||||
# Add expiry warning if subscription expires soon (but don't show for trial)
|
||||
# НОВОЕ: Добавляем URL из API в детальный просмотр
|
||||
if user_sub.short_uuid and api:
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
|
||||
if subscription_url:
|
||||
text += f"\n\n🔗 <a href='{subscription_url}'>Ссылка для подключения</a>"
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL: {e}")
|
||||
|
||||
# Add expiry warning if subscription expires soon
|
||||
if show_extend:
|
||||
text += f"\n\n⚠️ {t('subscription_expires_soon', user.language, days=days_until_expiry)}"
|
||||
elif subscription.is_trial and 0 <= days_until_expiry <= 3:
|
||||
# Специальное сообщение для тестовых подписок
|
||||
text += f"\n\n ℹ️ Тестовая подписка истекает через {days_until_expiry} дн. Продление недоступно."
|
||||
text += f"\n\nℹ️ Тестовая подписка истекает через {days_until_expiry} дн. Продление недоступно."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend)
|
||||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error viewing subscription detail: {e}")
|
||||
await callback.answer(t('error_occurred', user.language))
|
||||
@@ -849,7 +934,6 @@ async def extend_subscription_callback(callback: CallbackQuery, db: Database, **
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
# Получаем все подписки пользователя и находим нужную
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
@@ -857,18 +941,15 @@ async def extend_subscription_callback(callback: CallbackQuery, db: Database, **
|
||||
await callback.answer(t('subscription_not_found', user.language))
|
||||
return
|
||||
|
||||
# Get subscription details
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer(t('subscription_not_found', user.language))
|
||||
return
|
||||
|
||||
# Check if subscription is trial (can't extend trial)
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Тестовую подписку нельзя продлить")
|
||||
return
|
||||
|
||||
# Check if user has enough balance
|
||||
if user.balance < subscription.price:
|
||||
needed = subscription.price - user.balance
|
||||
text = f"❌ Недостаточно средств для продления!\n\n"
|
||||
@@ -904,7 +985,7 @@ async def extend_subscription_callback(callback: CallbackQuery, db: Database, **
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_extend_"))
|
||||
async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Confirm subscription extension"""
|
||||
"""Confirm subscription extension - ДОБАВЛЕНА ПОДДЕРЖКА URL ИЗ API"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
|
||||
@@ -915,7 +996,6 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
# Получаем все подписки пользователя и находим нужную
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
@@ -923,75 +1003,57 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
|
||||
await callback.answer(t('subscription_not_found', user.language))
|
||||
return
|
||||
|
||||
# Get subscription details
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer(t('subscription_not_found', user.language))
|
||||
return
|
||||
|
||||
# Check if subscription is trial (can't extend trial)
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Тестовую подписку нельзя продлить")
|
||||
return
|
||||
|
||||
# Check balance again
|
||||
if user.balance < subscription.price:
|
||||
await callback.answer("❌ Недостаточно средств")
|
||||
return
|
||||
|
||||
# Calculate new expiry date
|
||||
from datetime import datetime, timedelta
|
||||
now = datetime.utcnow()
|
||||
|
||||
# ИСПРАВЛЕНИЕ: Правильное вычисление новой даты истечения
|
||||
# Если подписка все еще активна, продлеваем от текущей даты истечения
|
||||
# Если истекла, продлеваем от текущего момента
|
||||
if user_sub.expires_at > now:
|
||||
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
|
||||
else:
|
||||
new_expiry = now + timedelta(days=subscription.duration_days)
|
||||
|
||||
# ГЛАВНОЕ ИСПРАВЛЕНИЕ: Обновляем подписку в RemnaWave с правильными полями
|
||||
# Update in RemnaWave
|
||||
if api and user_sub.short_uuid:
|
||||
try:
|
||||
logger.info(f"Updating RemnaWave subscription for shortUuid: {user_sub.short_uuid}")
|
||||
|
||||
# Сначала получаем информацию о пользователе по short_uuid
|
||||
remna_user_details = await api.get_user_by_short_uuid(user_sub.short_uuid)
|
||||
if remna_user_details:
|
||||
user_uuid = remna_user_details.get('uuid')
|
||||
if user_uuid:
|
||||
# ИСПРАВЛЕНИЕ: Используем правильное поле для даты истечения
|
||||
# В RemnaWave API может использоваться 'expireAt' или 'expiryTime'
|
||||
expiry_str = new_expiry.isoformat() + 'Z'
|
||||
|
||||
# Попробуем оба варианта поля даты истечения
|
||||
update_data_v1 = {
|
||||
update_data = {
|
||||
'enable': True,
|
||||
'expireAt': expiry_str # Вариант 1
|
||||
}
|
||||
|
||||
update_data_v2 = {
|
||||
'enable': True,
|
||||
'expiryTime': expiry_str # Вариант 2
|
||||
'expireAt': expiry_str
|
||||
}
|
||||
|
||||
logger.info(f"Updating user {user_uuid} with new expiry: {expiry_str}")
|
||||
|
||||
# Пробуем первый вариант
|
||||
result = await api.update_user(user_uuid, update_data_v1)
|
||||
result = await api.update_user(user_uuid, update_data)
|
||||
|
||||
if not result:
|
||||
# Если первый не сработал, пробуем второй
|
||||
logger.info("Trying alternative field name 'expiryTime'")
|
||||
result = await api.update_user(user_uuid, update_data_v2)
|
||||
# Try alternative field name
|
||||
update_data['expiryTime'] = expiry_str
|
||||
result = await api.update_user(user_uuid, update_data)
|
||||
|
||||
if result:
|
||||
logger.info(f"Successfully updated RemnaWave user expiry to {expiry_str}")
|
||||
logger.info(f"Successfully updated RemnaWave user expiry")
|
||||
else:
|
||||
logger.warning(f"Failed to update user in RemnaWave - trying direct API call")
|
||||
logger.warning(f"Failed to update user in RemnaWave")
|
||||
|
||||
# ДОПОЛНИТЕЛЬНАЯ ПОПЫТКА: Используем специальный метод для обновления даты истечения
|
||||
if hasattr(api, 'update_user_expiry'):
|
||||
result = await api.update_user_expiry(user_sub.short_uuid, expiry_str)
|
||||
if result:
|
||||
@@ -1003,7 +1065,6 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update expiry in RemnaWave: {e}")
|
||||
# НЕ прерываем выполнение, продолжаем обновление в локальной БД
|
||||
|
||||
# Update local database
|
||||
user_sub.expires_at = new_expiry
|
||||
@@ -1029,12 +1090,24 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
|
||||
success_text += f"💰 Списано: {subscription.price} руб.\n"
|
||||
success_text += f"💳 Остаток на балансе: {user.balance} руб."
|
||||
|
||||
# НОВОЕ: Получаем обновленный URL из API
|
||||
if api and user_sub.short_uuid:
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
|
||||
if subscription_url:
|
||||
success_text += f"\n\n🔗 <a href='{subscription_url}'>Обновленная ссылка для подключения</a>"
|
||||
success_text += f"\n📱 Можете использовать прежнюю конфигурацию или обновить по ссылке"
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get updated subscription URL: {e}")
|
||||
|
||||
await callback.message.edit_text(
|
||||
success_text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
]),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "subscription_extended", f"Sub: {subscription.name}")
|
||||
@@ -1046,13 +1119,11 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("get_connection_"))
|
||||
async def get_connection_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Get connection link"""
|
||||
"""Get connection link from API - ПОЛНОСТЬЮ ПЕРЕРАБОТАН"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
@@ -1067,73 +1138,52 @@ async def get_connection_callback(callback: CallbackQuery, db: Database, **kwarg
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
# Используем shortUuid напрямую
|
||||
short_uuid = user_sub.short_uuid
|
||||
if not user_sub.short_uuid:
|
||||
await callback.answer("❌ Данные подписки недоступны")
|
||||
return
|
||||
|
||||
# ИСПРАВЛЕНИЕ: Приоритет config.SUBSCRIPTION_BASE_URL
|
||||
if config and config.SUBSCRIPTION_BASE_URL:
|
||||
connection_url = f"{config.SUBSCRIPTION_BASE_URL.rstrip('/')}/sub/{short_uuid}"
|
||||
logger.info(f"Using config SUBSCRIPTION_BASE_URL: {connection_url}")
|
||||
elif api:
|
||||
# Если конфиг недоступен, используем API
|
||||
connection_url = await api.get_subscription_url(short_uuid)
|
||||
logger.info(f"Using API subscription URL: {connection_url}")
|
||||
else:
|
||||
# Последний fallback
|
||||
connection_url = f"{config.SUBSCRIPTION_BASE_URL.rstrip('/')}/sub/{short_uuid}"
|
||||
logger.warning(f"Using hardcoded fallback URL: {connection_url}")
|
||||
connection_url = None
|
||||
if api:
|
||||
try:
|
||||
connection_url = await api.get_subscription_url(user_sub.short_uuid)
|
||||
logger.info(f"Got subscription URL from API: {connection_url}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get URL from API: {e}")
|
||||
|
||||
text = t('connection_link', user.language, link=connection_url)
|
||||
if not connection_url:
|
||||
await callback.message.edit_text(
|
||||
"❌ Не удалось получить ссылку для подключения\n\nПопробуйте позже или обратитесь в поддержку",
|
||||
reply_markup=back_keyboard("my_subscriptions", user.language)
|
||||
)
|
||||
return
|
||||
|
||||
text = f"🔗 Ссылка для подключения готова!\n\n"
|
||||
text += f"📋 Подписка: {user_sub.id}\n"
|
||||
text += f"🔗 Ссылка: <code>{connection_url}</code>\n\n"
|
||||
text += f"📱 Инструкция:\n"
|
||||
text += f"1. Скопируйте ссылку выше\n"
|
||||
text += f"2. Откройте ваше VPN приложение\n"
|
||||
text += f"3. Добавьте конфигурацию по ссылке\n\n"
|
||||
text += f"💡 Или нажмите кнопку ниже для автоматического подключения"
|
||||
|
||||
# Создаем клавиатуру с кнопкой подключения
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔗 Подключиться", web_app=WebAppInfo(url=connection_url))],
|
||||
[InlineKeyboardButton(text=t('back', user.language), callback_data="my_subscriptions")]
|
||||
[InlineKeyboardButton(text="🚀 Подключиться автоматически", url=connection_url)],
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting connection link: {e}")
|
||||
await callback.answer(t('error_occurred', user.language))
|
||||
|
||||
@router.callback_query(F.data.startswith("connect_sub_"))
|
||||
async def connect_subscription_callback(callback: CallbackQuery, db: Database, api: RemnaWaveAPI, **kwargs):
|
||||
"""Handle subscription connection button"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
sub_id = int(callback.data.split("_")[2])
|
||||
user_sub = next((s for s in user_subs if s.id == sub_id), None)
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
# Получаем ссылку из RemnaWave API
|
||||
connection_url = await api.get_subscription_by_short_uuid(user_sub.short_uuid)
|
||||
|
||||
if not connection_url:
|
||||
# Используем config.SUBSCRIPTION_BASE_URL
|
||||
subscription_base_url = config.SUBSCRIPTION_BASE_URL if config else "http://localhost"
|
||||
connection_url = f"{subscription_base_url.rstrip('/')}/sub/{user_sub.short_uuid}"
|
||||
|
||||
# Открываем ссылку через URL кнопку
|
||||
await callback.answer(url=connection_url)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to subscription: {e}")
|
||||
await callback.answer(t('error_occurred', user.language))
|
||||
|
||||
# Support
|
||||
# Support и Promocode handlers
|
||||
@router.callback_query(F.data == "support")
|
||||
async def support_callback(callback: CallbackQuery, **kwargs):
|
||||
"""Show support info"""
|
||||
@@ -1152,7 +1202,6 @@ async def support_callback(callback: CallbackQuery, **kwargs):
|
||||
reply_markup=back_keyboard("main_menu", user.language)
|
||||
)
|
||||
|
||||
# Promocode
|
||||
@router.callback_query(F.data == "promocode")
|
||||
async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
|
||||
"""Handle promocode input"""
|
||||
|
||||
+258
-122
@@ -11,7 +11,7 @@ class RemnaWaveAPI:
|
||||
def __init__(self, base_url: str, token: str, subscription_base_url: str = None):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.token = token
|
||||
self.subscription_base_url = subscription_base_url or base_url
|
||||
self.subscription_base_url = subscription_base_url
|
||||
self.session = None
|
||||
|
||||
async def _get_session(self):
|
||||
@@ -19,7 +19,7 @@ class RemnaWaveAPI:
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json' # Явно запрашиваем JSON
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
self.session = aiohttp.ClientSession(
|
||||
@@ -77,7 +77,138 @@ class RemnaWaveAPI:
|
||||
logger.error(f"Request error for {endpoint}: {e}")
|
||||
return None
|
||||
|
||||
# User management
|
||||
async def get_subscription_info(self, short_uuid: str) -> Optional[Dict]:
|
||||
"""Get subscription info including URL from RemnaWave API"""
|
||||
try:
|
||||
logger.info(f"Getting subscription info for short_uuid: {short_uuid}")
|
||||
|
||||
# Пробуем разные эндпоинты для получения информации о подписке
|
||||
endpoints_to_try = [
|
||||
f'/api/subscriptions/{short_uuid}',
|
||||
f'/api/sub/{short_uuid}',
|
||||
f'/api/subscription/{short_uuid}'
|
||||
]
|
||||
|
||||
for endpoint in endpoints_to_try:
|
||||
logger.debug(f"Trying endpoint: {endpoint}")
|
||||
result = await self._make_request('GET', endpoint)
|
||||
|
||||
if result:
|
||||
logger.info(f"Successfully got subscription info from {endpoint}")
|
||||
|
||||
# Обрабатываем разные структуры ответа
|
||||
subscription_data = None
|
||||
|
||||
if 'response' in result:
|
||||
subscription_data = result['response']
|
||||
elif 'data' in result:
|
||||
subscription_data = result['data']
|
||||
elif 'subscription' in result:
|
||||
subscription_data = result['subscription']
|
||||
else:
|
||||
subscription_data = result
|
||||
|
||||
# Проверяем что получили нужные данные
|
||||
if subscription_data and (
|
||||
'subscriptionUrl' in subscription_data or
|
||||
'url' in subscription_data or
|
||||
'link' in subscription_data
|
||||
):
|
||||
return subscription_data
|
||||
|
||||
logger.warning(f"Could not get subscription info for {short_uuid} from any endpoint")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscription info for {short_uuid}: {e}")
|
||||
return None
|
||||
|
||||
async def get_subscription_url(self, short_uuid: str) -> str:
|
||||
"""Get subscription URL from RemnaWave API - ОБНОВЛЕННЫЙ МЕТОД"""
|
||||
try:
|
||||
logger.info(f"Getting subscription URL for short_uuid: {short_uuid}")
|
||||
|
||||
# Сначала пробуем получить URL из API
|
||||
subscription_info = await self.get_subscription_info(short_uuid)
|
||||
|
||||
if subscription_info:
|
||||
# Ищем URL в разных возможных полях
|
||||
subscription_url = (
|
||||
subscription_info.get('subscriptionUrl') or
|
||||
subscription_info.get('url') or
|
||||
subscription_info.get('link') or
|
||||
subscription_info.get('subscription_url')
|
||||
)
|
||||
|
||||
if subscription_url:
|
||||
logger.info(f"Got subscription URL from API: {subscription_url}")
|
||||
return subscription_url
|
||||
|
||||
# Если не получилось через API, пробуем через пользователя
|
||||
user_data = await self.get_user_by_short_uuid(short_uuid)
|
||||
if user_data and 'subscriptionUrl' in user_data:
|
||||
logger.info(f"Got subscription URL from user data: {user_data['subscriptionUrl']}")
|
||||
return user_data['subscriptionUrl']
|
||||
|
||||
# Последний fallback - формируем URL самостоятельно
|
||||
if self.subscription_base_url:
|
||||
fallback_url = f"{self.subscription_base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
logger.warning(f"Using fallback URL: {fallback_url}")
|
||||
return fallback_url
|
||||
else:
|
||||
# Если нет даже fallback URL, пытаемся угадать из base_url
|
||||
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
logger.warning(f"Using base_url fallback: {fallback_url}")
|
||||
return fallback_url
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get subscription URL for {short_uuid}: {e}")
|
||||
# Последний resort
|
||||
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
return fallback_url
|
||||
|
||||
async def get_all_subscriptions_with_urls(self) -> Optional[List]:
|
||||
"""Get all subscriptions with their URLs from RemnaWave API"""
|
||||
try:
|
||||
logger.info("Fetching all subscriptions with URLs from API")
|
||||
result = await self._make_request('GET', '/api/subscriptions')
|
||||
|
||||
if not result:
|
||||
logger.error("Empty response from subscriptions API")
|
||||
return []
|
||||
|
||||
subscriptions_list = []
|
||||
|
||||
# Extract subscriptions from response
|
||||
if 'response' in result and 'subscriptions' in result['response']:
|
||||
subscriptions_list = result['response']['subscriptions']
|
||||
elif 'subscriptions' in result:
|
||||
subscriptions_list = result['subscriptions']
|
||||
elif 'data' in result:
|
||||
subscriptions_list = result['data']
|
||||
elif isinstance(result, list):
|
||||
subscriptions_list = result
|
||||
|
||||
# Process subscriptions to ensure they have URLs
|
||||
processed_subscriptions = []
|
||||
for subscription in subscriptions_list:
|
||||
if subscription.get('isFound') and 'user' in subscription:
|
||||
user_data = subscription['user']
|
||||
|
||||
# Добавляем URL если его нет
|
||||
if 'subscriptionUrl' not in subscription and user_data.get('shortUuid'):
|
||||
subscription['subscriptionUrl'] = await self.get_subscription_url(user_data['shortUuid'])
|
||||
|
||||
processed_subscriptions.append(subscription)
|
||||
|
||||
logger.info(f"Processed {len(processed_subscriptions)} subscriptions with URLs")
|
||||
return processed_subscriptions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in get_all_subscriptions_with_urls: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
# User management - без изменений
|
||||
async def create_user(self, username: str, password: str = None,
|
||||
traffic_limit: int = 0, expiry_time: str = None,
|
||||
telegram_id: int = None, email: str = None,
|
||||
@@ -143,7 +274,6 @@ class RemnaWaveAPI:
|
||||
elif 'user' in result:
|
||||
user_data = result['user']
|
||||
else:
|
||||
# Если нет стандартных полей, возможно сам result - это пользователь
|
||||
if 'telegramId' in result or 'username' in result:
|
||||
user_data = result
|
||||
elif isinstance(result, list):
|
||||
@@ -155,6 +285,16 @@ class RemnaWaveAPI:
|
||||
# Проверяем что это действительно нужный пользователь
|
||||
if user_data.get('telegramId') == telegram_id:
|
||||
logger.info(f"Found user: {user_data.get('username')} for Telegram ID {telegram_id}")
|
||||
|
||||
# НОВОЕ: Обогащаем данные пользователя URL подписки если есть short_uuid
|
||||
if user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
|
||||
try:
|
||||
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
|
||||
user_data['subscriptionUrl'] = subscription_url
|
||||
logger.debug(f"Added subscription URL to user data: {subscription_url}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL for user: {e}")
|
||||
|
||||
return user_data
|
||||
else:
|
||||
logger.warning(f"Telegram ID mismatch: expected {telegram_id}, got {user_data.get('telegramId')}")
|
||||
@@ -167,11 +307,23 @@ class RemnaWaveAPI:
|
||||
result = await self._make_request('GET', f'/api/users/{uuid}')
|
||||
|
||||
if result:
|
||||
user_data = None
|
||||
if 'response' in result:
|
||||
return result['response']
|
||||
user_data = result['response']
|
||||
elif 'data' in result:
|
||||
return result['data']
|
||||
return result
|
||||
user_data = result['data']
|
||||
else:
|
||||
user_data = result
|
||||
|
||||
# Добавляем subscription URL если его нет
|
||||
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
|
||||
try:
|
||||
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
|
||||
user_data['subscriptionUrl'] = subscription_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL for user: {e}")
|
||||
|
||||
return user_data
|
||||
return None
|
||||
|
||||
async def get_user_by_short_uuid(self, short_uuid: str) -> Optional[Dict]:
|
||||
@@ -180,11 +332,23 @@ class RemnaWaveAPI:
|
||||
result = await self._make_request('GET', f'/api/users/by-short-uuid/{short_uuid}')
|
||||
|
||||
if result:
|
||||
user_data = None
|
||||
if 'response' in result:
|
||||
return result['response']
|
||||
user_data = result['response']
|
||||
elif 'data' in result:
|
||||
return result['data']
|
||||
return result
|
||||
user_data = result['data']
|
||||
else:
|
||||
user_data = result
|
||||
|
||||
# Добавляем subscription URL если его нет
|
||||
if user_data and 'subscriptionUrl' not in user_data:
|
||||
try:
|
||||
subscription_url = await self.get_subscription_url(short_uuid)
|
||||
user_data['subscriptionUrl'] = subscription_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL: {e}")
|
||||
|
||||
return user_data
|
||||
return None
|
||||
|
||||
async def update_user(self, uuid: str, data: Dict) -> Optional[Dict]:
|
||||
@@ -246,18 +410,8 @@ class RemnaWaveAPI:
|
||||
'trafficLimitBytes': traffic_bytes
|
||||
}
|
||||
return await self.update_user(uuid, update_data)
|
||||
|
||||
async def get_subscription_url(self, short_uuid: str) -> str:
|
||||
"""Get subscription URL for a given short UUID"""
|
||||
try:
|
||||
subscription_url = f"{self.subscription_base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
logger.info(f"Generated subscription URL: {subscription_url}")
|
||||
return subscription_url
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get subscription URL: {e}")
|
||||
return f"{self.subscription_base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
|
||||
# Nodes management - FIXED with proper endpoints
|
||||
# Nodes management
|
||||
async def get_all_nodes(self) -> Optional[List]:
|
||||
"""Get all nodes with proper field mapping"""
|
||||
try:
|
||||
@@ -381,7 +535,7 @@ class RemnaWaveAPI:
|
||||
"""Disable specific node"""
|
||||
return await self._make_request('POST', f'/api/nodes/{node_id}/actions/disable')
|
||||
|
||||
# System stats
|
||||
# System stats - без изменений
|
||||
async def get_system_stats(self) -> Optional[Dict]:
|
||||
"""Get system statistics with proper parsing"""
|
||||
try:
|
||||
@@ -432,13 +586,13 @@ class RemnaWaveAPI:
|
||||
return None
|
||||
|
||||
async def get_all_system_users_full(self) -> Optional[List]:
|
||||
"""Get all system users without pagination - FIXED"""
|
||||
"""Get all system users with subscription URLs - ОБНОВЛЕННЫЙ МЕТОД"""
|
||||
try:
|
||||
all_users = []
|
||||
offset = 0
|
||||
limit = 100
|
||||
|
||||
logger.info("Starting to fetch all system users")
|
||||
logger.info("Starting to fetch all system users with URLs")
|
||||
|
||||
while True:
|
||||
logger.debug(f"Fetching users batch: offset={offset}, limit={limit}")
|
||||
@@ -484,12 +638,23 @@ class RemnaWaveAPI:
|
||||
if not batch_users:
|
||||
logger.info(f"No users in batch at offset {offset}, stopping")
|
||||
break
|
||||
|
||||
# Логируем первого пользователя для отладки структуры
|
||||
if batch_users and offset == 0:
|
||||
logger.debug(f"First user structure: {list(batch_users[0].keys()) if batch_users[0] else 'empty'}")
|
||||
|
||||
all_users.extend(batch_users)
|
||||
|
||||
# НОВОЕ: Обогащаем каждого пользователя subscription URL
|
||||
enriched_users = []
|
||||
for user in batch_users:
|
||||
try:
|
||||
# Добавляем subscription URL если есть shortUuid и его еще нет
|
||||
if user.get('shortUuid') and 'subscriptionUrl' not in user:
|
||||
subscription_url = await self.get_subscription_url(user['shortUuid'])
|
||||
user['subscriptionUrl'] = subscription_url
|
||||
logger.debug(f"Added subscription URL for user {user.get('username', 'unknown')}")
|
||||
|
||||
enriched_users.append(user)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not enrich user {user.get('username', 'unknown')} with URL: {e}")
|
||||
enriched_users.append(user) # Добавляем пользователя даже без URL
|
||||
|
||||
all_users.extend(enriched_users)
|
||||
|
||||
# Проверяем условия остановки
|
||||
if len(batch_users) < limit:
|
||||
@@ -511,7 +676,8 @@ class RemnaWaveAPI:
|
||||
# Финальная статистика
|
||||
if all_users:
|
||||
active_users = len([u for u in all_users if str(u.get('status', '')).upper() == 'ACTIVE'])
|
||||
logger.info(f"Successfully fetched {len(all_users)} users (Active: {active_users})")
|
||||
users_with_urls = len([u for u in all_users if u.get('subscriptionUrl')])
|
||||
logger.info(f"Successfully fetched {len(all_users)} users (Active: {active_users}, With URLs: {users_with_urls})")
|
||||
else:
|
||||
logger.warning("No users found in system")
|
||||
|
||||
@@ -521,6 +687,9 @@ class RemnaWaveAPI:
|
||||
logger.error(f"Error getting all system users: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
# Остальные методы остаются без изменений...
|
||||
# (Internal squads, user search, user actions, bulk operations, debug methods, health check, etc.)
|
||||
|
||||
# Internal squads
|
||||
async def get_internal_squads_list(self) -> Optional[List[Dict]]:
|
||||
"""Get list of internal squads"""
|
||||
@@ -561,7 +730,6 @@ class RemnaWaveAPI:
|
||||
return 0
|
||||
|
||||
async def debug_users_api(self) -> Dict:
|
||||
"""Debug method to check users API response structure"""
|
||||
try:
|
||||
logger.info("=== DEBUGGING USERS API ===")
|
||||
|
||||
@@ -631,33 +799,69 @@ class RemnaWaveAPI:
|
||||
"""Get user by username"""
|
||||
result = await self._make_request('GET', f'/api/users/by-username/{username}')
|
||||
if result:
|
||||
user_data = None
|
||||
if 'response' in result:
|
||||
return result['response']
|
||||
user_data = result['response']
|
||||
elif 'data' in result:
|
||||
return result['data']
|
||||
return result
|
||||
user_data = result['data']
|
||||
else:
|
||||
user_data = result
|
||||
|
||||
# Добавляем subscription URL если его нет
|
||||
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
|
||||
try:
|
||||
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
|
||||
user_data['subscriptionUrl'] = subscription_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL: {e}")
|
||||
|
||||
return user_data
|
||||
return None
|
||||
|
||||
async def get_user_by_email(self, email: str) -> Optional[Dict]:
|
||||
"""Get user by email"""
|
||||
result = await self._make_request('GET', f'/api/users/by-email/{email}')
|
||||
if result:
|
||||
user_data = None
|
||||
if 'response' in result:
|
||||
return result['response']
|
||||
user_data = result['response']
|
||||
elif 'data' in result:
|
||||
return result['data']
|
||||
return result
|
||||
user_data = result['data']
|
||||
else:
|
||||
user_data = result
|
||||
|
||||
# Добавляем subscription URL если его нет
|
||||
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
|
||||
try:
|
||||
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
|
||||
user_data['subscriptionUrl'] = subscription_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL: {e}")
|
||||
|
||||
return user_data
|
||||
return None
|
||||
|
||||
async def get_user_by_tag(self, tag: str) -> Optional[Dict]:
|
||||
"""Get user by tag"""
|
||||
result = await self._make_request('GET', f'/api/users/by-tag/{tag}')
|
||||
if result:
|
||||
user_data = None
|
||||
if 'response' in result:
|
||||
return result['response']
|
||||
user_data = result['response']
|
||||
elif 'data' in result:
|
||||
return result['data']
|
||||
return result
|
||||
user_data = result['data']
|
||||
else:
|
||||
user_data = result
|
||||
|
||||
# Добавляем subscription URL если его нет
|
||||
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
|
||||
try:
|
||||
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
|
||||
user_data['subscriptionUrl'] = subscription_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL: {e}")
|
||||
|
||||
return user_data
|
||||
return None
|
||||
|
||||
# User actions
|
||||
@@ -821,8 +1025,9 @@ class RemnaWaveAPI:
|
||||
logger.error(f"Error getting nodes statistics: {e}")
|
||||
return {'data': []}
|
||||
|
||||
# ОБНОВЛЯЕМ метод get_all_subscriptions чтобы правильно обрабатывать URLs
|
||||
async def get_all_subscriptions(self) -> Optional[List]:
|
||||
"""Get all subscriptions from RemnaWave"""
|
||||
"""Get all subscriptions from RemnaWave - ОБНОВЛЕННАЯ ВЕРСИЯ"""
|
||||
try:
|
||||
logger.info("Fetching all subscriptions from API")
|
||||
result = await self._make_request('GET', '/api/subscriptions')
|
||||
@@ -859,9 +1064,19 @@ class RemnaWaveAPI:
|
||||
'trafficUsed': user_data.get('trafficUsed', '0'),
|
||||
'trafficLimit': user_data.get('trafficLimit', '0'),
|
||||
'daysLeft': user_data.get('daysLeft', 0),
|
||||
'subscriptionUrl': subscription.get('subscriptionUrl'),
|
||||
# ВАЖНО: Используем subscriptionUrl из самой подписки, а не генерируем
|
||||
'subscriptionUrl': subscription.get('subscriptionUrl') or subscription.get('url'),
|
||||
'links': subscription.get('links', [])
|
||||
}
|
||||
|
||||
# Если нет URL в подписке, пытаемся получить его
|
||||
if not processed_user.get('subscriptionUrl') and processed_user.get('shortUuid'):
|
||||
try:
|
||||
subscription_url = await self.get_subscription_url(processed_user['shortUuid'])
|
||||
processed_user['subscriptionUrl'] = subscription_url
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL for {processed_user.get('username')}: {e}")
|
||||
|
||||
processed_users.append(processed_user)
|
||||
|
||||
logger.info(f"Processed {len(processed_users)} active subscriptions")
|
||||
@@ -871,85 +1086,6 @@ class RemnaWaveAPI:
|
||||
logger.error(f"Exception in get_all_subscriptions: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def get_all_system_users_full(self) -> Optional[List]:
|
||||
"""Get all system users - updated to use subscriptions API"""
|
||||
try:
|
||||
# Сначала пробуем получить через API пользователей
|
||||
logger.info("Trying to get users via /api/users")
|
||||
users_result = await self._get_users_paginated()
|
||||
|
||||
if users_result:
|
||||
logger.info(f"Got {len(users_result)} users from users API")
|
||||
return users_result
|
||||
|
||||
# Если не получилось, пробуем через подписки
|
||||
logger.info("Fallback to subscriptions API")
|
||||
subscriptions_users = await self.get_all_subscriptions()
|
||||
|
||||
if subscriptions_users:
|
||||
logger.info(f"Got {len(subscriptions_users)} users from subscriptions API")
|
||||
return subscriptions_users
|
||||
|
||||
logger.warning("No users found via any API endpoint")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all system users: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def _get_users_paginated(self) -> Optional[List]:
|
||||
"""Get users via paginated API calls"""
|
||||
try:
|
||||
all_users = []
|
||||
offset = 0
|
||||
limit = 100
|
||||
|
||||
while True:
|
||||
logger.debug(f"Fetching users batch: offset={offset}, limit={limit}")
|
||||
result = await self._make_request('GET', '/api/users',
|
||||
params={'offset': offset, 'limit': limit})
|
||||
|
||||
if not result:
|
||||
break
|
||||
|
||||
batch_users = []
|
||||
|
||||
# Extract users from various response structures
|
||||
if isinstance(result, dict):
|
||||
if 'users' in result:
|
||||
batch_users = result['users'] if isinstance(result['users'], list) else []
|
||||
elif 'data' in result:
|
||||
batch_users = result['data'] if isinstance(result['data'], list) else []
|
||||
elif 'response' in result:
|
||||
if isinstance(result['response'], dict):
|
||||
if 'users' in result['response']:
|
||||
batch_users = result['response']['users'] if isinstance(result['response']['users'], list) else []
|
||||
elif 'data' in result['response']:
|
||||
batch_users = result['response']['data'] if isinstance(result['response']['data'], list) else []
|
||||
elif isinstance(result['response'], list):
|
||||
batch_users = result['response']
|
||||
elif isinstance(result, list):
|
||||
batch_users = result
|
||||
|
||||
if not batch_users:
|
||||
break
|
||||
|
||||
all_users.extend(batch_users)
|
||||
|
||||
if len(batch_users) < limit:
|
||||
break
|
||||
|
||||
offset += limit
|
||||
|
||||
if offset > 10000: # Safety limit
|
||||
break
|
||||
|
||||
return all_users if all_users else None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in _get_users_paginated: {e}")
|
||||
return None
|
||||
|
||||
async def bulk_reset_all_traffic(self) -> Optional[Dict]:
|
||||
"""Reset traffic for all users using the correct API endpoint"""
|
||||
try:
|
||||
|
||||
@@ -64,6 +64,7 @@ TRANSLATIONS = {
|
||||
'subscription_expires': 'Истекает: {date}',
|
||||
'subscription_expired': 'Истекла',
|
||||
'subscription_active': 'Активна до: {date}',
|
||||
'your_subscriptions': '👀 Список ваших подписок:',
|
||||
|
||||
# Payments
|
||||
'payment_created': '✅ Заявка на пополнение создана.\nОбратитесь к @{support} для оплаты.',
|
||||
|
||||
@@ -205,9 +205,10 @@ def format_bytes(bytes_value: int) -> str:
|
||||
else:
|
||||
return f"{value:.1f} {units[unit_index]}"
|
||||
|
||||
def get_subscription_connection_url(base_url: str, short_uuid: str) -> str:
|
||||
"""Generate subscription connection URL"""
|
||||
return f"{base_url.rstrip('/')}/api/sub/{short_uuid}"
|
||||
# УДАЛЯЕМ функцию get_subscription_connection_url - теперь URL берется из API
|
||||
# def get_subscription_connection_url(base_url: str, short_uuid: str) -> str:
|
||||
# """Generate subscription connection URL"""
|
||||
# return f"{base_url.rstrip('/')}/api/sub/{short_uuid}"
|
||||
|
||||
def log_user_action(telegram_id: int, action: str, details: str = None):
|
||||
"""Log user action for audit"""
|
||||
@@ -260,6 +261,43 @@ def is_subscription_expiring_soon(expires_at: datetime, warning_days: int = 2) -
|
||||
days_left = calculate_days_until_expiry(expires_at)
|
||||
return days_left <= warning_days
|
||||
|
||||
# НОВАЯ ФУНКЦИЯ: Извлечение subscription URL из данных пользователя
|
||||
def extract_subscription_url(user_data: Dict[str, Any]) -> Optional[str]:
|
||||
"""Extract subscription URL from user data with fallback logic"""
|
||||
if not user_data:
|
||||
return None
|
||||
|
||||
# Пробуем разные возможные поля для URL
|
||||
url_fields = [
|
||||
'subscriptionUrl',
|
||||
'subscription_url',
|
||||
'url',
|
||||
'link',
|
||||
'connectionUrl',
|
||||
'connection_url'
|
||||
]
|
||||
|
||||
for field in url_fields:
|
||||
url = user_data.get(field)
|
||||
if url and isinstance(url, str) and url.strip():
|
||||
return url.strip()
|
||||
|
||||
return None
|
||||
|
||||
def format_subscription_url_display(subscription_url: str, lang: str = 'ru') -> str:
|
||||
"""Format subscription URL for display in messages"""
|
||||
if not subscription_url:
|
||||
return "❌ URL недоступен" if lang == 'ru' else "❌ URL unavailable"
|
||||
|
||||
# Показываем только домен для безопасности
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(subscription_url)
|
||||
domain = parsed.netloc or parsed.path.split('/')[0]
|
||||
return f"🔗 {domain}"
|
||||
except Exception:
|
||||
return "🔗 Ссылка готова" if lang == 'ru' else "🔗 Link ready"
|
||||
|
||||
class States:
|
||||
"""State constants for FSM"""
|
||||
WAITING_LANGUAGE = "waiting_language"
|
||||
|
||||
Reference in New Issue
Block a user