using System.Text.Json; namespace BSO.Launcher; /// /// Releases direkt vom Git-Server (git.mbm.wtf, Gitea): Mod aus BSO-Mod, Launcher aus BSO-Launcher (beide oeffentlich). /// Jedes Release hat die Datei und bso-release.json (Version, SHA-256, Signatur aus release.ps1) - installiert wird weiter /// nur, was mit dem BSO-Release-Schluessel signiert ist, egal woher die Datei kommt. /// public static class GiteaReleases { public const string ApiBase = "https://git.mbm.wtf/api/v1/repos/MrMaho/"; public const string ManifestName = "bso-release.json"; static string Repo(string product) => product == "launcher" ? "BSO-Launcher" : "BSO-Mod"; sealed record Manifest(string product, string version, string sha256, long size, string file, string signature); /// Version aus dem Tag ("v0.5.0" -> "0.5.0") oder null. static string? TagVersion(JsonElement release) { string tag = release.GetProperty("tag_name").GetString() ?? ""; string v = tag.TrimStart('v', 'V'); return Version.TryParse(v, out _) ? v : null; } static bool Published(JsonElement release) => !release.GetProperty("draft").GetBoolean() && !release.GetProperty("prerelease").GetBoolean(); static async Task Fetch(string product) { using var doc = JsonDocument.Parse(await Api.Http.GetStringAsync(ApiBase + Repo(product) + "/releases?limit=20")); return doc.RootElement.EnumerateArray().Where(Published).Select(r => r.Clone()).ToArray(); } /// Neueste Version von mod/launcher - null, wenn es keine gibt oder Gitea nicht erreichbar ist. public static async Task Latest(string product) { try { var newest = (await Fetch(product)) .Select(r => (release: r, version: TagVersion(r))) .Where(x => x.version != null) .OrderByDescending(x => Version.Parse(x.version!)) .FirstOrDefault(); if (newest.version == null) return null; var assets = newest.release.GetProperty("assets").EnumerateArray() .ToDictionary(a => a.GetProperty("name").GetString() ?? "", a => a.GetProperty("browser_download_url").GetString() ?? ""); if (!assets.TryGetValue(ManifestName, out string? manifestUrl)) return null; var m = JsonSerializer.Deserialize(await Api.Http.GetStringAsync(manifestUrl)); if (m == null || m.product != product || !assets.TryGetValue(m.file, out string? fileUrl)) return null; return new Api.Release(product, m.version, m.sha256, m.size, newest.release.GetProperty("body").GetString() ?? "", newest.release.GetProperty("published_at").GetString() ?? "", m.file, fileUrl, m.signature); } catch (Exception e) { LauncherLog.Error($"Gitea-Release {product}: {e.Message}"); return null; } } /// Neuigkeiten: Releases von Mod und Launcher, neueste zuerst. null = Gitea nicht erreichbar. public static async Task?> List() { var items = new List<(DateTimeOffset date, object item)>(); int reached = 0; foreach (string product in new[] { "mod", "launcher" }) { try { foreach (var r in await Fetch(product)) { if (TagVersion(r) is not { } version) continue; string date = r.GetProperty("published_at").GetString() ?? ""; items.Add((DateTimeOffset.TryParse(date, out var d) ? d : default, new { product, version, changelog = r.GetProperty("body").GetString() ?? "", date })); } reached++; } catch (Exception e) { LauncherLog.Error($"Gitea-Releases {product}: {e.Message}"); } } return reached == 0 ? null : items.OrderByDescending(x => x.date).Select(x => x.item).ToList(); } }