From 83aec980b3ee739fb4bc251217556b3ae44f741b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 1 Oct 2022 21:09:39 -0400 Subject: Add UpdateManifest site type. Adds the UpdateManifest site key and associated client. This required some additional features in the existing update machinery. Each "version" can now (optionally) have its own download URL. Mod Page objects can now specify that subkey matching (for that page) should be "strict". A strict subkey match does not fall back to matching with no subkey if a subkey was provided but produced no versions. It also strips the leading '@' from the subkey. IModDownload objects are now responsible for deciding whether a subkey matches or not. The default behavior is unchanged, but this allows different mod sites to have different rules for subkey matching (which the UpdateManifest mod site uses to force exact matches). --- .../Framework/Clients/GenericModDownload.cs | 15 ++++-- src/SMAPI.Web/Framework/Clients/GenericModPage.cs | 16 ++++++ .../UpdateManifest/IUpdateManifestClient.cs | 9 ++++ .../UpdateManifest/TextAsJsonMediaTypeFormatter.cs | 15 ++++++ .../Clients/UpdateManifest/UpdateManifestClient.cs | 55 ++++++++++++++++++++ .../UpdateManifest/UpdateManifestModDownload.cs | 27 ++++++++++ .../UpdateManifest/UpdateManifestModModel.cs | 26 ++++++++++ .../UpdateManifest/UpdateManifestModPage.cs | 58 ++++++++++++++++++++++ .../Clients/UpdateManifest/UpdateManifestModel.cs | 23 +++++++++ .../UpdateManifest/UpdateManifestVersionModel.cs | 26 ++++++++++ 10 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs (limited to 'src/SMAPI.Web/Framework/Clients') diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index b37e5cda..5cc03aba 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -17,6 +17,10 @@ namespace StardewModdingAPI.Web.Framework.Clients /// The download's file version. public string? Version { get; } + /// + /// The URL for this download, if it has one distinct from the mod page's URL. + /// + public string? Url { get; } /********* ** Public methods @@ -25,21 +29,22 @@ namespace StardewModdingAPI.Web.Framework.Clients /// The download's display name. /// The download's description. /// The download's file version. - public GenericModDownload(string name, string? description, string? version) + /// The download's URL (if different from the mod page's URL). + public GenericModDownload(string name, string? description, string? version, string? url = null) { this.Name = name; this.Description = description; this.Version = version; + this.Url = url; } /// - /// Return true if the subkey matches this download. A subkey matches if it appears as + /// Return if the subkey matches this download. A subkey matches if it appears as /// a substring in the name or description. /// /// the subkey - /// true if matches this download, otherwise false - /// - public bool MatchesSubkey(string subkey) { + /// if matches this download, otherwise + public virtual bool MatchesSubkey(string subkey) { return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true; } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 5353c7e1..4c66e1a0 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,6 +40,8 @@ namespace StardewModdingAPI.Web.Framework.Clients [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] public bool IsValid => this.Status == RemoteModStatus.Ok; + /// Whether to use strict subkey matching or not. + public bool IsSubkeyStrict { get; set; } = false; /********* ** Public methods @@ -79,5 +81,19 @@ namespace StardewModdingAPI.Web.Framework.Clients return this; } + + /// Returns the mod page name. + /// ignored + /// The mod page name. + public virtual string? GetName(string? subkey) { + return this.Name; + } + + /// Returns the mod page URL. + /// ignored + /// The mod page URL. + public virtual string? GetUrl(string? subkey) { + return this.Url; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs new file mode 100644 index 00000000..36d018c7 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -0,0 +1,9 @@ +// Copyright 2022 Jamie Taylor +using System; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// An HTTP client for fetching an update manifest from an arbitrary URL. + internal interface IUpdateManifestClient : IModSiteClient, IDisposable { } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs new file mode 100644 index 00000000..48e3c294 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs @@ -0,0 +1,15 @@ +// Copyright 2022 Jamie Taylor +using System.Net.Http.Formatting; +using System.Net.Http.Headers; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// + /// A that can parse from content of type text/plain. + /// + internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter { + /// Construct a new + public TextAsJsonMediaTypeFormatter() { + this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs new file mode 100644 index 00000000..d7cf4945 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -0,0 +1,55 @@ +// Copyright 2022 Jamie Taylor +using System; +using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using System.Threading.Tasks; +using System.Net; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// An HTTP client for fetching an update manifest from an arbitrary URL. + internal class UpdateManifestClient : IUpdateManifestClient { + /********* + ** Fields + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.UpdateManifest; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the API client. + public UpdateManifestClient(string userAgent) { + this.Client = new FluentClient() + .SetUserAgent(userAgent); + this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter()); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() { + this.Client.Dispose(); + } + + /// + public async Task GetModData(string id) { + UpdateManifestModel? manifest; + try { + manifest = await this.Client.GetAsync(id).As(); + } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); + } + if (manifest is null) { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"Error parsing manifest at {id}"); + } + + return new UpdateManifestModPage(id, manifest); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs new file mode 100644 index 00000000..117ae15c --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -0,0 +1,27 @@ +// Copyright 2022 Jamie Taylor +using System; +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Metadata about a mod download in an update manifest file. + internal class UpdateManifestModDownload : GenericModDownload { + /// The subkey for this mod download + private readonly string subkey; + /// Construct an instance. + /// The subkey for this download. + /// The mod name for this download. + /// The download's version. + /// The download's URL. + public UpdateManifestModDownload(string subkey, string name, string? version, string? url) : base(name, null, version, url) { + this.subkey = subkey; + } + + /// + /// Returns iff the given subkey is the same as the subkey for this download. + /// + /// The subkey to match + /// if is the same as the subkey for this download, otherwise. + public override bool MatchesSubkey(string subkey) { + return this.subkey == subkey; + } + } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs new file mode 100644 index 00000000..4ec9c03d --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs @@ -0,0 +1,26 @@ +// Copyright 2022 Jamie Taylor +using System; +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Data model for a mod in an update manifest. + internal class UpdateManifestModModel { + /// The mod's name. + public string Name { get; } + + /// The mod's URL. + public string? Url { get; } + + /// The versions for this mod. + public UpdateManifestVersionModel[] Versions { get; } + + /// Construct an instance. + /// The mod's name. + /// The mod's URL. + /// The versions for this mod. + public UpdateManifestModModel(string name, string? url, UpdateManifestVersionModel[] versions) { + this.Name = name; + this.Url = url; + this.Versions = versions; + } + } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs new file mode 100644 index 00000000..109175b5 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -0,0 +1,58 @@ +// Copyright 2022 Jamie Taylor +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Metadata about an update manifest "page". + internal class UpdateManifestModPage : GenericModPage { + /// The update manifest model. + private UpdateManifestModel manifest; + + /// Constuct an instance. + /// The "id" (i.e., URL) of this update manifest. + /// The manifest object model. + public UpdateManifestModPage(string id, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, id) { + this.IsSubkeyStrict = true; + this.manifest = manifest; + this.SetInfo(name: id, url: id, version: null, downloads: TranslateDownloads(manifest).ToArray()); + } + + /// Return the mod name for the given subkey, if it exists in this update manifest. + /// The subkey. + /// The mod name for the given subkey, or if this manifest does not contain the given subkey. + public override string? GetName(string? subkey) { + if (subkey is null) + return null; + this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); + return modModel?.Name; + } + + /// Return the mod URL for the given subkey, if it exists in this update manifest. + /// The subkey. + /// The mod URL for the given subkey, or if this manifest does not contain the given subkey. + public override string? GetUrl(string? subkey) { + if (subkey is null) + return null; + this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); + return modModel?.Url; + } + + + /********* + ** Private methods + *********/ + /// Translate the downloads from the manifest's object model into objects. + /// The manifest object model. + /// An for each in the manifest. + private static IEnumerable TranslateDownloads(UpdateManifestModel manifest) { + foreach (var entry in manifest.Subkeys) { + foreach (var version in entry.Value.Versions) { + yield return new UpdateManifestModDownload(entry.Key, entry.Value.Name, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl); + } + } + } + + } +} \ No newline at end of file diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs new file mode 100644 index 00000000..03f89726 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs @@ -0,0 +1,23 @@ +// Copyright 2022 Jamie Taylor +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Data model for an update manifest. + internal class UpdateManifestModel { + /// The manifest format version. + public string ManifestVersion { get; } + + /// The subkeys in this update manifest. + public IDictionary Subkeys { get; } + + /// Construct an instance. + /// The manifest format version. + /// The subkeys in this update manifest. + public UpdateManifestModel(string manifestVersion, IDictionary subkeys) { + this.ManifestVersion = manifestVersion; + this.Subkeys = subkeys; + } + } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs new file mode 100644 index 00000000..55b6db61 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs @@ -0,0 +1,26 @@ +// Copyright 2022 Jamie Taylor +using System; +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Data model for a Version in an update manifest. + internal class UpdateManifestVersionModel { + /// The semantic version string. + public string Version { get; } + + /// The URL for this version's download page (if any). + public string? DownloadPageUrl { get; } + + /// The URL for this version's direct file download (if any). + public string? DownloadFileUrl { get; } + + /// Construct an instance. + /// The semantic version string. + /// This version's download page URL (if any). + /// This version's direct file download URL (if any). + public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) { + this.Version = version; + this.DownloadPageUrl = downloadPageUrl; + this.DownloadFileUrl = downloadFileUrl; + } + } +} + -- cgit