Neueste Mod/Launcher-Version und Neuigkeiten kommen von git.mbm.wtf (bso-release.json mit Signatur), die Website ist nur noch Reserve (mit -dev zuerst). SPIELEN auf der Startseite startet ohne Server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
4.0 KiB
C#
86 lines
4.0 KiB
C#
using System.Text.Json;
|
|
|
|
namespace BSO.Launcher;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
|
|
/// <summary>Version aus dem Tag ("v0.5.0" -> "0.5.0") oder null.</summary>
|
|
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<JsonElement[]> 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();
|
|
}
|
|
|
|
/// <summary>Neueste Version von mod/launcher - null, wenn es keine gibt oder Gitea nicht erreichbar ist.</summary>
|
|
public static async Task<Api.Release?> 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<Manifest>(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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Neuigkeiten: Releases von Mod und Launcher, neueste zuerst. null = Gitea nicht erreichbar.</summary>
|
|
public static async Task<List<object>?> 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();
|
|
}
|
|
}
|