Files
Amnezia-Web-Panel-main/managers/xui_servers.py
T
2026-07-26 07:39:06 +03:00

215 lines
7.9 KiB
Python

"""Multi-panel 3x-ui server registry stored in settings.xui_servers."""
from __future__ import annotations
import uuid
from typing import Any, Optional
def _new_id() -> str:
return str(uuid.uuid4())
def normalize_server(raw: dict | None) -> dict:
raw = raw or {}
return {
'id': str(raw.get('id') or _new_id()),
'name': (raw.get('name') or '').strip() or '3x-ui',
'url': (raw.get('url') or '').strip().rstrip('/'),
'sub_url': (raw.get('sub_url') or '').strip().rstrip('/'),
'api_token': (raw.get('api_token') or '').strip(),
'username': (raw.get('username') or '').strip(),
'password': raw.get('password') or '',
'default_inbound_id': int(raw.get('default_inbound_id') or 0),
'enabled': bool(raw.get('enabled', True)),
'sync_users': bool(raw.get('sync_users', False)),
}
def legacy_sync_to_server(sync: dict) -> Optional[dict]:
"""Build one xui_servers entry from old settings.sync.xui_* fields."""
sync = sync or {}
url = (sync.get('xui_url') or '').strip()
if not url:
return None
return normalize_server({
'id': 'legacy-default',
'name': '3x-ui (default)',
'url': url,
'sub_url': sync.get('xui_sub_url') or '',
'api_token': sync.get('xui_api_token') or '',
'username': sync.get('xui_username') or '',
'password': sync.get('xui_password') or '',
'default_inbound_id': int(sync.get('xui_inbound_id') or 0),
'enabled': True,
'sync_users': bool(sync.get('xui_sync_users')),
})
def ensure_xui_servers(settings: dict) -> list:
"""Ensure settings['xui_servers'] exists; migrate legacy sync fields once."""
settings = settings if isinstance(settings, dict) else {}
servers = settings.get('xui_servers')
if not isinstance(servers, list):
servers = []
servers = [normalize_server(s) for s in servers if isinstance(s, dict)]
if not servers:
migrated = legacy_sync_to_server(settings.get('sync') or {})
if migrated:
servers = [migrated]
settings['xui_servers'] = servers
# Mirror primary into legacy sync.* for older code paths (no get_xui_server - avoids recursion)
sync = settings.setdefault('sync', {})
primary = None
for s in servers:
if s.get('enabled'):
primary = s
break
if primary is None and servers:
primary = servers[0]
if primary and primary.get('url'):
sync.setdefault('xui_url', primary.get('url') or '')
sync.setdefault('xui_sub_url', primary.get('sub_url') or '')
sync.setdefault('xui_api_token', primary.get('api_token') or '')
sync.setdefault('xui_username', primary.get('username') or '')
sync.setdefault('xui_password', primary.get('password') or '')
sync.setdefault('xui_inbound_id', int(primary.get('default_inbound_id') or 0))
return servers
def list_xui_servers(settings: dict, *, enabled_only: bool = False) -> list:
servers = ensure_xui_servers(settings)
if enabled_only:
return [s for s in servers if s.get('enabled')]
return servers
def get_xui_server(settings: dict, panel_id: Optional[str] = None) -> Optional[dict]:
servers = ensure_xui_servers(settings)
if not servers:
return None
if panel_id:
for s in servers:
if str(s.get('id')) == str(panel_id):
return s
return None
for s in servers:
if s.get('enabled'):
return s
return servers[0]
def public_server_view(server: dict) -> dict:
"""Safe view for UI (no password/token secrets in list if desired — we keep them for admin edit)."""
return {
'id': server.get('id'),
'name': server.get('name') or '3x-ui',
'url': server.get('url') or '',
'sub_url': server.get('sub_url') or '',
'api_token': server.get('api_token') or '',
'username': server.get('username') or '',
'password': server.get('password') or '',
'default_inbound_id': int(server.get('default_inbound_id') or 0),
'enabled': bool(server.get('enabled', True)),
'sync_users': bool(server.get('sync_users', False)),
'has_token': bool(server.get('api_token')),
'has_password': bool(server.get('password')),
}
def upsert_xui_server(settings: dict, payload: dict, *, panel_id: Optional[str] = None) -> dict:
servers = ensure_xui_servers(settings)
panel_id = panel_id or payload.get('id')
existing = None
if panel_id:
for s in servers:
if str(s.get('id')) == str(panel_id):
existing = s
break
merged = dict(existing or {})
for key in ('name', 'url', 'sub_url', 'api_token', 'username', 'password'):
if key in payload and payload[key] is not None:
# Empty password/token on edit means "keep previous" when editing
if key in ('password', 'api_token') and existing and payload[key] == '':
continue
merged[key] = payload[key]
if 'default_inbound_id' in payload and payload['default_inbound_id'] is not None:
merged['default_inbound_id'] = int(payload['default_inbound_id'] or 0)
if 'enabled' in payload and payload['enabled'] is not None:
merged['enabled'] = bool(payload['enabled'])
if 'sync_users' in payload and payload['sync_users'] is not None:
merged['sync_users'] = bool(payload['sync_users'])
if not merged.get('id'):
merged['id'] = _new_id()
server = normalize_server(merged)
if not server['url']:
raise ValueError('3x-ui URL is required')
if not server['name']:
server['name'] = server['url']
if existing:
for i, s in enumerate(servers):
if str(s.get('id')) == str(existing.get('id')):
servers[i] = server
break
else:
servers.append(server)
settings['xui_servers'] = servers
_mirror_primary_to_sync(settings)
return server
def delete_xui_server(settings: dict, panel_id: str) -> bool:
servers = ensure_xui_servers(settings)
new_list = [s for s in servers if str(s.get('id')) != str(panel_id)]
if len(new_list) == len(servers):
return False
settings['xui_servers'] = new_list
_mirror_primary_to_sync(settings)
return True
def _mirror_primary_to_sync(settings: dict) -> None:
sync = settings.setdefault('sync', {})
primary = get_xui_server(settings, None)
if not primary:
sync['xui_url'] = ''
sync['xui_sub_url'] = ''
return
sync['xui_url'] = primary.get('url') or ''
sync['xui_sub_url'] = primary.get('sub_url') or ''
sync['xui_api_token'] = primary.get('api_token') or ''
sync['xui_username'] = primary.get('username') or ''
sync['xui_password'] = primary.get('password') or ''
sync['xui_inbound_id'] = int(primary.get('default_inbound_id') or 0)
if any(s.get('sync_users') for s in settings.get('xui_servers') or []):
sync['xui_sync_users'] = True
def server_to_settings_slice(server: dict) -> dict:
"""Shape expected by xui_api._settings_creds / from_panel_settings."""
return {
'sync': {
'xui_url': server.get('url') or '',
'xui_sub_url': server.get('sub_url') or '',
'xui_api_token': server.get('api_token') or '',
'xui_username': server.get('username') or '',
'xui_password': server.get('password') or '',
'xui_inbound_id': int(server.get('default_inbound_id') or 0),
}
}
def resolve_panel_settings(full_settings: dict, panel_id: Optional[str] = None) -> dict:
"""Return a settings-like dict scoped to one 3x-ui panel (fallback: primary / legacy)."""
server = get_xui_server(full_settings, panel_id)
if server:
return server_to_settings_slice(server)
# Fallback to legacy global sync
return {'sync': (full_settings or {}).get('sync') or {}}