import asyncio import json import re from datetime import datetime, timezone from urllib.error import URLError from urllib.request import Request, urlopen UPSTREAM_REPO_WEB = "https://git.nesterovic.cc/nessi/NexaPG" UPSTREAM_REPO_API = "https://git.nesterovic.cc/api/v1/repos/nessi/NexaPG" def _extract_version(payload: str) -> str | None: txt = payload.strip() if not txt: return None try: data = json.loads(txt) if isinstance(data, dict): for key in ("latest_version", "version", "tag_name", "name"): value = data.get(key) if isinstance(value, str) and value.strip(): return value.strip() if isinstance(data, list) and data and isinstance(data[0], dict): for key in ("latest_version", "version", "tag_name", "name"): value = data[0].get(key) if isinstance(value, str) and value.strip(): return value.strip() except json.JSONDecodeError: pass first_line = txt.splitlines()[0].strip() if first_line: return first_line[:64] return None def _parse_semver(value: str) -> tuple[int, ...] | None: normalized = value.strip().lower() if normalized.startswith("v"): normalized = normalized[1:] parts = re.findall(r"\d+", normalized) if not parts: return None return tuple(int(p) for p in parts[:4]) def is_update_available(current_version: str, latest_version: str) -> bool: current = _parse_semver(current_version) latest = _parse_semver(latest_version) if current and latest: max_len = max(len(current), len(latest)) current = current + (0,) * (max_len - len(current)) latest = latest + (0,) * (max_len - len(latest)) return latest > current return latest_version.strip() != current_version.strip() def _get_json(url: str): req = Request(url, headers={"User-Agent": "NexaPG/1.0"}) with urlopen(req, timeout=8) as response: raw = response.read(64_000).decode("utf-8", errors="replace") return json.loads(raw) def _fetch_latest_from_upstream_sync() -> tuple[str, str]: latest_release_url = f"{UPSTREAM_REPO_API}/releases/latest" try: release = _get_json(latest_release_url) if isinstance(release, dict): tag = (release.get("tag_name") or release.get("name") or "").strip() if tag: return tag[:64], "release" except Exception as exc: raise ValueError(f"Could not fetch latest release from upstream repository: {exc}") from exc raise ValueError("No published release found in upstream repository") async def fetch_latest_from_upstream() -> tuple[str | None, str | None, str | None]: try: latest, ref = await asyncio.to_thread(_fetch_latest_from_upstream_sync) return latest, ref, None except URLError as exc: return None, None, f"Version check failed: {exc.reason}" except Exception as exc: # noqa: BLE001 return None, None, f"Version check failed: {exc}" def utcnow() -> datetime: return datetime.now(timezone.utc)