ensure_xui_servers no longer calls get_xui_server when mirroring the primary panel into legacy sync fields. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
7.9 KiB
Python
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 {}}
|