summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie Taylor <Jamie.Taylor@pobox.com>2022-10-01 21:09:39 -0400
committerJamie Taylor <Jamie.Taylor@pobox.com>2022-10-01 21:30:14 -0400
commit83aec980b3ee739fb4bc251217556b3ae44f741b (patch)
treeafa8c91eb20cf56ed2cb35563ba3bd2d1e4c8a46
parent6d4eed56b1d80d02db773b0cd2f372baec6b2d1b (diff)
downloadSMAPI-83aec980b3ee739fb4bc251217556b3ae44f741b.tar.gz
SMAPI-83aec980b3ee739fb4bc251217556b3ae44f741b.tar.bz2
SMAPI-83aec980b3ee739fb4bc251217556b3ae44f741b.zip
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).
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs5
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs14
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModDownload.cs15
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModPage.cs16
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs9
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs15
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs55
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs27
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs26
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs58
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs23
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs26
-rw-r--r--src/SMAPI.Web/Framework/IModDownload.cs7
-rw-r--r--src/SMAPI.Web/Framework/IModPage.cs17
-rw-r--r--src/SMAPI.Web/Framework/ModInfoModel.cs11
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs64
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj4
-rw-r--r--src/SMAPI.Web/Startup.cs3
18 files changed, 359 insertions, 36 deletions
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,
/// <summary>The Nexus Mods mod repository.</summary>
- Nexus
+ Nexus,
+
+ /// <summary>An arbitrary URL for an update manifest file.</summary>
+ 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
/// <param name="github">The GitHub API client.</param>
/// <param name="modDrop">The ModDrop API client.</param>
/// <param name="nexus">The Nexus API client.</param>
- public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
+ /// <param name="updateManifest">The UpdateManifest client.</param>
+ public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> 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 });
}
/// <summary>Fetch version metadata for the given mods.</summary>
@@ -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
/// <summary>The download's file version.</summary>
public string? Version { get; }
+ /// <summary>
+ /// The URL for this download, if it has one distinct from the mod page's URL.
+ /// </summary>
+ public string? Url { get; }
/*********
** Public methods
@@ -25,21 +29,22 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <param name="name">The download's display name.</param>
/// <param name="description">The download's description.</param>
/// <param name="version">The download's file version.</param>
- public GenericModDownload(string name, string? description, string? version)
+ /// <param name="url">The download's URL (if different from the mod page's URL).</param>
+ public GenericModDownload(string name, string? description, string? version, string? url = null)
{
this.Name = name;
this.Description = description;
this.Version = version;
+ this.Url = url;
}
/// <summary>
- /// Return true if the subkey matches this download. A subkey matches if it appears as
+ /// Return <see langword="true"/> if the subkey matches this download. A subkey matches if it appears as
/// a substring in the name or description.
/// </summary>
/// <param name="subkey">the subkey</param>
- /// <returns><c>true</c> if <paramref name="subkey"/> matches this download, otherwise <c>false</c></returns>
- /// <exception cref="System.NotImplementedException"></exception>
- public bool MatchesSubkey(string subkey) {
+ /// <returns><see langword="true"/> if <paramref name="subkey"/> matches this download, otherwise <see langword="false"/></returns>
+ 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;
+ /// <summary>Whether to use strict subkey matching or not.</summary>
+ public bool IsSubkeyStrict { get; set; } = false;
/*********
** Public methods
@@ -79,5 +81,19 @@ namespace StardewModdingAPI.Web.Framework.Clients
return this;
}
+
+ /// <summary>Returns the mod page name.</summary>
+ /// <param name="subkey">ignored</param>
+ /// <returns>The mod page name.</returns>
+ public virtual string? GetName(string? subkey) {
+ return this.Name;
+ }
+
+ /// <summary>Returns the mod page URL.</summary>
+ /// <param name="subkey">ignored</param>
+ /// <returns>The mod page URL.</returns>
+ 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
+{
+ /// <summary>An HTTP client for fetching an update manifest from an arbitrary URL.</summary>
+ 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 {
+ /// <summary>
+ /// A <see cref="JsonMediaTypeFormatter"/> that can parse from content of type <c>text/plain</c>.
+ /// </summary>
+ internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter {
+ /// <summary>Construct a new <see cref="JsonMediaTypeFormatter"/></summary>
+ 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 {
+ /// <summary>An HTTP client for fetching an update manifest from an arbitrary URL.</summary>
+ internal class UpdateManifestClient : IUpdateManifestClient {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The underlying HTTP client.</summary>
+ private readonly IClient Client;
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The unique key for the mod site.</summary>
+ public ModSiteKey SiteKey => ModSiteKey.UpdateManifest;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="userAgent">The user agent for the API client.</param>
+ public UpdateManifestClient(string userAgent) {
+ this.Client = new FluentClient()
+ .SetUserAgent(userAgent);
+ this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter());
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose() {
+ this.Client.Dispose();
+ }
+
+ /// <inheritdoc/>
+ public async Task<IModPage?> GetModData(string id) {
+ UpdateManifestModel? manifest;
+ try {
+ manifest = await this.Client.GetAsync(id).As<UpdateManifestModel?>();
+ } 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 {
+ /// <summary>Metadata about a mod download in an update manifest file.</summary>
+ internal class UpdateManifestModDownload : GenericModDownload {
+ /// <summary>The subkey for this mod download</summary>
+ private readonly string subkey;
+ /// <summary>Construct an instance.</summary>
+ /// <param name="subkey">The subkey for this download.</param>
+ /// <param name="name">The mod name for this download.</param>
+ /// <param name="version">The download's version.</param>
+ /// <param name="url">The download's URL.</param>
+ public UpdateManifestModDownload(string subkey, string name, string? version, string? url) : base(name, null, version, url) {
+ this.subkey = subkey;
+ }
+
+ /// <summary>
+ /// Returns <see langword="true"/> iff the given subkey is the same as the subkey for this download.
+ /// </summary>
+ /// <param name="subkey">The subkey to match</param>
+ /// <returns><see langword="true"/> if <paramref name="subkey"/> is the same as the subkey for this download, <see langword="false"/> otherwise.</returns>
+ 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 {
+ /// <summary>Data model for a mod in an update manifest.</summary>
+ internal class UpdateManifestModModel {
+ /// <summary>The mod's name.</summary>
+ public string Name { get; }
+
+ /// <summary>The mod's URL.</summary>
+ public string? Url { get; }
+
+ /// <summary>The versions for this mod.</summary>
+ public UpdateManifestVersionModel[] Versions { get; }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The mod's name.</param>
+ /// <param name="url">The mod's URL.</param>
+ /// <param name="versions">The versions for this mod.</param>
+ 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 {
+ /// <summary>Metadata about an update manifest "page".</summary>
+ internal class UpdateManifestModPage : GenericModPage {
+ /// <summary>The update manifest model.</summary>
+ private UpdateManifestModel manifest;
+
+ /// <summary>Constuct an instance.</summary>
+ /// <param name="id">The "id" (i.e., URL) of this update manifest.</param>
+ /// <param name="manifest">The manifest object model.</param>
+ 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());
+ }
+
+ /// <summary>Return the mod name for the given subkey, if it exists in this update manifest.</summary>
+ /// <param name="subkey">The subkey.</param>
+ /// <returns>The mod name for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns>
+ public override string? GetName(string? subkey) {
+ if (subkey is null)
+ return null;
+ this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel);
+ return modModel?.Name;
+ }
+
+ /// <summary>Return the mod URL for the given subkey, if it exists in this update manifest.</summary>
+ /// <param name="subkey">The subkey.</param>
+ /// <returns>The mod URL for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns>
+ 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
+ *********/
+ /// <summary>Translate the downloads from the manifest's object model into <see cref="IModDownload"/> objects.</summary>
+ /// <param name="manifest">The manifest object model.</param>
+ /// <returns>An <see cref="IModDownload"/> for each <see cref="UpdateManifestVersionModel"/> in the manifest.</returns>
+ private static IEnumerable<IModDownload> 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 {
+ /// <summary>Data model for an update manifest.</summary>
+ internal class UpdateManifestModel {
+ /// <summary>The manifest format version.</summary>
+ public string ManifestVersion { get; }
+
+ /// <summary>The subkeys in this update manifest.</summary>
+ public IDictionary<string, UpdateManifestModModel> Subkeys { get; }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="manifestVersion">The manifest format version.</param>
+ /// <param name="subkeys">The subkeys in this update manifest.</param>
+ public UpdateManifestModel(string manifestVersion, IDictionary<string, UpdateManifestModModel> 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 {
+ /// <summary>Data model for a Version in an update manifest.</summary>
+ internal class UpdateManifestVersionModel {
+ /// <summary>The semantic version string.</summary>
+ public string Version { get; }
+
+ /// <summary>The URL for this version's download page (if any).</summary>
+ public string? DownloadPageUrl { get; }
+
+ /// <summary>The URL for this version's direct file download (if any).</summary>
+ public string? DownloadFileUrl { get; }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="version">The semantic version string.</param>
+ /// <param name="downloadPageUrl">This version's download page URL (if any).</param>
+ /// <param name="downloadFileUrl">This version's direct file download URL (if any).</param>
+ 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
/// <summary>The download's file version.</summary>
string? Version { get; }
- /// <summary>Return <c>true</c> iff the subkey matches this download</summary>
+ /// <summary>This download's URL (if it has a URL that is different from the containing mod page's URL).</summary>
+ string? Url { get; }
+
+ /// <summary>Return <see langword="true"/> iff the subkey matches this download</summary>
/// <param name="subkey">the subkey</param>
- /// <returns><c>true</c> if <paramref name="subkey"/> matches this download, otherwise <c>false</c></returns>
+ /// <returns><see langword="true"/> if <paramref name="subkey"/> matches this download, otherwise <see langword="false"/></returns>
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; }
+ /// <summary>
+ /// 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 <c>@</c> is stripped from the subkey value before searching for matches.
+ /// </summary>
+ bool IsSubkeyStrict { get; }
/*********
** Methods
*********/
+
+ /// <summary>Get the mod name associated with the given subkey, if any.</summary>
+ /// <param name="subkey">The subkey.</param>
+ /// <returns>The mod name associated with the given subkey (if any)</returns>
+ string? GetName(string? subkey);
+
+ /// <summary>Get the URL for the mod associated with the given subkey, if any.</summary>
+ /// <param name="subkey">The subkey.</param>
+ /// <returns>The URL for the mod associated with the given subkey (if any)</returns>
+ string? GetUrl(string? subkey);
+
/// <summary>Set the fetched mod info.</summary>
/// <param name="name">The mod name.</param>
/// <param name="version">The mod's semantic version number.</param>
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
/// <summary>The error message indicating why the mod is invalid (if applicable).</summary>
public string? Error { get; private set; }
+ /// <summary>The URL associated with the mod's latest version (if distinct from the mod page's URL).</summary>
+ public string? MainVersionUrl { get; private set; }
+
+ /// <summary>The URL associated with the mod's <see cref="PreviewVersion"/> (if distinct from the mod page's URL).</summary>
+ public string? PreviewVersionUrl { get; private set; }
/*********
** Public methods
@@ -65,11 +70,15 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Set the mod version info.</summary>
/// <param name="version">The semantic version for the mod's latest release.</param>
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
+ /// <param name="mainVersionUrl">The URL associated with <paramref name="version"/>, if different from the mod page's URL.</param>
+ /// <param name="previewVersionUrl">The URL associated with <paramref name="previewVersion"/>, if different from the mod page's URL.</param>
[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);
}
/// <summary>Get a semantic local version for update checks.</summary>
@@ -113,10 +115,12 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="main">The main mod version.</param>
/// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param>
- 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 @@
<None Remove="Properties\PublishProfiles\**" />
<None Remove="Properties\ServiceDependencies\**" />
<Content Remove="aws-beanstalk-tools-defaults.json" />
+ <None Remove="Framework\Clients\UpdateManifest\" />
</ItemGroup>
<ItemGroup>
@@ -48,4 +49,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
+ <ItemGroup>
+ <Folder Include="Framework\Clients\UpdateManifest\" />
+ </ItemGroup>
</Project>
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<IUpdateManifestClient>(new UpdateManifestClient(userAgent: userAgent));
}
// init helpers