summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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