From d5d4935f69611a8979a45bb056e22ec811042d6c Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:16:41 +0200 Subject: [PATCH] feat(users): Integrate real-time online status and enhance UI --- core/cli_api.py | 4 +- core/scripts/hysteria2/list_users.py | 55 +++++++ .../webpanel/routers/api/v1/schema/user.py | 6 +- core/scripts/webpanel/routers/user/user.py | 7 +- .../webpanel/routers/user/viewmodel.py | 7 +- core/scripts/webpanel/templates/users.html | 138 +++++++++++++----- 6 files changed, 173 insertions(+), 44 deletions(-) create mode 100644 core/scripts/hysteria2/list_users.py diff --git a/core/cli_api.py b/core/cli_api.py index c5d987e..07e30cb 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -40,7 +40,7 @@ class Command(Enum): EXTRA_CONFIG_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'extra_config.py') TRAFFIC_STATUS = 'traffic.py' # won't be called directly (it's a python module) UPDATE_GEO = os.path.join(SCRIPT_DIR, 'hysteria2', 'update_geo.py') - LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.sh') + LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.py') SERVER_INFO = os.path.join(SCRIPT_DIR, 'hysteria2', 'server_info.py') BACKUP_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'backup.py') RESTORE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restore.py') @@ -256,7 +256,7 @@ def list_users() -> dict[str, dict[str, Any]] | None: ''' Lists all users. ''' - if res := run_cmd(['bash', Command.LIST_USERS.value]): + if res := run_cmd(['python3', Command.LIST_USERS.value]): return json.loads(res) diff --git a/core/scripts/hysteria2/list_users.py b/core/scripts/hysteria2/list_users.py new file mode 100644 index 0000000..a1ce151 --- /dev/null +++ b/core/scripts/hysteria2/list_users.py @@ -0,0 +1,55 @@ +import sys +import json +from pathlib import Path + +try: + from hysteria2_api import Hysteria2Client +except ImportError: + sys.exit("Error: hysteria2_api library not found. Please install it.") + +sys.path.append(str(Path(__file__).parent.parent)) +from paths import USERS_FILE, CONFIG_FILE, API_BASE_URL + +def get_secret() -> str | None: + if not CONFIG_FILE.exists(): + return None + try: + with CONFIG_FILE.open('r') as f: + config_data = json.load(f) + return config_data.get("trafficStats", {}).get("secret") + except (json.JSONDecodeError, IOError): + return None + +def get_users() -> dict: + if not USERS_FILE.exists(): + return {} + try: + with USERS_FILE.open('r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + return {} + +def main(): + users_dict = get_users() + secret = get_secret() + + if secret and users_dict: + try: + client = Hysteria2Client(base_url=API_BASE_URL, secret=secret) + online_clients = client.get_online_clients() + + for username, status in online_clients.items(): + if status.is_online and username in users_dict: + users_dict[username]['online_count'] = status.connections + except Exception: + pass + + users_list = [ + {**user_data, 'username': username, 'online_count': user_data.get('online_count', 0)} + for username, user_data in users_dict.items() + ] + + print(json.dumps(users_list, indent=2)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/schema/user.py b/core/scripts/webpanel/routers/api/v1/schema/user.py index 64dc768..4a2ddca 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/user.py +++ b/core/scripts/webpanel/routers/api/v1/schema/user.py @@ -4,19 +4,21 @@ from pydantic import BaseModel, RootModel, Field, field_validator class UserInfoResponse(BaseModel): + username: str password: str max_download_bytes: int expiration_days: int - account_creation_date: str + account_creation_date: Optional[str] = None blocked: bool unlimited_ip: bool = Field(False, alias='unlimited_user') status: Optional[str] = None upload_bytes: Optional[int] = None download_bytes: Optional[int] = None + online_count: int = 0 class UserListResponse(RootModel): - root: dict[str, UserInfoResponse] + root: List[UserInfoResponse] class UsernamesRequest(BaseModel): usernames: List[str] diff --git a/core/scripts/webpanel/routers/user/user.py b/core/scripts/webpanel/routers/user/user.py index 565225f..77d86de 100644 --- a/core/scripts/webpanel/routers/user/user.py +++ b/core/scripts/webpanel/routers/user/user.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, HTTPException, Request, Depends from fastapi.templating import Jinja2Templates @@ -13,10 +12,8 @@ router = APIRouter() @router.get('/') async def users(request: Request, templates: Jinja2Templates = Depends(get_templates)): try: - dict_users = cli_api.list_users() # type: ignore - users: list[User] = [] - if dict_users: - users: list[User] = [User.from_dict(key, value) for key, value in dict_users.items()] # type: ignore + users_list = cli_api.list_users() or [] + users: list[User] = [User.from_dict(user_data.get('username', ''), user_data) for user_data in users_list] return templates.TemplateResponse('users.html', {'users': users, 'request': request}) except Exception as e: diff --git a/core/scripts/webpanel/routers/user/viewmodel.py b/core/scripts/webpanel/routers/user/viewmodel.py index 0c3134d..9db3110 100644 --- a/core/scripts/webpanel/routers/user/viewmodel.py +++ b/core/scripts/webpanel/routers/user/viewmodel.py @@ -11,6 +11,7 @@ class User(BaseModel): expiry_days: str enable: bool unlimited_ip: bool + online_count: int = 0 @staticmethod def from_dict(username: str, user_data: dict): @@ -36,7 +37,8 @@ class User(BaseModel): 'expiry_date': 'N/A', 'expiry_days': 'N/A', 'enable': False, - 'unlimited_ip': False + 'unlimited_ip': False, + 'online_count': 0 } expiration_days = user_data.get('expiration_days', 0) @@ -77,7 +79,8 @@ class User(BaseModel): 'expiry_date': display_expiry_date, 'expiry_days': display_expiry_days, 'enable': not user_data.get('blocked', False), - 'unlimited_ip': user_data.get('unlimited_user', False) + 'unlimited_ip': user_data.get('unlimited_user', False), + 'online_count': user_data.get('online_count', 0) } @staticmethod diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html index 873e1c8..a2faa9d 100644 --- a/core/scripts/webpanel/templates/users.html +++ b/core/scripts/webpanel/templates/users.html @@ -93,35 +93,38 @@ {% for user in users|sort(attribute='username', case_sensitive=false) %}