Files
remnawave-bedolaga-telegram…/remnawave_api.py
2025-08-09 06:59:36 +03:00

1155 lines
51 KiB
Python

import aiohttp
import asyncio
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
import json
logger = logging.getLogger(__name__)
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
self.session = None
async def _get_session(self):
if self.session is None or self.session.closed:
headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
timeout = aiohttp.ClientTimeout(total=30)
self.session = aiohttp.ClientSession(
headers=headers,
timeout=timeout
)
return self.session
async def close(self):
if self.session and not self.session.closed:
await self.session.close()
async def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None, params: Optional[Dict] = None) -> Optional[Dict]:
if not endpoint.startswith('/api/'):
endpoint = '/api' + endpoint
url = f"{self.base_url}{endpoint}"
session = await self._get_session()
try:
logger.debug(f"Making {method} request to {url}")
async with session.request(method, url, json=data, params=params) as response:
content_type = response.headers.get('Content-Type', '')
logger.debug(f"Response Content-Type: {content_type}, Status: {response.status}")
response_text = await response.text()
if 'text/html' in content_type:
logger.error(f"Got HTML response instead of JSON from {url}")
logger.debug(f"HTML Response (first 500 chars): {response_text[:500]}")
return None
if response.status in [200, 201]:
if response_text:
try:
return await response.json()
except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON from {url}: {e}")
logger.debug(f"Raw response: {response_text[:500]}")
return None
return None
elif response.status == 404:
logger.warning(f"API 404 for {endpoint}")
return None
else:
logger.error(f"API error: {response.status}, {response_text[:200]}")
return None
except aiohttp.ContentTypeError as e:
logger.error(f"Content type error for {endpoint}: {e}")
return None
except Exception as e:
logger.error(f"Request error for {endpoint}: {e}")
return None
async def get_subscription_info(self, short_uuid: str) -> Optional[Dict]:
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:
try:
logger.info(f"Getting subscription URL for short_uuid: {short_uuid}")
subscription_info = await self.get_subscription_info(short_uuid)
if subscription_info:
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
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']
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 = 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}")
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
return fallback_url
async def get_all_subscriptions_with_urls(self) -> Optional[List]:
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 = []
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
processed_subscriptions = []
for subscription in subscriptions_list:
if subscription.get('isFound') and 'user' in subscription:
user_data = subscription['user']
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 []
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,
internal_squads: List[str] = None, activeInternalSquads: List[str] = None):
if expiry_time is None:
expiry_time = (datetime.now() + timedelta(days=30)).isoformat() + 'Z'
data = {
'username': username,
'trafficLimitBytes': traffic_limit,
'expireAt': expiry_time,
'status': 'ACTIVE'
}
if password:
data['trojanPassword'] = password
if telegram_id:
data['telegramId'] = telegram_id
if email:
data['email'] = email
if internal_squads:
data['internalSquads'] = internal_squads
if activeInternalSquads:
data['activeInternalSquads'] = activeInternalSquads
result = await self._make_request('POST', '/api/users', data)
if result and 'response' in result:
return {'data': result['response']}
return result
async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[Dict]:
logger.debug(f"Getting user by Telegram ID: {telegram_id}")
result = await self._make_request('GET', f'/api/users/by-telegram-id/{telegram_id}')
if not result:
logger.debug(f"No result for Telegram ID {telegram_id}")
return None
logger.debug(f"Raw API response type: {type(result)}")
logger.debug(f"Raw API response: {result}")
user_data = None
if isinstance(result, dict):
if 'response' in result:
response_data = result['response']
if isinstance(response_data, dict):
user_data = response_data
elif isinstance(response_data, list) and response_data:
user_data = response_data[0] # Берем первого пользователя
elif 'data' in result:
data = result['data']
if isinstance(data, dict):
user_data = data
elif isinstance(data, list) and data:
user_data = data[0]
elif 'user' in result:
user_data = result['user']
else:
if 'telegramId' in result or 'username' in result:
user_data = result
elif isinstance(result, list):
if result:
user_data = result[0]
if user_data and isinstance(user_data, dict):
if user_data.get('telegramId') == telegram_id:
logger.info(f"Found user: {user_data.get('username')} for Telegram ID {telegram_id}")
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')}")
logger.warning(f"User with Telegram ID {telegram_id} not found or invalid response structure")
return None
async def get_user_by_uuid(self, uuid: str) -> Optional[Dict]:
logger.debug(f"Getting user by UUID: {uuid}")
result = await self._make_request('GET', f'/api/users/{uuid}')
if result:
user_data = None
if 'response' in result:
user_data = result['response']
elif 'data' in result:
user_data = result['data']
else:
user_data = result
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]:
logger.debug(f"Getting user by short UUID: {short_uuid}")
result = await self._make_request('GET', f'/api/users/by-short-uuid/{short_uuid}')
if result:
user_data = None
if 'response' in result:
user_data = result['response']
elif 'data' in result:
user_data = result['data']
else:
user_data = result
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]:
update_data = {'uuid': uuid}
if 'enable' in data:
update_data['status'] = 'ACTIVE' if data['enable'] else 'DISABLED'
if 'expireAt' in data:
update_data['expireAt'] = data['expireAt']
if 'expiryTime' in data:
update_data['expireAt'] = data['expiryTime']
if 'trafficLimitBytes' in data:
update_data['trafficLimitBytes'] = data['trafficLimitBytes']
if 'status' in data:
update_data['status'] = data['status']
for key in ['activeInternalSquads', 'telegramId', 'email']:
if key in data:
update_data[key] = data[key]
logger.debug(f"Updating user {uuid} with data: {update_data}")
result = await self._make_request('PATCH', '/api/users', update_data)
logger.debug(f"Update user result: {result}")
return result
async def update_user_expiry(self, short_uuid: str, new_expiry: str) -> Optional[Dict]:
try:
user_data = await self.get_user_by_short_uuid(short_uuid)
if not user_data:
logger.error(f"Could not find user with short UUID: {short_uuid}")
return None
user_uuid = user_data.get('uuid')
if not user_uuid:
logger.error(f"Could not get UUID from user data")
return None
update_data = {
'status': 'ACTIVE',
'expireAt': new_expiry
}
logger.info(f"Updating user {user_uuid} expiry to {new_expiry}")
return await self.update_user(user_uuid, update_data)
except Exception as e:
logger.error(f"Exception in update_user_expiry: {e}")
return None
async def update_user_traffic_limit(self, uuid: str, traffic_limit_gb: int) -> Optional[Dict]:
traffic_bytes = traffic_limit_gb * 1024 * 1024 * 1024 if traffic_limit_gb > 0 else 0
update_data = {
'trafficLimitBytes': traffic_bytes
}
return await self.update_user(uuid, update_data)
async def get_all_nodes(self) -> Optional[List]:
try:
logger.debug("Requesting nodes from /api/nodes")
result = await self._make_request('GET', '/api/nodes')
if not result:
logger.error("Empty response from nodes API")
return []
nodes_list = []
if 'response' in result:
nodes_list = result['response'] if isinstance(result['response'], list) else []
elif 'data' in result:
nodes_list = result['data'] if isinstance(result['data'], list) else []
elif isinstance(result, list):
nodes_list = result
processed_nodes = []
for node in nodes_list:
processed_node = {
'id': node.get('uuid', node.get('id')),
'uuid': node.get('uuid', node.get('id')),
'name': node.get('name', 'Unknown Node'),
'address': node.get('address', node.get('url', '')),
'status': self._determine_node_status(node),
'isConnected': node.get('isConnected', False),
'isDisabled': node.get('isDisabled', True),
'isNodeOnline': node.get('isNodeOnline', False),
'isXrayRunning': node.get('isXrayRunning', False),
'cpuUsage': node.get('cpuUsage', 0),
'memUsage': node.get('memUsage', 0),
'usersCount': node.get('usersOnline', node.get('usersCount', 0)),
'xrayVersion': node.get('xrayVersion'),
'nodeVersion': node.get('nodeVersion'),
'xrayUptime': node.get('xrayUptime'),
'cpuModel': node.get('cpuModel'),
'totalRam': node.get('totalRam'),
'trafficUsedBytes': node.get('trafficUsedBytes', 0),
'viewPosition': node.get('viewPosition'),
'countryCode': node.get('countryCode'),
'region': node.get('region'),
'city': node.get('city'),
'provider': node.get('provider'),
}
processed_nodes.append(processed_node)
logger.info(f"Processed {len(processed_nodes)} nodes with enhanced info")
return processed_nodes
except Exception as e:
logger.error(f"Exception in get_all_nodes: {e}", exc_info=True)
return []
def _determine_node_status(self, node: Dict) -> str:
is_connected = node.get('isConnected', False)
is_disabled = node.get('isDisabled', False)
is_node_online = node.get('isNodeOnline', False)
is_xray_running = node.get('isXrayRunning', False)
logger.debug(f"Node {node.get('name', 'unknown')}: connected={is_connected}, disabled={is_disabled}, online={is_node_online}, xray={is_xray_running}")
if is_disabled:
return 'disabled'
elif not is_connected:
return 'disconnected'
elif is_connected and is_node_online and is_xray_running:
return 'online'
elif is_connected and is_node_online and not is_xray_running:
return 'xray_stopped'
else:
return 'offline'
async def restart_node(self, node_id: str) -> Optional[Dict]:
try:
logger.info(f"Attempting to restart node: {node_id}")
result = await self._make_request('POST', f'/api/nodes/{node_id}/actions/restart')
if result:
logger.info(f"Successfully sent restart command for node {node_id}")
return result
logger.error(f"Failed to restart node {node_id}")
return None
except Exception as e:
logger.error(f"Error restarting node {node_id}: {e}")
return None
async def restart_all_nodes(self) -> Optional[Dict]:
try:
logger.info("Attempting to restart all nodes")
nodes = await self.get_all_nodes()
if not nodes:
return None
success_count = 0
for node in nodes:
node_id = node.get('uuid', node.get('id'))
if node_id:
result = await self.restart_node(node_id)
if result:
success_count += 1
await asyncio.sleep(0.5)
return {
'success': success_count > 0,
'restarted_nodes': success_count,
'total_nodes': len(nodes)
}
except Exception as e:
logger.error(f"Error restarting all nodes: {e}")
return None
async def enable_node(self, node_id: str) -> Optional[Dict]:
return await self._make_request('POST', f'/api/nodes/{node_id}/actions/enable')
async def disable_node(self, node_id: str) -> Optional[Dict]:
return await self._make_request('POST', f'/api/nodes/{node_id}/actions/disable')
async def get_system_stats(self) -> Optional[Dict]:
try:
logger.info("Fetching enhanced system stats from /api/system/stats...")
system_result = await self._make_request('GET', '/api/system/stats')
if not system_result or 'response' not in system_result:
logger.error("Invalid response from /api/system/stats")
return None
system_data = system_result['response']
logger.info(f"System stats received: {list(system_data.keys())}")
users_stats = system_data.get('users', {})
status_counts = users_stats.get('statusCounts', {})
total_users = users_stats.get('totalUsers', 0)
active_users = status_counts.get('ACTIVE', 0)
disabled_users = status_counts.get('DISABLED', 0)
limited_users = status_counts.get('LIMITED', 0)
expired_users = status_counts.get('EXPIRED', 0)
online_stats = system_data.get('onlineStats', {})
online_now = online_stats.get('onlineNow', 0)
last_day = online_stats.get('lastDay', 0)
last_week = online_stats.get('lastWeek', 0)
never_online = online_stats.get('neverOnline', 0)
cpu_info = system_data.get('cpu', {})
memory_info = system_data.get('memory', {})
def bytes_to_gb(bytes_value):
return round(bytes_value / (1024**3), 2) if bytes_value else 0
memory_total_gb = bytes_to_gb(memory_info.get('total', 0))
memory_active_gb = bytes_to_gb(memory_info.get('active', 0))
memory_available_gb = bytes_to_gb(memory_info.get('available', 0))
memory_free_gb = bytes_to_gb(memory_info.get('free', 0))
all_nodes = await self.get_all_nodes()
total_nodes = len(all_nodes) if all_nodes else 0
nodes_online = 0
if all_nodes:
nodes_online = len([n for n in all_nodes if n.get('status') == 'online'])
if total_nodes == 0:
nodes_info = system_data.get('nodes', {})
nodes_online = nodes_info.get('totalOnline', 0)
total_nodes = nodes_online
bandwidth_result = await self._make_request('GET', '/api/system/stats/bandwidth')
enhanced_bandwidth = {}
if bandwidth_result and 'response' in bandwidth_result:
enhanced_bandwidth = bandwidth_result['response']
logger.info(f"Enhanced bandwidth stats received")
stats = {
'users': active_users,
'active_users': active_users,
'total_users': total_users,
'disabled_users': disabled_users,
'limited_users': limited_users,
'expired_users': expired_users,
'online_stats': {
'online_now': online_now,
'last_day': last_day,
'last_week': last_week,
'never_online': never_online
},
'system_resources': {
'cpu': {
'cores': cpu_info.get('cores', 0),
'physical_cores': cpu_info.get('physicalCores', 0)
},
'memory': {
'total_gb': memory_total_gb,
'active_gb': memory_active_gb,
'available_gb': memory_available_gb,
'free_gb': memory_free_gb,
'usage_percent': round((memory_active_gb / memory_total_gb * 100), 1) if memory_total_gb > 0 else 0
},
'uptime': system_data.get('uptime', 0)
},
'nodes': {
'total': total_nodes,
'online': nodes_online,
'offline': total_nodes - nodes_online
},
'bandwidth': enhanced_bandwidth,
'total_traffic_bytes': users_stats.get('totalTrafficBytes', '0')
}
logger.info(f"Enhanced system stats compiled successfully")
logger.info(f"Users: {total_users} total, {active_users} active, {online_now} online now")
logger.info(f"Memory: {memory_active_gb}/{memory_total_gb} GB active, {memory_available_gb} GB available")
logger.info(f"Nodes: {nodes_online}/{total_nodes} online")
return stats
except Exception as e:
logger.error(f"Error getting enhanced system stats: {e}", exc_info=True)
return None
async def get_bandwidth_stats(self) -> Optional[Dict]:
try:
logger.info("Fetching detailed bandwidth statistics")
result = await self._make_request('GET', '/api/system/stats/bandwidth')
if result and 'response' in result:
return result['response']
return None
except Exception as e:
logger.error(f"Error getting bandwidth stats: {e}")
return None
async def get_node_detailed_info(self, node_id: str) -> Optional[Dict]:
try:
logger.info(f"Getting detailed info for node: {node_id}")
result = await self._make_request('GET', f'/api/nodes/{node_id}')
if result:
node_data = None
if 'response' in result:
node_data = result['response']
elif 'data' in result:
node_data = result['data']
else:
node_data = result
if node_data:
enhanced_node = {
'id': node_data.get('uuid', node_data.get('id')),
'uuid': node_data.get('uuid', node_data.get('id')),
'name': node_data.get('name', 'Unknown Node'),
'address': node_data.get('address', ''),
'status': self._determine_node_status(node_data),
'isConnected': node_data.get('isConnected', False),
'isDisabled': node_data.get('isDisabled', True),
'isNodeOnline': node_data.get('isNodeOnline', False),
'isXrayRunning': node_data.get('isXrayRunning', False),
'cpuUsage': node_data.get('cpuUsage', 0),
'memUsage': node_data.get('memUsage', 0),
'usersCount': node_data.get('usersOnline', node_data.get('usersCount', 0)),
'xrayVersion': node_data.get('xrayVersion'),
'nodeVersion': node_data.get('nodeVersion'),
'xrayUptime': node_data.get('xrayUptime'),
'cpuModel': node_data.get('cpuModel'),
'totalRam': node_data.get('totalRam'),
'trafficUsedBytes': node_data.get('trafficUsedBytes', 0),
'viewPosition': node_data.get('viewPosition'),
'countryCode': node_data.get('countryCode'),
'region': node_data.get('region'),
'city': node_data.get('city'),
'provider': node_data.get('provider'),
}
return enhanced_node
return None
except Exception as e:
logger.error(f"Error getting detailed node info: {e}")
return None
async def get_all_system_users_full(self) -> Optional[List]:
try:
all_users = []
offset = 0
limit = 100
logger.info("Starting to fetch all system users with URLs")
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:
logger.warning(f"Empty result at offset {offset}")
break
logger.debug(f"Raw API response structure: {list(result.keys()) if isinstance(result, dict) else 'not dict'}")
batch_users = []
total_count = None
if isinstance(result, dict):
if 'total' in result:
total_count = result['total']
logger.info(f"Total users in system: {total_count}")
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 'items' in result:
batch_users = result['items'] if isinstance(result['items'], list) else []
elif isinstance(result, list):
batch_users = result
logger.info(f"Found {len(batch_users)} users in batch at offset {offset}")
if not batch_users:
logger.info(f"No users in batch at offset {offset}, stopping")
break
enriched_users = []
for user in batch_users:
try:
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)
all_users.extend(enriched_users)
if len(batch_users) < limit:
logger.info(f"Last batch (got {len(batch_users)} < {limit})")
break
if total_count and len(all_users) >= total_count:
logger.info(f"Reached total count: {total_count}")
break
offset += limit
if offset > 10000:
logger.warning("Offset limit reached, stopping")
break
if all_users:
active_users = len([u for u in all_users if str(u.get('status', '')).upper() == 'ACTIVE'])
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")
return all_users
except Exception as e:
logger.error(f"Error getting all system users: {e}", exc_info=True)
return []
async def get_internal_squads_list(self) -> Optional[List[Dict]]:
logger.info("Fetching internal squads list")
result = await self._make_request('GET', '/api/internal-squads')
if result:
if 'response' in result:
if 'internalSquads' in result['response']:
return result['response']['internalSquads']
return result['response'] if isinstance(result['response'], list) else []
elif 'data' in result:
return result['data'] if isinstance(result['data'], list) else []
elif isinstance(result, list):
return result
return []
async def get_users_count(self) -> Optional[int]:
try:
result = await self._make_request('GET', '/api/users', params={'limit': 1})
if result:
if 'total' in result:
return result['total']
elif 'totalCount' in result:
return result['totalCount']
elif 'count' in result:
return result['count']
all_users = await self.get_all_system_users_full()
return len(all_users) if all_users else 0
except Exception as e:
logger.error(f"Error getting users count: {e}")
return 0
async def debug_users_api(self) -> Dict:
try:
logger.info("=== DEBUGGING USERS API ===")
result = await self._make_request('GET', '/api/users', params={'limit': 1, 'offset': 0})
debug_info = {
'api_response_type': type(result).__name__,
'api_response_keys': list(result.keys()) if isinstance(result, dict) else None,
'has_users': False,
'users_location': None,
'first_user_structure': None,
'total_count': None
}
if isinstance(result, dict):
if 'users' in result:
debug_info['users_location'] = 'root.users'
debug_info['has_users'] = True
if isinstance(result['users'], list) and result['users']:
debug_info['first_user_structure'] = list(result['users'][0].keys())
elif 'data' in result:
debug_info['users_location'] = 'root.data'
debug_info['has_users'] = True
if isinstance(result['data'], list) and result['data']:
debug_info['first_user_structure'] = list(result['data'][0].keys())
elif 'response' in result:
if isinstance(result['response'], dict):
if 'users' in result['response']:
debug_info['users_location'] = 'root.response.users'
debug_info['has_users'] = True
if isinstance(result['response']['users'], list) and result['response']['users']:
debug_info['first_user_structure'] = list(result['response']['users'][0].keys())
elif 'data' in result['response']:
debug_info['users_location'] = 'root.response.data'
debug_info['has_users'] = True
if isinstance(result['response']['data'], list) and result['response']['data']:
debug_info['first_user_structure'] = list(result['response']['data'][0].keys())
elif isinstance(result['response'], list):
debug_info['users_location'] = 'root.response (list)'
debug_info['has_users'] = True
if result['response']:
debug_info['first_user_structure'] = list(result['response'][0].keys())
for key in ['total', 'totalCount', 'count', 'totalUsers']:
if key in result:
debug_info['total_count'] = result[key]
debug_info['total_count_field'] = key
break
elif isinstance(result, list):
debug_info['users_location'] = 'root (list)'
debug_info['has_users'] = True
if result:
debug_info['first_user_structure'] = list(result[0].keys())
logger.info(f"Debug info: {debug_info}")
return debug_info
except Exception as e:
logger.error(f"Error in debug_users_api: {e}")
return {'error': str(e)}
async def get_user_by_username(self, username: str) -> Optional[Dict]:
result = await self._make_request('GET', f'/api/users/by-username/{username}')
if result:
user_data = None
if 'response' in result:
user_data = result['response']
elif 'data' in result:
user_data = result['data']
else:
user_data = result
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]:
result = await self._make_request('GET', f'/api/users/by-email/{email}')
if result:
user_data = None
if 'response' in result:
user_data = result['response']
elif 'data' in result:
user_data = result['data']
else:
user_data = result
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]:
result = await self._make_request('GET', f'/api/users/by-tag/{tag}')
if result:
user_data = None
if 'response' in result:
user_data = result['response']
elif 'data' in result:
user_data = result['data']
else:
user_data = result
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 disable_user(self, uuid: str) -> Optional[Dict]:
result = await self._make_request('POST', f'/api/users/{uuid}/actions/disable')
if not result:
return await self.update_user(uuid, {'status': 'DISABLED'})
return result
async def enable_user(self, uuid: str) -> Optional[Dict]:
result = await self._make_request('POST', f'/api/users/{uuid}/actions/enable')
if not result:
return await self.update_user(uuid, {'status': 'ACTIVE'})
return result
async def reset_user_traffic(self, uuid: str) -> Optional[Dict]:
return await self._make_request('POST', f'/api/users/{uuid}/actions/reset-traffic')
async def revoke_user_subscription(self, uuid: str) -> Optional[Dict]:
return await self._make_request('POST', f'/api/users/{uuid}/actions/revoke')
async def bulk_reset_traffic(self, user_uuids: List[str]) -> Optional[Dict]:
data = {'uuids': user_uuids}
return await self._make_request('POST', '/api/users/bulk/reset-traffic', data)
async def bulk_update_users(self, user_uuids: List[str], fields: Dict) -> Optional[Dict]:
data = {
'uuids': user_uuids,
'fields': fields
}
return await self._make_request('POST', '/api/users/bulk/update', data)
async def bulk_delete_users(self, user_uuids: List[str]) -> Optional[Dict]:
data = {'uuids': user_uuids}
return await self._make_request('POST', '/api/users/bulk/delete', data)
async def debug_api_response(self, endpoint: str, method: str = 'GET', data: Optional[Dict] = None) -> Dict:
try:
if not endpoint.startswith('/api/'):
endpoint = '/api' + endpoint if not endpoint.startswith('/') else '/api/' + endpoint
url = f"{self.base_url}{endpoint}"
session = await self._get_session()
logger.info(f"DEBUG: Making {method} request to {url}")
if data:
logger.info(f"DEBUG: Request data: {data}")
async with session.request(method, url, json=data) as response:
response_text = await response.text()
headers = dict(response.headers)
content_type = headers.get('Content-Type', '')
debug_info = {
'status': response.status,
'headers': headers,
'content_type': content_type,
'raw_text': response_text[:500] + '...' if len(response_text) > 500 else response_text,
'url': url,
'method': method,
'success': response.status in [200, 201] and 'application/json' in content_type
}
logger.info(f"DEBUG API Response: Status={response.status}, Content-Type={content_type}, Length={len(response_text)}")
if 'application/json' in content_type:
try:
json_data = json.loads(response_text) if response_text else None
debug_info['json'] = json_data
debug_info['parsed_successfully'] = True
if isinstance(json_data, dict):
debug_info['response_keys'] = list(json_data.keys())
if 'data' in json_data:
debug_info['data_type'] = type(json_data['data']).__name__
if isinstance(json_data['data'], list):
debug_info['data_count'] = len(json_data['data'])
elif isinstance(json_data, list):
debug_info['data_type'] = 'list'
debug_info['data_count'] = len(json_data)
except Exception as parse_error:
debug_info['json'] = None
debug_info['parsed_successfully'] = False
debug_info['parse_error'] = str(parse_error)
else:
debug_info['error'] = f"Wrong content type: {content_type}"
debug_info['parsed_successfully'] = False
return debug_info
except Exception as e:
logger.error(f"DEBUG API Error: {e}")
return {
'error': str(e),
'success': False,
'url': f"{self.base_url}{endpoint}",
'method': method
}
async def get_system_health(self) -> Optional[Dict]:
try:
nodes = await self.get_all_nodes()
if not nodes:
return {
'status': 'error',
'nodes_online': 0,
'nodes_total': 0,
'message': 'No nodes data available'
}
online_nodes = len([n for n in nodes if n.get('status') == 'online'])
total_nodes = len(nodes)
if total_nodes == 0:
status = 'no_nodes'
elif online_nodes == 0:
status = 'critical'
elif online_nodes < total_nodes / 2:
status = 'warning'
else:
status = 'healthy'
return {
'status': status,
'nodes_online': online_nodes,
'nodes_total': total_nodes
}
except Exception as e:
logger.error(f"Error getting system health: {e}")
return {
'status': 'error',
'nodes_online': 0,
'nodes_total': 0,
'message': str(e)
}
async def get_nodes_statistics(self) -> Optional[Dict]:
try:
nodes = await self.get_all_nodes()
return {'data': nodes if nodes else []}
except Exception as e:
logger.error(f"Error getting nodes statistics: {e}")
return {'data': []}
async def get_all_subscriptions(self) -> Optional[List]:
try:
logger.info("Fetching all subscriptions from API")
result = await self._make_request('GET', '/api/subscriptions')
if not result:
logger.error("Empty response from subscriptions API")
return []
subscriptions_list = []
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
processed_users = []
for subscription in subscriptions_list:
if subscription.get('isFound') and 'user' in subscription:
user_data = subscription['user']
processed_user = {
'shortUuid': user_data.get('shortUuid'),
'username': user_data.get('username'),
'expireAt': user_data.get('expiresAt'),
'isActive': user_data.get('isActive', False),
'status': user_data.get('userStatus', 'UNKNOWN'),
'trafficUsed': user_data.get('trafficUsed', '0'),
'trafficLimit': user_data.get('trafficLimit', '0'),
'daysLeft': user_data.get('daysLeft', 0),
'subscriptionUrl': subscription.get('subscriptionUrl') or subscription.get('url'),
'links': subscription.get('links', [])
}
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")
return processed_users
except Exception as e:
logger.error(f"Exception in get_all_subscriptions: {e}", exc_info=True)
return []
async def bulk_reset_all_traffic(self) -> Optional[Dict]:
try:
logger.info("Attempting bulk traffic reset for all users")
all_users = await self.get_all_system_users_full()
if not all_users:
logger.warning("No users found for bulk traffic reset")
return {'success': False, 'message': 'No users found'}
user_uuids = []
for user in all_users:
user_uuid = user.get('uuid') or user.get('id') or user.get('shortUuid')
if user_uuid:
user_uuids.append(user_uuid)
if not user_uuids:
logger.warning("No valid UUIDs found for bulk operation")
return {'success': False, 'message': 'No valid user UUIDs'}
logger.info(f"Resetting traffic for {len(user_uuids)} users")
bulk_result = await self.bulk_reset_traffic(user_uuids)
if bulk_result:
return {'success': True, 'affected_users': len(user_uuids)}
success_count = 0
for user_uuid in user_uuids:
try:
result = await self.reset_user_traffic(user_uuid)
if result:
success_count += 1
except Exception as e:
logger.error(f"Failed to reset traffic for user {user_uuid}: {e}")
await asyncio.sleep(0.1)
return {
'success': success_count > 0,
'affected_users': success_count,
'total_users': len(user_uuids)
}
except Exception as e:
logger.error(f"Error in bulk_reset_all_traffic: {e}")
return {'success': False, 'error': str(e)}