From f34e2f2d0828ff9160c8243bc54581f35dc81b6a Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 19 Mar 2026 04:17:18 +0000 Subject: [PATCH] feat: Add Uptime Kuma version check via Socket.IO Retrieve the running Uptime Kuma version by authenticating over Socket.IO and reading the info event. This adds a new login_socket dataset field, the fetch_socket_info helper, and the python-socketio and websocket-client dependencies. --- .env.sample | 4 ++++ BACKLOG.md | 9 ++++++++ README.md | 2 ++ check_updates.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +++ services.yaml | 13 +++++++++++ 6 files changed, 90 insertions(+) diff --git a/.env.sample b/.env.sample index 6b86b1f..10e3174 100644 --- a/.env.sample +++ b/.env.sample @@ -15,3 +15,7 @@ MATRIX_ACCESS_TOKEN=replace-with-access-token # Uptime Kuma push URL UPTIME_KUMA_PUSH_URL=https://uptime.example.net/api/push/your-token + +# Uptime Kuma login for version check +UPTIME_KUMA_USERNAME=your-username +UPTIME_KUMA_PASSWORD=your-password diff --git a/BACKLOG.md b/BACKLOG.md index a186ffc..c80b8fb 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -152,3 +152,12 @@ Acceptance criteria: - The script sends a request to the push URL when it runs. - The push URL is configured via `.env`. - Failures to reach the push URL are logged without crashing the run. + +## US-17 - Uptime Kuma Service Check + +As a maintainer, I want to track the running version of Uptime Kuma so updates are reported like other services. + +Acceptance criteria: +- The dataset includes Uptime Kuma with a current version endpoint and extraction rule. +- The upstream latest version uses the official GitHub releases. +- Any required authentication is documented in the dataset. diff --git a/README.md b/README.md index aed1677..eab0552 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ export MATRIX_HOMESERVER=... export MATRIX_ROOM_ID=... export MATRIX_ACCESS_TOKEN=... export UPTIME_KUMA_PUSH_URL=... +export UPTIME_KUMA_USERNAME=... +export UPTIME_KUMA_PASSWORD=... ``` The script also reads `.env` automatically if present. diff --git a/check_updates.py b/check_updates.py index b759382..dd03b38 100644 --- a/check_updates.py +++ b/check_updates.py @@ -12,6 +12,7 @@ from urllib.parse import urlencode, quote from urllib.request import HTTPCookieProcessor, Request, build_opener, urlopen import bcrypt +import socketio import yaml @@ -34,6 +35,7 @@ class ServiceConfig: upstream_latest_headers: Optional[Dict[str, str]] notes: Optional[str] login: Optional[Dict[str, Any]] + login_socket: Optional[Dict[str, Any]] def load_yaml(path: str) -> Dict[str, Any]: @@ -109,6 +111,10 @@ def load_services(config: Dict[str, Any]) -> Dict[str, ServiceConfig]: if login is not None and not isinstance(login, dict): raise ValueError("login must be a mapping") + login_socket = entry.get("login_socket") + if login_socket is not None and not isinstance(login_socket, dict): + raise ValueError("login_socket must be a mapping") + loaded[name] = ServiceConfig( name=name, base_url=base_url, @@ -120,6 +126,7 @@ def load_services(config: Dict[str, Any]) -> Dict[str, ServiceConfig]: upstream_latest_headers=upstream_headers, notes=entry.get("notes"), login=login, + login_socket=login_socket, ) return loaded @@ -414,6 +421,53 @@ def authenticate_service(service: ServiceConfig, timeout: float, user_agent: str return "; ".join(cookies) +def fetch_socket_info(service: ServiceConfig, timeout: float) -> Optional[str]: + if not service.login_socket: + return None + base_url = service.login_socket.get("url") + username_env = service.login_socket.get("username_env") + password_env = service.login_socket.get("password_env") + if not base_url or not username_env or not password_env: + raise ValueError(f"Login socket config incomplete for {service.name}") + + username = os.getenv(username_env) + password = os.getenv(password_env) + if not username or not password: + raise ValueError(f"Missing credentials for {service.name}") + + info_payload: Dict[str, Any] = {} + sio = socketio.Client(reconnection=False, request_timeout=timeout) + + @sio.on("info") + def on_info(data): + nonlocal info_payload + info_payload = data or {} + + sio.connect(base_url, transports=["websocket"], wait_timeout=timeout) + + response = sio.call( + "login", + { + "username": username, + "password": password, + "token": "", + }, + timeout=timeout, + ) + if not response or not response.get("ok"): + sio.disconnect() + raise ValueError(f"Socket login failed for {service.name}") + + if not info_payload.get("version"): + try: + info_payload = sio.call("info", timeout=timeout) or info_payload + except Exception: + pass + + sio.disconnect() + return info_payload.get("version") + + def check_service(service: ServiceConfig, timeout: float, user_agent: str) -> Dict[str, Any]: result: Dict[str, Any] = { "name": service.name, @@ -444,6 +498,11 @@ def check_service(service: ServiceConfig, timeout: float, user_agent: str) -> Di ) except Exception as exc: result["current_error"] = str(exc) + elif service.login_socket: + try: + result["current"] = fetch_socket_info(service, timeout) + except Exception as exc: + result["current_error"] = str(exc) if service.upstream_latest_version_url and service.upstream_latest_extract: headers = build_headers(service.upstream_latest_headers) diff --git a/pyproject.toml b/pyproject.toml index b4fa0a4..82a557e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,9 @@ requires-python = ">=3.10" dependencies = [ "PyYAML>=6.0", "bcrypt>=4.0", + "python-socketio>=5.11", + "requests>=2.31", + "websocket-client>=1.7", ] [project.optional-dependencies] diff --git a/services.yaml b/services.yaml index acc1230..a3924e7 100644 --- a/services.yaml +++ b/services.yaml @@ -83,6 +83,19 @@ services: upstream_latest_extract: type: jsonpath value: $.tag_name + - name: Uptime Kuma + base_url: https://uptime.jeena.net/ + current_version_url: + current_version_extract: + login_socket: + url: https://uptime.jeena.net/ + username_env: UPTIME_KUMA_USERNAME + password_env: UPTIME_KUMA_PASSWORD + upstream_latest_version_url: https://api.github.com/repos/louislam/uptime-kuma/releases/latest + upstream_latest_extract: + type: jsonpath + value: $.tag_name + notes: Version retrieved via authenticated Socket.IO info event. - name: FreshRSS base_url: https://rss.jeena.net/ current_version_url: https://rss.jeena.net/i/?a=about