From 6d4eed56b1d80d02db773b0cd2f372baec6b2d1b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 22 Sep 2022 23:03:26 -0400 Subject: refactor UpdateKey parsing, move responsibility for subkey matching UpdateKey parsing now allows multiple : and @ inside the update key, splitting on the first occurence of each Subkey matching is moved into IModDownload / GenericModDownload, in preparation for some Mod Sites using something less error-prone than substring matching. --- .../Framework/UpdateData/UpdateKey.cs | 45 +++++++++++++--------- .../Framework/Clients/GenericModDownload.cs | 14 +++++++ src/SMAPI.Web/Framework/IModDownload.cs | 5 +++ src/SMAPI.Web/Framework/ModSiteManager.cs | 12 +++--- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 960caf96..7c2cc73c 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -54,35 +54,44 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData public UpdateKey(ModSiteKey site, string? id, string? subkey) : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } + /// + /// Split a string into two at a delimiter. If the delimiter does not appear in the string then the second + /// value of the returned tuple is null. Both returned strings are trimmed of whitespace. + /// + /// The string to split. + /// The character on which to split. + /// + /// If true then the second string returned will include the delimiter character + /// (provided that the string is not null) + /// + /// + /// A pair containing the string consisting of all characters in before the first + /// occurrence of , and a string consisting of all characters in + /// after the first occurrence of or null if the delimiter does not + /// exist in s. Both strings are trimmed of whitespace. + /// + private static (string, string?) Bifurcate(string s, char delimiter, bool keepDelimiter = false) { + int pos = s.IndexOf(delimiter); + if (pos < 0) + return (s.Trim(), null); + return (s.Substring(0, pos).Trim(), s.Substring(pos + (keepDelimiter ? 0 : 1)).Trim()); + } + /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string? raw) { + if (raw is null) + return new UpdateKey(raw, ModSiteKey.Unknown, null, null); // extract site + ID - string? rawSite; - string? id; - { - string[]? parts = raw?.Trim().Split(':'); - if (parts?.Length != 2) - return new UpdateKey(raw, ModSiteKey.Unknown, null, null); - - rawSite = parts[0].Trim(); - id = parts[1].Trim(); - } + (string rawSite, string? id) = Bifurcate(raw, ':'); if (string.IsNullOrWhiteSpace(id)) id = null; // extract subkey string? subkey = null; if (id != null) - { - string[] parts = id.Split('@'); - if (parts.Length == 2) - { - id = parts[0].Trim(); - subkey = $"@{parts[1]}".Trim(); - } - } + (id, subkey) = Bifurcate(id, '@', true); // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 548f17c3..b37e5cda 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Web.Framework.Clients { /// Generic metadata about a file download on a mod page. @@ -29,5 +31,17 @@ namespace StardewModdingAPI.Web.Framework.Clients this.Description = description; this.Version = version; } + + /// + /// Return true 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) { + return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true + || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true; + } } } diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs index fe171785..13739f8f 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -14,5 +14,10 @@ namespace StardewModdingAPI.Web.Framework /// The download's file version. string? Version { get; } + + /// Return true iff the subkey matches this download + /// the subkey + /// true if matches this download, otherwise false + bool MatchesSubkey(string subkey); } } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 674b9ffc..70070d63 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -119,7 +119,7 @@ namespace StardewModdingAPI.Web.Framework preview = null; // parse all versions from the mod page - IEnumerable<(string? name, string? description, ISemanticVersion? version)> GetAllVersions() + IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() { if (mod != null) { @@ -132,14 +132,14 @@ namespace StardewModdingAPI.Web.Framework // get mod version ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); if (modVersion != null) - yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version)); + yield return (download: null, version: ParseAndMapVersion(mod.Version)); // get file versions foreach (IModDownload download in mod.Downloads) { ISemanticVersion? cur = ParseAndMapVersion(download.Version); if (cur != null) - yield return (download.Name, download.Description, cur); + yield return (download, cur); } } } @@ -148,13 +148,13 @@ namespace StardewModdingAPI.Web.Framework .ToArray(); // get main + preview versions - void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(string? name, string? description, ISemanticVersion? version), bool>? filter = null) + void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) { mainVersion = null; previewVersion = null; // get latest main + preview version - foreach ((string? name, string? description, ISemanticVersion? version) entry in versions) + foreach ((IModDownload? download, ISemanticVersion? version) entry in versions) { if (entry.version is null || filter?.Invoke(entry) == false) continue; @@ -178,7 +178,7 @@ namespace StardewModdingAPI.Web.Framework } if (subkey is not null) - TryGetVersions(out main, out preview, entry => entry.name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || entry.description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true); + TryGetVersions(out main, out preview, entry => entry.download?.MatchesSubkey(subkey) == true); if (main is null) TryGetVersions(out main, out preview); -- cgit 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/UpdateData/ModSiteKey.cs | 5 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 14 +++-- .../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 +++++++++ src/SMAPI.Web/Framework/IModDownload.cs | 7 ++- src/SMAPI.Web/Framework/IModPage.cs | 17 ++++++ src/SMAPI.Web/Framework/ModInfoModel.cs | 11 +++- src/SMAPI.Web/Framework/ModSiteManager.cs | 64 ++++++++++++++-------- src/SMAPI.Web/SMAPI.Web.csproj | 4 ++ src/SMAPI.Web/Startup.cs | 3 + 18 files changed, 359 insertions(+), 36 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 diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs index 47cd3f7e..d1dd9049 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData ModDrop, /// The Nexus Mods mod repository. - Nexus + Nexus, + + /// An arbitrary URL for an update manifest file. + UpdateManifest } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 71fb42c2..1c34f2af 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -22,6 +22,7 @@ using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest; using StardewModdingAPI.Web.Framework.ConfigModels; namespace StardewModdingAPI.Web.Controllers @@ -63,14 +64,15 @@ namespace StardewModdingAPI.Web.Controllers /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + /// The UpdateManifest client. + public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); this.WikiCache = wikiCache; this.ModCache = modCache; this.Config = config; - this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus }); + this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus, updateManifest }); } /// Fetch version metadata for the given mods. @@ -161,18 +163,22 @@ namespace StardewModdingAPI.Web.Controllers // if there's only a prerelease version (e.g. from GitHub), don't override the main version ISemanticVersion? curMain = data.Version; + string? curMainUrl = data.MainVersionUrl; ISemanticVersion? curPreview = data.PreviewVersion; + string? curPreviewUrl = data.PreviewVersionUrl; if (curPreview == null && curMain?.IsPrerelease() == true) { curPreview = curMain; + curPreviewUrl = curMainUrl; curMain = null; + curMainUrl = null; } // handle versions if (this.IsNewer(curMain, main?.Version)) - main = new ModEntryVersionModel(curMain, data.Url!); + main = new ModEntryVersionModel(curMain, curMainUrl ?? data.Url!); if (this.IsNewer(curPreview, optional?.Version)) - optional = new ModEntryVersionModel(curPreview, data.Url!); + optional = new ModEntryVersionModel(curPreview, curPreviewUrl ?? data.Url!); } // get unofficial version 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; + } + } +} + diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs index 13739f8f..b0e9a664 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -15,9 +15,12 @@ namespace StardewModdingAPI.Web.Framework /// The download's file version. string? Version { get; } - /// Return true iff the subkey matches this download + /// This download's URL (if it has a URL that is different from the containing mod page's URL). + string? Url { get; } + + /// Return iff the subkey matches this download /// the subkey - /// true if matches this download, otherwise false + /// if matches this download, otherwise bool MatchesSubkey(string subkey); } } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 4d0a8d61..3fc8bb81 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,10 +39,27 @@ namespace StardewModdingAPI.Web.Framework [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } + /// + /// Does this page use strict subkey matching. Pages that use string subkey matching do not fall back + /// to searching for versions without a subkey if there are no versions found when given a subkey. + /// Additionally, the leading @ is stripped from the subkey value before searching for matches. + /// + bool IsSubkeyStrict { get; } /********* ** Methods *********/ + + /// Get the mod name associated with the given subkey, if any. + /// The subkey. + /// The mod name associated with the given subkey (if any) + string? GetName(string? subkey); + + /// Get the URL for the mod associated with the given subkey, if any. + /// The subkey. + /// The URL for the mod associated with the given subkey (if any) + string? GetUrl(string? subkey); + /// Set the fetched mod info. /// The mod name. /// The mod's semantic version number. diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs index e70b60bf..17415589 100644 --- a/src/SMAPI.Web/Framework/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -27,6 +27,11 @@ namespace StardewModdingAPI.Web.Framework /// The error message indicating why the mod is invalid (if applicable). public string? Error { get; private set; } + /// The URL associated with the mod's latest version (if distinct from the mod page's URL). + public string? MainVersionUrl { get; private set; } + + /// The URL associated with the mod's (if distinct from the mod page's URL). + public string? PreviewVersionUrl { get; private set; } /********* ** Public methods @@ -65,11 +70,15 @@ namespace StardewModdingAPI.Web.Framework /// Set the mod version info. /// The semantic version for the mod's latest release. /// The semantic version for the mod's latest preview release, if available and different from . + /// The URL associated with , if different from the mod page's URL. + /// The URL associated with , if different from the mod page's URL. [MemberNotNull(nameof(ModInfoModel.Version))] - public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null) + public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null, string? mainVersionUrl = null, string? previewVersionUrl = null) { this.Version = version; this.PreviewVersion = previewVersion; + this.MainVersionUrl = mainVersionUrl; + this.PreviewVersionUrl = previewVersionUrl; return this; } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 70070d63..5581d799 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -66,23 +66,25 @@ namespace StardewModdingAPI.Web.Framework { // get base model ModInfoModel model = new(); - if (page.IsValid) - model.SetBasicInfo(page.Name, page.Url); - else - { + if (!page.IsValid) { model.SetError(page.Status, page.Error); return model; } + if (page.IsSubkeyStrict && subkey is not null) { + if (subkey.Length > 0 && subkey[0] == '@') { + subkey = subkey.Substring(1); + } + } + // fetch versions - bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion); - if (!hasVersions && subkey != null) - hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion); + bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl); if (!hasVersions) - return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions."); + return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions."); + model.SetBasicInfo(page.GetName(subkey) ?? page.Name, page.GetUrl(subkey) ?? page.Url); // return info - return model.SetVersions(mainVersion!, previewVersion); + return model.SetVersions(mainVersion!, previewVersion, mainVersionUrl, previewVersionUrl); } /// Get a semantic local version for update checks. @@ -113,10 +115,12 @@ namespace StardewModdingAPI.Web.Framework /// The changes to apply to remote versions for update checks. /// The main mod version. /// The latest prerelease version, if newer than . - private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview) + private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainVersionUrl, out string? previewVersionUrl) { main = null; preview = null; + mainVersionUrl = null; + previewVersionUrl = null; // parse all versions from the mod page IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() @@ -132,7 +136,7 @@ namespace StardewModdingAPI.Web.Framework // get mod version ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); if (modVersion != null) - yield return (download: null, version: ParseAndMapVersion(mod.Version)); + yield return (download: null, version: modVersion); // get file versions foreach (IModDownload download in mod.Downloads) @@ -148,10 +152,12 @@ namespace StardewModdingAPI.Web.Framework .ToArray(); // get main + preview versions - void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) + void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) { mainVersion = null; previewVersion = null; + mainVersionUrl = null; + previewVersionUrl = null; // get latest main + preview version foreach ((IModDownload? download, ISemanticVersion? version) entry in versions) @@ -159,28 +165,40 @@ namespace StardewModdingAPI.Web.Framework if (entry.version is null || filter?.Invoke(entry) == false) continue; - if (entry.version.IsPrerelease()) - previewVersion ??= entry.version; - else - mainVersion ??= entry.version; - - if (mainVersion != null) + if (entry.version.IsPrerelease()) { + if (previewVersion is null) { + previewVersion = entry.version; + previewVersionUrl = entry.download?.Url; + } + } else { + mainVersion = entry.version; + mainVersionUrl = entry.download?.Url; break; // any others will be older since entries are sorted by version + } } // normalize values if (previewVersion is not null) { - mainVersion ??= previewVersion; // if every version is prerelease, latest one is the main version - if (!previewVersion.IsNewerThan(mainVersion)) + if (mainVersion is null) { + // if every version is prerelease, latest one is the main version + mainVersion = previewVersion; + mainVersionUrl = previewVersionUrl; + } + if (!previewVersion.IsNewerThan(mainVersion)) { previewVersion = null; + previewVersionUrl = null; + } } } - if (subkey is not null) - TryGetVersions(out main, out preview, entry => entry.download?.MatchesSubkey(subkey) == true); + if (subkey is not null) { + TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl, entry => entry.download?.MatchesSubkey(subkey) == true); + if (mod?.IsSubkeyStrict == true) + return main != null; + } if (main is null) - TryGetVersions(out main, out preview); + TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl); return main != null; } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index d26cb6f8..cfec1d08 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,6 +12,7 @@ + @@ -48,4 +49,7 @@ PreserveNewest + + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 54c25979..a068a998 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -20,6 +20,7 @@ using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.Clients.Pastebin; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest; using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.RedirectRules; @@ -149,6 +150,8 @@ namespace StardewModdingAPI.Web baseUrl: api.PastebinBaseUrl, userAgent: userAgent )); + + services.AddSingleton(new UpdateManifestClient(userAgent: userAgent)); } // init helpers -- cgit From 55fd4839da43e7ca205eaa85480786e3dfe8af6f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: minor formatting, copyediting, and error-handling --- .../Framework/UpdateData/ModSiteKey.cs | 2 +- .../Framework/UpdateData/UpdateKey.cs | 47 ++++++------- src/SMAPI.Web/Controllers/ModsApiController.cs | 6 +- .../Framework/Clients/GenericModDownload.cs | 27 ++++---- src/SMAPI.Web/Framework/Clients/GenericModPage.cs | 19 ++--- .../UpdateManifest/IUpdateManifestClient.cs | 3 +- .../UpdateManifest/TextAsJsonMediaTypeFormatter.cs | 18 +++-- .../Clients/UpdateManifest/UpdateManifestClient.cs | 37 ++++++---- .../UpdateManifest/UpdateManifestModDownload.cs | 40 ++++++----- .../UpdateManifest/UpdateManifestModModel.cs | 32 +++++---- .../UpdateManifest/UpdateManifestModPage.cs | 80 +++++++++++++--------- .../Clients/UpdateManifest/UpdateManifestModel.cs | 30 +++++--- .../UpdateManifest/UpdateManifestVersionModel.cs | 28 +++++--- src/SMAPI.Web/Framework/IModDownload.cs | 13 ++-- src/SMAPI.Web/Framework/IModPage.cs | 18 ++--- src/SMAPI.Web/Framework/ModInfoModel.cs | 37 ++++++---- src/SMAPI.Web/Framework/ModSiteManager.cs | 72 ++++++++++++------- 17 files changed, 293 insertions(+), 216 deletions(-) diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs index d1dd9049..195b0367 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The Nexus Mods mod repository. Nexus, - /// An arbitrary URL for an update manifest file. + /// An arbitrary URL to a JSON file containing update data. UpdateManifest } } diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 7c2cc73c..3e8064fd 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -54,29 +54,6 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData public UpdateKey(ModSiteKey site, string? id, string? subkey) : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } - /// - /// Split a string into two at a delimiter. If the delimiter does not appear in the string then the second - /// value of the returned tuple is null. Both returned strings are trimmed of whitespace. - /// - /// The string to split. - /// The character on which to split. - /// - /// If true then the second string returned will include the delimiter character - /// (provided that the string is not null) - /// - /// - /// A pair containing the string consisting of all characters in before the first - /// occurrence of , and a string consisting of all characters in - /// after the first occurrence of or null if the delimiter does not - /// exist in s. Both strings are trimmed of whitespace. - /// - private static (string, string?) Bifurcate(string s, char delimiter, bool keepDelimiter = false) { - int pos = s.IndexOf(delimiter); - if (pos < 0) - return (s.Trim(), null); - return (s.Substring(0, pos).Trim(), s.Substring(pos + (keepDelimiter ? 0 : 1)).Trim()); - } - /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string? raw) @@ -84,14 +61,14 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData if (raw is null) return new UpdateKey(raw, ModSiteKey.Unknown, null, null); // extract site + ID - (string rawSite, string? id) = Bifurcate(raw, ':'); - if (string.IsNullOrWhiteSpace(id)) + (string rawSite, string? id) = UpdateKey.SplitTwoParts(raw, ':'); + if (string.IsNullOrEmpty(id)) id = null; // extract subkey string? subkey = null; if (id != null) - (id, subkey) = Bifurcate(id, '@', true); + (id, subkey) = UpdateKey.SplitTwoParts(id, '@', true); // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) @@ -160,5 +137,23 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData { return $"{site}:{id}{subkey}".Trim(); } + + + /********* + ** Private methods + *********/ + /// Split a string into two parts at a delimiter and trim whitespace. + /// The string to split. + /// The character on which to split. + /// Whether to include the delimiter in the second string. + /// Returns a tuple containing the two strings, with the second value null if the delimiter wasn't found. + private static (string, string?) SplitTwoParts(string str, char delimiter, bool keepDelimiter = false) + { + int splitIndex = str.IndexOf(delimiter); + + return splitIndex >= 0 + ? (str.Substring(0, splitIndex).Trim(), str.Substring(splitIndex + (keepDelimiter ? 0 : 1)).Trim()) + : (str.Trim(), null); + } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1c34f2af..5fc4987d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Web.Controllers /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - /// The UpdateManifest client. + /// The API client for arbitrary update manifest URLs. public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); @@ -163,9 +163,9 @@ namespace StardewModdingAPI.Web.Controllers // if there's only a prerelease version (e.g. from GitHub), don't override the main version ISemanticVersion? curMain = data.Version; - string? curMainUrl = data.MainVersionUrl; ISemanticVersion? curPreview = data.PreviewVersion; - string? curPreviewUrl = data.PreviewVersionUrl; + string? curMainUrl = data.MainModPageUrl; + string? curPreviewUrl = data.PreviewModPageUrl; if (curPreview == null && curMain?.IsPrerelease() == true) { curPreview = curMain; diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 5cc03aba..6c9c08ef 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -17,10 +17,9 @@ 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; } + /// The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from. + public string? ModPageUrl { get; } + /********* ** Public methods @@ -29,23 +28,21 @@ namespace StardewModdingAPI.Web.Framework.Clients /// The download's display name. /// The download's description. /// The download's file version. - /// The download's URL (if different from the mod page's URL). - public GenericModDownload(string name, string? description, string? version, string? url = null) + /// The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from. + public GenericModDownload(string name, string? description, string? version, string? modPageUrl = null) { this.Name = name; this.Description = description; this.Version = version; - this.Url = url; + this.ModPageUrl = modPageUrl; } - /// - /// Return if the subkey matches this download. A subkey matches if it appears as - /// a substring in the name or description. - /// - /// the subkey - /// if matches this download, otherwise - public virtual bool MatchesSubkey(string subkey) { - return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true + /// Get whether the subkey matches this download. + /// The update subkey to check. + public virtual bool MatchesSubkey(string subkey) + { + return + this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) || 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 4c66e1a0..e939f1d8 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,9 +40,10 @@ 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. + /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. public bool IsSubkeyStrict { get; set; } = false; + /********* ** Public methods *********/ @@ -82,17 +83,17 @@ namespace StardewModdingAPI.Web.Framework.Clients return this; } - /// Returns the mod page name. - /// ignored - /// The mod page name. - public virtual string? GetName(string? subkey) { + /// Get the mod name for an update subkey, if different from the mod page name. + /// The update subkey. + 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) { + /// Get the mod page URL for an update subkey, if different from the mod page it was fetched from. + /// The update subkey. + 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 index 36d018c7..dd9f5811 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -3,7 +3,6 @@ using System; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// An HTTP client for fetching an update manifest from an arbitrary URL. + /// An API client for fetching update metadata from an arbitrary JSON 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 index 48e3c294..02722cb1 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs @@ -1,14 +1,18 @@ -// Copyright 2022 Jamie Taylor +// 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 { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// A that can parse from content of type text/plain. + internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter + { + /********* + ** Public methods + *********/ /// Construct a new - public TextAsJsonMediaTypeFormatter() { + 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 index d7cf4945..88a5c2f6 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,19 +1,21 @@ // Copyright 2022 Jamie Taylor -using System; +using System.Net; +using System.Threading.Tasks; 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 { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// An API client for fetching update metadata from an arbitrary JSON URL. + internal class UpdateManifestClient : IUpdateManifestClient + { /********* ** Fields *********/ /// The underlying HTTP client. private readonly IClient Client; + /********* ** Accessors *********/ @@ -26,30 +28,35 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { *********/ /// Construct an instance. /// The user agent for the API client. - public UpdateManifestClient(string userAgent) { + 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() { + public void Dispose() + { this.Client.Dispose(); } /// - public async Task GetModData(string id) { + public async Task GetModData(string id) + { UpdateManifestModel? manifest; - try { + 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}"); + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } - return new UpdateManifestModPage(id, manifest); + return manifest is not null + ? new UpdateManifestModPage(id, manifest) + : new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"The update manifest at {id} has an invalid format"); } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs index 117ae15c..0a6d4736 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -1,27 +1,35 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +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; + internal class UpdateManifestModDownload : GenericModDownload + { + /********* + ** Fields + *********/ + /// The update subkey for this mod download. + private readonly string Subkey; + + + /********* + ** Public methods + *********/ /// Construct an instance. - /// The subkey for this download. + /// The field name for this mod download in the manifest. /// 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; + public UpdateManifestModDownload(string fieldName, string name, string? version, string? url) + : base(name, null, version, url) + { + this.Subkey = fieldName; } - /// - /// 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; + /// Get whether the subkey matches this download. + /// The update subkey to check. + public override bool MatchesSubkey(string subkey) + { + return subkey == this.Subkey; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs index 4ec9c03d..92642321 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs @@ -1,26 +1,34 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// Data model for a mod in an update manifest. - internal class UpdateManifestModModel { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// The data model for a mod in an update manifest file. + internal class UpdateManifestModModel + { + /********* + ** Accessors + *********/ /// The mod's name. - public string Name { get; } + public string? Name { get; } - /// The mod's URL. + /// The mod page URL from which to download updates. public string? Url { get; } - /// The versions for this mod. - public UpdateManifestVersionModel[] Versions { get; } + /// The available versions for this mod. + public UpdateManifestVersionModel[]? Versions { get; } + + /********* + ** Public methods + *********/ /// Construct an instance. /// The mod's name. - /// The mod's URL. - /// The versions for this mod. - public UpdateManifestModModel(string name, string? url, UpdateManifestVersionModel[] versions) { + /// The mod page URL from which to download updates. + /// The available 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 index 109175b5..bbc7b5da 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -1,58 +1,74 @@ -// Copyright 2022 Jamie Taylor -using System; +// Copyright 2022 Jamie Taylor using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +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) { + internal class UpdateManifestModPage : GenericModPage + { + /********* + ** Fields + *********/ + /// The mods from the update manifest. + private readonly IDictionary Mods; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The URL of the update manifest file. + /// The parsed update manifest. + public UpdateManifestModPage(string url, UpdateManifestModel manifest) + : base(ModSiteKey.UpdateManifest, url) + { this.IsSubkeyStrict = true; - this.manifest = manifest; - this.SetInfo(name: id, url: id, version: null, downloads: TranslateDownloads(manifest).ToArray()); + this.Mods = manifest.Mods ?? new Dictionary(); + this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).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; + public override string? GetName(string? subkey) + { + return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + ? modModel.Name + : null; } /// 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; + public override string? GetUrl(string? subkey) + { + return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + ? modModel.Url + : null; } /********* ** 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); - } + /// Convert the raw download info from an update manifest to . + /// The mods from the update manifest. + private IEnumerable ParseDownloads(IDictionary? mods) + { + if (mods is null) + yield break; + + foreach ((string modKey, UpdateManifestModModel mod) in mods) + { + if (mod.Versions is null) + continue; + + foreach (UpdateManifestVersionModel version in mod.Versions) + yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, 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 index 03f89726..ad618022 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs @@ -1,23 +1,31 @@ // 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 { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// The data model for an update manifest file. + internal class UpdateManifestModel + { + /********* + ** Accessors + *********/ /// The manifest format version. - public string ManifestVersion { get; } + public string? ManifestVersion { get; } - /// The subkeys in this update manifest. - public IDictionary Subkeys { get; } + /// The mod info in this update manifest. + public IDictionary? Mods { get; } + + /********* + ** Public methods + *********/ /// Construct an instance. /// The manifest format version. - /// The subkeys in this update manifest. - public UpdateManifestModel(string manifestVersion, IDictionary subkeys) { + /// The mod info in this update manifest. + public UpdateManifestModel(string manifestVersion, IDictionary mods) + { this.ManifestVersion = manifestVersion; - this.Subkeys = subkeys; + this.Mods = mods; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs index 55b6db61..90e054d8 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs @@ -1,10 +1,14 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +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; } + internal class UpdateManifestVersionModel + { + /********* + ** Accessors + *********/ + /// The mod's semantic version. + public string? Version { get; } /// The URL for this version's download page (if any). public string? DownloadPageUrl { get; } @@ -12,15 +16,19 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { /// The URL for this version's direct file download (if any). public string? DownloadFileUrl { get; } + + /********* + ** Public methods + *********/ /// 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) { + /// The mod's semantic version. + /// 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; } } } - diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs index b0e9a664..8cb82989 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -15,12 +15,15 @@ namespace StardewModdingAPI.Web.Framework /// The download's file version. string? Version { get; } - /// This download's URL (if it has a URL that is different from the containing mod page's URL). - string? Url { get; } + /// The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from. + string? ModPageUrl { get; } - /// Return iff the subkey matches this download - /// the subkey - /// if matches this download, otherwise + + /********* + ** Methods + *********/ + /// Get whether the subkey matches this download. + /// The update subkey to check. bool MatchesSubkey(string subkey); } } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 3fc8bb81..ef1513eb 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,25 +39,19 @@ namespace StardewModdingAPI.Web.Framework [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } - /// - /// Does this page use strict subkey matching. Pages that use string subkey matching do not fall back - /// to searching for versions without a subkey if there are no versions found when given a subkey. - /// Additionally, the leading @ is stripped from the subkey value before searching for matches. - /// + /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. Additionally, the leading @ is stripped from the subkey value before searching for matches. bool IsSubkeyStrict { get; } + /********* ** Methods *********/ - - /// Get the mod name associated with the given subkey, if any. - /// The subkey. - /// The mod name associated with the given subkey (if any) + /// Get the mod name for an update subkey, if different from the mod page name. + /// The update subkey. string? GetName(string? subkey); - /// Get the URL for the mod associated with the given subkey, if any. - /// The subkey. - /// The URL for the mod associated with the given subkey (if any) + /// Get the mod page URL for an update subkey, if different from the mod page it was fetched from. + /// The update subkey. string? GetUrl(string? subkey); /// Set the fetched mod info. diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs index 17415589..502c0827 100644 --- a/src/SMAPI.Web/Framework/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -27,11 +27,12 @@ namespace StardewModdingAPI.Web.Framework /// The error message indicating why the mod is invalid (if applicable). public string? Error { get; private set; } - /// The URL associated with the mod's latest version (if distinct from the mod page's URL). - public string? MainVersionUrl { get; private set; } + /// The mod page URL from which can be downloaded, if different from the . + public string? MainModPageUrl { get; private set; } + + /// The mod page URL from which can be downloaded, if different from the . + public string? PreviewModPageUrl { get; private set; } - /// The URL associated with the mod's (if distinct from the mod page's URL). - public string? PreviewVersionUrl { get; private set; } /********* ** Public methods @@ -51,7 +52,8 @@ namespace StardewModdingAPI.Web.Framework { this .SetBasicInfo(name, url) - .SetVersions(version!, previewVersion) + .SetMainVersion(version!) + .SetPreviewVersion(previewVersion) .SetError(status, error!); } @@ -67,18 +69,25 @@ namespace StardewModdingAPI.Web.Framework return this; } - /// Set the mod version info. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The URL associated with , if different from the mod page's URL. - /// The URL associated with , if different from the mod page's URL. + /// Set the mod's main version info. + /// The semantic version for the mod's latest stable release. + /// The mod page URL from which can be downloaded, if different from the . [MemberNotNull(nameof(ModInfoModel.Version))] - public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null, string? mainVersionUrl = null, string? previewVersionUrl = null) + public ModInfoModel SetMainVersion(ISemanticVersion version, string? modPageUrl = null) { this.Version = version; - this.PreviewVersion = previewVersion; - this.MainVersionUrl = mainVersionUrl; - this.PreviewVersionUrl = previewVersionUrl; + this.MainModPageUrl = modPageUrl; + + return this; + } + + /// Set the mod's preview version info. + /// The semantic version for the mod's latest preview release. + /// The mod page URL from which can be downloaded, if different from the . + public ModInfoModel SetPreviewVersion(ISemanticVersion? version, string? modPageUrl = null) + { + this.PreviewVersion = version; + this.PreviewModPageUrl = modPageUrl; return this; } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 5581d799..350159a3 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -66,25 +66,34 @@ namespace StardewModdingAPI.Web.Framework { // get base model ModInfoModel model = new(); - if (!page.IsValid) { + if (!page.IsValid) + { model.SetError(page.Status, page.Error); return model; } - if (page.IsSubkeyStrict && subkey is not null) { - if (subkey.Length > 0 && subkey[0] == '@') { + // trim subkey in strict mode + if (page.IsSubkeyStrict && subkey is not null) + { + if (subkey.StartsWith('@')) subkey = subkey.Substring(1); - } } // fetch versions - bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl); + bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); if (!hasVersions) return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions."); - model.SetBasicInfo(page.GetName(subkey) ?? page.Name, page.GetUrl(subkey) ?? page.Url); + // apply mod page info + model.SetBasicInfo( + name: page.GetName(subkey) ?? page.Name, + url: page.GetUrl(subkey) ?? page.Url + ); + // return info - return model.SetVersions(mainVersion!, previewVersion, mainVersionUrl, previewVersionUrl); + return model + .SetMainVersion(mainVersion!, mainModPageUrl) + .SetPreviewVersion(previewVersion, previewModPageUrl); } /// Get a semantic local version for update checks. @@ -115,12 +124,14 @@ namespace StardewModdingAPI.Web.Framework /// The changes to apply to remote versions for update checks. /// The main mod version. /// The latest prerelease version, if newer than . - private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainVersionUrl, out string? previewVersionUrl) + /// The mod page URL from which can be downloaded, if different from the 's URL. + /// The mod page URL from which can be downloaded, if different from the 's URL. + private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainModPageUrl, out string? previewModPageUrl) { main = null; preview = null; - mainVersionUrl = null; - previewVersionUrl = null; + mainModPageUrl = null; + previewModPageUrl = null; // parse all versions from the mod page IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() @@ -152,12 +163,12 @@ namespace StardewModdingAPI.Web.Framework .ToArray(); // get main + preview versions - void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) + void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainUrl, out string? previewUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) { mainVersion = null; previewVersion = null; - mainVersionUrl = null; - previewVersionUrl = null; + mainUrl = null; + previewUrl = null; // get latest main + preview version foreach ((IModDownload? download, ISemanticVersion? version) entry in versions) @@ -165,14 +176,18 @@ namespace StardewModdingAPI.Web.Framework if (entry.version is null || filter?.Invoke(entry) == false) continue; - if (entry.version.IsPrerelease()) { - if (previewVersion is null) { + if (entry.version.IsPrerelease()) + { + if (previewVersion is null) + { previewVersion = entry.version; - previewVersionUrl = entry.download?.Url; + previewUrl = entry.download?.ModPageUrl; } - } else { + } + else + { mainVersion = entry.version; - mainVersionUrl = entry.download?.Url; + mainUrl = entry.download?.ModPageUrl; break; // any others will be older since entries are sorted by version } } @@ -180,26 +195,31 @@ namespace StardewModdingAPI.Web.Framework // normalize values if (previewVersion is not null) { - if (mainVersion is null) { + if (mainVersion is null) + { // if every version is prerelease, latest one is the main version mainVersion = previewVersion; - mainVersionUrl = previewVersionUrl; + mainUrl = previewUrl; } - if (!previewVersion.IsNewerThan(mainVersion)) { + if (!previewVersion.IsNewerThan(mainVersion)) + { previewVersion = null; - previewVersionUrl = null; + previewUrl = null; } } } - if (subkey is not null) { - TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl, entry => entry.download?.MatchesSubkey(subkey) == true); + // get versions for subkey + if (subkey is not null) + { + TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true); if (mod?.IsSubkeyStrict == true) return main != null; } - if (main is null) - TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl); + // fallback to non-subkey versions + if (main is null) + TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl); return main != null; } -- cgit From 25c2081d43bd4026552cda687fb56216dd3a9f8e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: encapsulate update manifest implementation details when possible --- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- .../UpdateManifest/UpdateManifestModDownload.cs | 2 +- .../UpdateManifest/UpdateManifestModPage.cs | 4 +- src/SMAPI.Web/Framework/IModPage.cs | 2 +- src/SMAPI.Web/Framework/ModSiteManager.cs | 63 +++++++++++----------- src/SMAPI.Web/SMAPI.Web.csproj | 4 -- 6 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 5fc4987d..2003e25f 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -301,7 +301,7 @@ namespace StardewModdingAPI.Web.Controllers } // get version info - return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions); + return this.ModSites.GetPageVersions(page, updateKey, allowNonStandardVersions, mapRemoteVersions); } /// Get update keys based on the available mod metadata, while maintaining the precedence order. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs index 0a6d4736..0128fa17 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest public UpdateManifestModDownload(string fieldName, string name, string? version, string? url) : base(name, null, version, url) { - this.Subkey = fieldName; + this.Subkey = '@' + fieldName; } /// Get whether the subkey matches this download. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index bbc7b5da..befad268 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest /// The mod name for the given subkey, or if this manifest does not contain the given subkey. public override string? GetName(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) ? modModel.Name : null; } @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest /// The mod URL for the given subkey, or if this manifest does not contain the given subkey. public override string? GetUrl(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) ? modModel.Url : null; } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index ef1513eb..84af9516 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,7 +39,7 @@ namespace StardewModdingAPI.Web.Framework [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } - /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. Additionally, the leading @ is stripped from the subkey value before searching for matches. + /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. bool IsSubkeyStrict { get; } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 350159a3..f1a088d5 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -59,11 +59,13 @@ namespace StardewModdingAPI.Web.Framework /// Parse version info for the given mod page info. /// The mod page info. - /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) + /// The update key to match in available files. /// The changes to apply to remote versions for update checks. /// Whether to allow non-standard versions. - public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) + public ModInfoModel GetPageVersions(IModPage page, UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) { + bool isManifest = updateKey.Site == ModSiteKey.UpdateManifest; + // get base model ModInfoModel model = new(); if (!page.IsValid) @@ -71,23 +73,23 @@ namespace StardewModdingAPI.Web.Framework model.SetError(page.Status, page.Error); return model; } - - // trim subkey in strict mode - if (page.IsSubkeyStrict && subkey is not null) - { - if (subkey.StartsWith('@')) - subkey = subkey.Substring(1); - } + else if (!isManifest) // if this is a manifest, the 'mod page' is the JSON file + model.SetBasicInfo(page.Name, page.Url); // fetch versions - bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); + bool hasVersions = this.TryGetLatestVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); if (!hasVersions) - return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions."); + { + string displayId = isManifest + ? page.Id + updateKey.Subkey + : page.Id; + return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{displayId}' has no valid versions."); + } // apply mod page info model.SetBasicInfo( - name: page.GetName(subkey) ?? page.Name, - url: page.GetUrl(subkey) ?? page.Url + name: page.GetName(updateKey.Subkey) ?? page.Name, + url: page.GetUrl(updateKey.Subkey) ?? page.Url ); // return info @@ -132,30 +134,29 @@ namespace StardewModdingAPI.Web.Framework preview = null; mainModPageUrl = null; previewModPageUrl = null; + if (mod is null) + return false; // parse all versions from the mod page IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() { - if (mod != null) + ISemanticVersion? ParseAndMapVersion(string? raw) { - ISemanticVersion? ParseAndMapVersion(string? raw) - { - raw = this.NormalizeVersion(raw); - return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions); - } + raw = this.NormalizeVersion(raw); + return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions); + } - // get mod version - ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); - if (modVersion != null) - yield return (download: null, version: modVersion); + // get mod version + ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); + if (modVersion != null) + yield return (download: null, version: modVersion); - // get file versions - foreach (IModDownload download in mod.Downloads) - { - ISemanticVersion? cur = ParseAndMapVersion(download.Version); - if (cur != null) - yield return (download, cur); - } + // get file versions + foreach (IModDownload download in mod.Downloads) + { + ISemanticVersion? cur = ParseAndMapVersion(download.Version); + if (cur != null) + yield return (download, cur); } } var versions = GetAllVersions() @@ -213,7 +214,7 @@ namespace StardewModdingAPI.Web.Framework if (subkey is not null) { TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true); - if (mod?.IsSubkeyStrict == true) + if (mod.IsSubkeyStrict) return main != null; } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index cfec1d08..d26cb6f8 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,7 +12,6 @@ - @@ -49,7 +48,4 @@ PreserveNewest - - - -- cgit From e5576d9c925210c83ba9f123c2ced86377ece560 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: require subkey for update manifest checks --- src/SMAPI.Web/Framework/Clients/GenericModPage.cs | 4 +-- .../UpdateManifest/UpdateManifestModPage.cs | 2 +- src/SMAPI.Web/Framework/IModPage.cs | 4 +-- src/SMAPI.Web/Framework/ModSiteManager.cs | 29 +++++++++------------- src/SMAPI.Web/Framework/RemoteModStatus.cs | 3 +++ 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index e939f1d8..63ca5a95 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,8 +40,8 @@ namespace StardewModdingAPI.Web.Framework.Clients [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] public bool IsValid => this.Status == RemoteModStatus.Ok; - /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. - public bool IsSubkeyStrict { get; set; } = false; + /// Whether this mod page requires update subkeys and does not allow matching downloads without them. + public bool RequireSubkey { get; set; } = false; /********* diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index befad268..7537eb24 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest public UpdateManifestModPage(string url, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, url) { - this.IsSubkeyStrict = true; + this.RequireSubkey = true; this.Mods = manifest.Mods ?? new Dictionary(); this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray()); } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 84af9516..85be41e2 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,8 +39,8 @@ namespace StardewModdingAPI.Web.Framework [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } - /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. - bool IsSubkeyStrict { get; } + /// Whether this mod page requires update subkeys and does not allow matching downloads without them. + bool RequireSubkey { get; } /********* diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index f1a088d5..4bb72f78 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -64,27 +64,26 @@ namespace StardewModdingAPI.Web.Framework /// Whether to allow non-standard versions. public ModInfoModel GetPageVersions(IModPage page, UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) { - bool isManifest = updateKey.Site == ModSiteKey.UpdateManifest; + // get ID to show in errors + string displayId = page.RequireSubkey + ? page.Id + updateKey.Subkey + : page.Id; - // get base model + // validate ModInfoModel model = new(); if (!page.IsValid) - { - model.SetError(page.Status, page.Error); - return model; - } - else if (!isManifest) // if this is a manifest, the 'mod page' is the JSON file + return model.SetError(page.Status, page.Error); + if (page.RequireSubkey && updateKey.Subkey is null) + return model.SetError(RemoteModStatus.RequiredSubkeyMissing, $"The {page.Site} mod with ID '{displayId}' requires an update subkey indicating which mod to fetch."); + + // add basic info (unless it's a manifest, in which case the 'mod page' is the JSON file) + if (updateKey.Site != ModSiteKey.UpdateManifest) model.SetBasicInfo(page.Name, page.Url); // fetch versions bool hasVersions = this.TryGetLatestVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); if (!hasVersions) - { - string displayId = isManifest - ? page.Id + updateKey.Subkey - : page.Id; return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{displayId}' has no valid versions."); - } // apply mod page info model.SetBasicInfo( @@ -212,14 +211,10 @@ namespace StardewModdingAPI.Web.Framework // get versions for subkey if (subkey is not null) - { TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true); - if (mod.IsSubkeyStrict) - return main != null; - } // fallback to non-subkey versions - if (main is null) + if (main is null && !mod.RequireSubkey) TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl); return main != null; } diff --git a/src/SMAPI.Web/Framework/RemoteModStatus.cs b/src/SMAPI.Web/Framework/RemoteModStatus.cs index 139ecfd3..235bcec4 100644 --- a/src/SMAPI.Web/Framework/RemoteModStatus.cs +++ b/src/SMAPI.Web/Framework/RemoteModStatus.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework /// The mod does not exist. DoesNotExist, + /// The mod page exists, but it requires a subkey and none was provided. + RequiredSubkeyMissing, + /// The mod was temporarily unavailable (e.g. the site could not be reached or an unknown error occurred). TemporaryError } -- cgit From 5c22406c13ef4933a5e17e9036d9fd3ca9b9a9a7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: adjust JSON formatter instead of adding a new one --- .../UpdateManifest/TextAsJsonMediaTypeFormatter.cs | 19 ------------------- .../Clients/UpdateManifest/UpdateManifestClient.cs | 4 +++- 2 files changed, 3 insertions(+), 20 deletions(-) delete mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs deleted file mode 100644 index 02722cb1..00000000 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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 - { - /********* - ** Public methods - *********/ - /// 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 index 88a5c2f6..0199027f 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,5 +1,6 @@ // Copyright 2022 Jamie Taylor using System.Net; +using System.Net.Http.Headers; using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -32,7 +33,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { this.Client = new FluentClient() .SetUserAgent(userAgent); - this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter()); + + this.Client.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. -- cgit From d76964c5f381a5b8faba123646d851e7ae2c06f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: group response models --- .../ResponseModels/UpdateManifestModModel.cs | 34 ++++++++++++++++++++++ .../ResponseModels/UpdateManifestModel.cs | 31 ++++++++++++++++++++ .../ResponseModels/UpdateManifestVersionModel.cs | 34 ++++++++++++++++++++++ .../Clients/UpdateManifest/UpdateManifestClient.cs | 1 + .../UpdateManifest/UpdateManifestModModel.cs | 34 ---------------------- .../UpdateManifest/UpdateManifestModPage.cs | 1 + .../Clients/UpdateManifest/UpdateManifestModel.cs | 31 -------------------- .../UpdateManifest/UpdateManifestVersionModel.cs | 34 ---------------------- 8 files changed, 101 insertions(+), 99 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs new file mode 100644 index 00000000..ee1fbeb6 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs @@ -0,0 +1,34 @@ +// Copyright 2022 Jamie Taylor +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels +{ + /// The data model for a mod in an update manifest file. + internal class UpdateManifestModModel + { + /********* + ** Accessors + *********/ + /// The mod's name. + public string? Name { get; } + + /// The mod page URL from which to download updates. + public string? Url { get; } + + /// The available versions for this mod. + public UpdateManifestVersionModel[]? Versions { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's name. + /// The mod page URL from which to download updates. + /// The available 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/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs new file mode 100644 index 00000000..e213fbab --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -0,0 +1,31 @@ +// Copyright 2022 Jamie Taylor +using System.Collections.Generic; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels +{ + /// The data model for an update manifest file. + internal class UpdateManifestModel + { + /********* + ** Accessors + *********/ + /// The manifest format version. + public string? ManifestVersion { get; } + + /// The mod info in this update manifest. + public IDictionary? Mods { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The manifest format version. + /// The mod info in this update manifest. + public UpdateManifestModel(string manifestVersion, IDictionary mods) + { + this.ManifestVersion = manifestVersion; + this.Mods = mods; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs new file mode 100644 index 00000000..1e84501f --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs @@ -0,0 +1,34 @@ +// Copyright 2022 Jamie Taylor +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels +{ + /// Data model for a Version in an update manifest. + internal class UpdateManifestVersionModel + { + /********* + ** Accessors + *********/ + /// The mod's semantic version. + 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; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's semantic version. + /// 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; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 0199027f..0d19900e 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -4,6 +4,7 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs deleted file mode 100644 index 92642321..00000000 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2022 Jamie Taylor -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest -{ - /// The data model for a mod in an update manifest file. - internal class UpdateManifestModModel - { - /********* - ** Accessors - *********/ - /// The mod's name. - public string? Name { get; } - - /// The mod page URL from which to download updates. - public string? Url { get; } - - /// The available versions for this mod. - public UpdateManifestVersionModel[]? Versions { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's name. - /// The mod page URL from which to download updates. - /// The available 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 index 7537eb24..251ed9ec 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs deleted file mode 100644 index ad618022..00000000 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2022 Jamie Taylor -using System.Collections.Generic; - -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest -{ - /// The data model for an update manifest file. - internal class UpdateManifestModel - { - /********* - ** Accessors - *********/ - /// The manifest format version. - public string? ManifestVersion { get; } - - /// The mod info in this update manifest. - public IDictionary? Mods { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The manifest format version. - /// The mod info in this update manifest. - public UpdateManifestModel(string manifestVersion, IDictionary mods) - { - this.ManifestVersion = manifestVersion; - this.Mods = mods; - } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs deleted file mode 100644 index 90e054d8..00000000 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2022 Jamie Taylor -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest -{ - /// Data model for a Version in an update manifest. - internal class UpdateManifestVersionModel - { - /********* - ** Accessors - *********/ - /// The mod's semantic version. - 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; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's semantic version. - /// 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 From 610e2722c6687591faacb942d6f752c5f3c620d7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: rename & validate format version --- .../ResponseModels/UpdateManifestModel.cs | 10 +++++----- .../Clients/UpdateManifest/UpdateManifestClient.cs | 21 ++++++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs index e213fbab..ff3dccbc 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -9,8 +9,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels /********* ** Accessors *********/ - /// The manifest format version. - public string? ManifestVersion { get; } + /// The manifest format version. This is equivalent to the SMAPI version, and is used to parse older manifests correctly if later versions of SMAPI change the expected format. + public string Format { get; } /// The mod info in this update manifest. public IDictionary? Mods { get; } @@ -20,11 +20,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels ** Public methods *********/ /// Construct an instance. - /// The manifest format version. + /// The manifest format version. /// The mod info in this update manifest. - public UpdateManifestModel(string manifestVersion, IDictionary mods) + public UpdateManifestModel(string format, IDictionary mods) { - this.ManifestVersion = manifestVersion; + this.Format = format; this.Mods = mods; } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 0d19900e..cd102ec5 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,8 +1,12 @@ // Copyright 2022 Jamie Taylor + +using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Headers; using System.Threading.Tasks; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels; @@ -45,21 +49,32 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest } /// + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method which ensures the annotations are correct.")] public async Task GetModData(string id) { + // get raw update manifest UpdateManifestModel? manifest; try { manifest = await this.Client.GetAsync(id).As(); + if (manifest is null) + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is empty"); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } + catch (Exception ex) + { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has an invalid format: {ex.Message}"); + } + + // validate + if (!SemanticVersion.TryParse(manifest.Format, out _)) + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has invalid format version '{manifest.Format}'"); - return manifest is not null - ? new UpdateManifestModPage(id, manifest) - : new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"The update manifest at {id} has an invalid format"); + // build model + return new UpdateManifestModPage(id, manifest); } } } -- cgit From 3eb98b565f48c26384f0e83e4012fc9b40f1d819 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: simplify & validate manifest mod page URLs This avoids an issue where users are told to download it from the JSON manifest URL. --- .../ResponseModels/UpdateManifestModModel.cs | 14 ++++++++------ .../UpdateManifest/ResponseModels/UpdateManifestModel.cs | 6 +++--- .../ResponseModels/UpdateManifestVersionModel.cs | 15 +++++---------- .../Clients/UpdateManifest/UpdateManifestClient.cs | 11 +++++++++++ .../Clients/UpdateManifest/UpdateManifestModPage.cs | 15 ++++++--------- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs index ee1fbeb6..418fb26b 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs @@ -1,4 +1,6 @@ // Copyright 2022 Jamie Taylor +using System; + namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels { /// The data model for a mod in an update manifest file. @@ -11,10 +13,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels public string? Name { get; } /// The mod page URL from which to download updates. - public string? Url { get; } + public string? ModPageUrl { get; } /// The available versions for this mod. - public UpdateManifestVersionModel[]? Versions { get; } + public UpdateManifestVersionModel[] Versions { get; } /********* @@ -22,13 +24,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels *********/ /// Construct an instance. /// The mod's name. - /// The mod page URL from which to download updates. + /// The mod page URL from which to download updates. /// The available versions for this mod. - public UpdateManifestModModel(string? name, string? url, UpdateManifestVersionModel[]? versions) + public UpdateManifestModModel(string? name, string? modPageUrl, UpdateManifestVersionModel[]? versions) { this.Name = name; - this.Url = url; - this.Versions = versions; + this.ModPageUrl = modPageUrl; + this.Versions = versions ?? Array.Empty(); } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs index ff3dccbc..3b930ff3 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels public string Format { get; } /// The mod info in this update manifest. - public IDictionary? Mods { get; } + public IDictionary Mods { get; } /********* @@ -22,10 +22,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels /// Construct an instance. /// The manifest format version. /// The mod info in this update manifest. - public UpdateManifestModel(string format, IDictionary mods) + public UpdateManifestModel(string format, IDictionary? mods) { this.Format = format; - this.Mods = mods; + this.Mods = mods ?? new Dictionary(); } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs index 1e84501f..7cfb0cfc 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs @@ -10,11 +10,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels /// The mod's semantic version. 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; } + /// The mod page URL from which to download updates, if different from . + public string? ModPageUrl { get; } /********* @@ -22,13 +19,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels *********/ /// Construct an instance. /// The mod's semantic version. - /// 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) + /// The mod page URL from which to download updates, if different from . + public UpdateManifestVersionModel(string version, string? modPageUrl) { this.Version = version; - this.DownloadPageUrl = downloadPageUrl; - this.DownloadFileUrl = downloadFileUrl; + this.ModPageUrl = modPageUrl; } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index cd102ec5..9a2887c2 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -72,6 +72,17 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest // validate if (!SemanticVersion.TryParse(manifest.Format, out _)) return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has invalid format version '{manifest.Format}'"); + foreach ((string modKey, UpdateManifestModModel mod) in manifest.Mods) + { + if (string.IsNullOrWhiteSpace(mod.ModPageUrl)) + { + foreach (UpdateManifestVersionModel download in mod.Versions) + { + if (string.IsNullOrWhiteSpace(download.ModPageUrl)) + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is invalid (all mod downloads must have a mod page URL)"); + } + } + } // build model return new UpdateManifestModPage(id, manifest); diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index 251ed9ec..f4ad0500 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest : base(ModSiteKey.UpdateManifest, url) { this.RequireSubkey = true; - this.Mods = manifest.Mods ?? new Dictionary(); + this.Mods = manifest.Mods; this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray()); } @@ -35,8 +35,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest /// The mod name for the given subkey, or if this manifest does not contain the given subkey. public override string? GetName(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) - ? modModel.Name + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod) + ? mod.Name : null; } @@ -45,8 +45,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest /// The mod URL for the given subkey, or if this manifest does not contain the given subkey. public override string? GetUrl(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) - ? modModel.Url + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod) + ? mod.ModPageUrl : null; } @@ -63,11 +63,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest foreach ((string modKey, UpdateManifestModModel mod) in mods) { - if (mod.Versions is null) - continue; - foreach (UpdateManifestVersionModel version in mod.Versions) - yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl); + yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, version.Version, version.ModPageUrl); } } -- cgit From d609294e2e4335b28a39eeb8746fe59cc55af495 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: don't allow update manifests before SMAPI 4.0.0 until the feature is released --- src/SMAPI.Web/Controllers/ModsApiController.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 2003e25f..f687c7dd 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -147,7 +147,12 @@ namespace StardewModdingAPI.Web.Controllers foreach (UpdateKey updateKey in updateKeys) { // validate update key - if (!updateKey.LooksValid) + if ( + !updateKey.LooksValid +#if SMAPI_DEPRECATED + || (updateKey.Site == ModSiteKey.UpdateManifest && apiVersion?.IsNewerThan("4.0.0-alpha") != true) // 4.0-alpha feature, don't make available to released mods in case it changes before release +#endif + ) { errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'."); continue; -- cgit From b5f46000f4583dbb15a97981376a3695be2f74f3 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sun, 29 Jan 2023 18:14:24 -0500 Subject: Remove copyright line from individual files --- src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs | 1 - .../Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs | 1 - .../Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs | 1 - .../Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs | 1 - src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs | 1 - .../Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs | 1 - src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs | 1 - 7 files changed, 7 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs index dd9f5811..bf1edd3f 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs index 418fb26b..ead5c229 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs index 3b930ff3..5ccd31b0 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System.Collections.Generic; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs index 7cfb0cfc..6678f5eb 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels { /// Data model for a Version in an update manifest. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 9a2887c2..6040c5b9 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System; using System.Diagnostics.CodeAnalysis; diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs index 0128fa17..f8cb760a 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { /// Metadata about a mod download in an update manifest file. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index f4ad0500..df752713 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; -- cgit From a4b193b920879b7db255f387af8e3c9b4fa2b46a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 19:06:10 -0500 Subject: add stricter validation for update manifests --- .../Clients/UpdateManifest/UpdateManifestClient.cs | 35 ++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 6040c5b9..1614beab 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -57,7 +57,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { manifest = await this.Client.GetAsync(id).As(); if (manifest is null) - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is empty"); + return this.GetFormatError(id, "manifest can't be empty"); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { @@ -65,26 +65,43 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest } catch (Exception ex) { - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has an invalid format: {ex.Message}"); + return this.GetFormatError(id, ex.Message); } // validate if (!SemanticVersion.TryParse(manifest.Format, out _)) - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has invalid format version '{manifest.Format}'"); - foreach ((string modKey, UpdateManifestModModel mod) in manifest.Mods) + return this.GetFormatError(id, $"invalid format version '{manifest.Format}'"); + foreach (UpdateManifestModModel mod in manifest.Mods.Values) { + if (mod is null) + return this.GetFormatError(id, "a mod record can't be null"); if (string.IsNullOrWhiteSpace(mod.ModPageUrl)) + return this.GetFormatError(id, $"all mods must have a {nameof(mod.ModPageUrl)} value"); + foreach (UpdateManifestVersionModel? version in mod.Versions) { - foreach (UpdateManifestVersionModel download in mod.Versions) - { - if (string.IsNullOrWhiteSpace(download.ModPageUrl)) - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is invalid (all mod downloads must have a mod page URL)"); - } + if (version is null) + return this.GetFormatError(id, "a version record can't be null"); + if (string.IsNullOrWhiteSpace(version.Version)) + return this.GetFormatError(id, $"all version records must have a {nameof(version.Version)} field"); + if (!SemanticVersion.TryParse(version.Version, out _)) + return this.GetFormatError(id, $"invalid mod version '{version.Version}'"); } } // build model return new UpdateManifestModPage(id, manifest); } + + + /********* + ** Private methods + *********/ + /// Get a mod page instance with an error indicating the update manifest is invalid. + /// The full URL to the update manifest. + /// A human-readable reason phrase indicating why it's invalid. + private IModPage GetFormatError(string url, string reason) + { + return new GenericModPage(this.SiteKey, url).SetError(RemoteModStatus.InvalidData, $"The update manifest at {url} is invalid ({reason})"); + } } } -- cgit