summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md1
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs1
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs32
-rw-r--r--src/SMAPI.Web/BackgroundService.cs7
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs54
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs12
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs39
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs20
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs20
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs5
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs6
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs12
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs24
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs10
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs42
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs8
-rw-r--r--src/SMAPI.Web/Startup.cs2
-rw-r--r--src/SMAPI.Web/appsettings.json2
18 files changed, 220 insertions, 77 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index b425112d..fd0cda51 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -26,6 +26,7 @@ These changes have not been released yet.
* Fixed map reloads not updating door warps.
* Fixed outdoor tilesheets being seasonalised when added to an indoor location.
* Fixed update checks failing for Nexus mods marked as adult content.
+ * Fixed update checks not recognising releases on GitHub if they're not explicitly listed as update keys.
* For the mod compatibility list:
* Now loads faster (since data is fetched in a background service).
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
index fff86891..06c44308 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
index 865ebcf7..3fc1759e 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
@@ -3,7 +3,7 @@ using System;
namespace StardewModdingAPI.Toolkit.Framework.UpdateData
{
/// <summary>A namespaced mod ID which uniquely identifies a mod within a mod repository.</summary>
- public class UpdateKey
+ public class UpdateKey : IEquatable<UpdateKey>
{
/*********
** Accessors
@@ -38,6 +38,12 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
&& !string.IsNullOrWhiteSpace(id);
}
+ /// <summary>Construct an instance.</summary>
+ /// <param name="repository">The mod repository containing the mod.</param>
+ /// <param name="id">The mod ID within the repository.</param>
+ public UpdateKey(ModRepositoryKey repository, string id)
+ : this($"{repository}:{id}", repository, id) { }
+
/// <summary>Parse a raw update key.</summary>
/// <param name="raw">The raw update key to parse.</param>
public static UpdateKey Parse(string raw)
@@ -69,5 +75,29 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
? $"{this.Repository}:{this.ID}"
: this.RawText;
}
+
+ /// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
+ /// <param name="other">An object to compare with this object.</param>
+ public bool Equals(UpdateKey other)
+ {
+ return
+ other != null
+ && this.Repository == other.Repository
+ && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase);
+ }
+
+ /// <summary>Determines whether the specified object is equal to the current object.</summary>
+ /// <param name="obj">The object to compare with the current object.</param>
+ public override bool Equals(object obj)
+ {
+ return obj is UpdateKey other && this.Equals(other);
+ }
+
+ /// <summary>Serves as the default hash function. </summary>
+ /// <returns>A hash code for the current object.</returns>
+ public override int GetHashCode()
+ {
+ return $"{this.Repository}:{this.ID}".ToLower().GetHashCode();
+ }
}
}
diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs
index 2dd3b921..dfd2c1b9 100644
--- a/src/SMAPI.Web/BackgroundService.cs
+++ b/src/SMAPI.Web/BackgroundService.cs
@@ -50,11 +50,11 @@ namespace StardewModdingAPI.Web
// set startup tasks
BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync());
- BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleMods());
+ BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleModsAsync());
// set recurring tasks
RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); // every 10 minutes
- RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleMods(), "0 * * * *"); // hourly
+ RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleModsAsync(), "0 * * * *"); // hourly
return Task.CompletedTask;
}
@@ -84,9 +84,10 @@ namespace StardewModdingAPI.Web
}
/// <summary>Remove mods which haven't been requested in over 48 hours.</summary>
- public static async Task RemoveStaleMods()
+ public static Task RemoveStaleModsAsync()
{
BackgroundService.ModCache.RemoveStaleMods(TimeSpan.FromHours(48));
+ return Task.CompletedTask;
}
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 195ee5bf..13dd5529 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -123,18 +123,33 @@ namespace StardewModdingAPI.Web.Controllers
// crossreference data
ModDataRecord record = this.ModDatabase.Get(search.ID);
WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
- string[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
+ UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
+
+ // add soft lookups (don't log errors if the target doesn't exist)
+ UpdateKey[] softUpdateKeys = updateKeys.All(key => key.Repository != ModRepositoryKey.GitHub) && !string.IsNullOrWhiteSpace(wikiEntry?.GitHubRepo)
+ ? new[] { new UpdateKey(ModRepositoryKey.GitHub, wikiEntry.GitHubRepo) }
+ : new UpdateKey[0];
// get latest versions
ModEntryModel result = new ModEntryModel { ID = search.ID };
IList<string> errors = new List<string>();
- foreach (string updateKey in updateKeys)
+ foreach (UpdateKey updateKey in updateKeys.Concat(softUpdateKeys))
{
+ bool isSoftLookup = softUpdateKeys.Contains(updateKey);
+
+ // validate update key
+ if (!updateKey.LooksValid)
+ {
+ errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
+ continue;
+ }
+
// fetch data
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey);
if (data.Error != null)
{
- errors.Add(data.Error);
+ if (!isSoftLookup || data.Status != RemoteModStatus.DoesNotExist)
+ errors.Add(data.Error);
continue;
}
@@ -221,32 +236,27 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get the mod info for an update key.</summary>
/// <param name="updateKey">The namespaced update key.</param>
- private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(string updateKey)
+ private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey)
{
- // parse update key
- UpdateKey parsed = UpdateKey.Parse(updateKey);
- if (!parsed.LooksValid)
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
-
// get mod
- if (!this.ModCache.TryGetMod(parsed.Repository, parsed.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes))
+ if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes))
{
// get site
- if (!this.Repositories.TryGetValue(parsed.Repository, out IModRepository repository))
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
+ if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
+ return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
// fetch mod
- ModInfoModel result = await repository.GetModInfoAsync(parsed.ID);
+ ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID);
if (result.Error == null)
{
if (result.Version == null)
- result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
+ result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
else if (!SemanticVersion.TryParse(result.Version, out _))
- result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
+ result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
}
// cache mod
- this.ModCache.SaveMod(repository.VendorKey, parsed.ID, result, out mod);
+ this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod);
}
return mod.GetModel();
}
@@ -255,7 +265,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="specifiedKeys">The specified update keys.</param>
/// <param name="record">The mod's entry in SMAPI's internal database.</param>
/// <param name="entry">The mod's entry in the wiki list.</param>
- public IEnumerable<string> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
+ public IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
{
IEnumerable<string> GetRaw()
{
@@ -283,10 +293,14 @@ namespace StardewModdingAPI.Web.Controllers
}
}
- HashSet<string> seen = new HashSet<string>(StringComparer.InvariantCulture);
- foreach (string key in GetRaw())
+ HashSet<UpdateKey> seen = new HashSet<UpdateKey>();
+ foreach (string rawKey in GetRaw())
{
- if (!string.IsNullOrWhiteSpace(key) && seen.Add(key))
+ if (string.IsNullOrWhiteSpace(rawKey))
+ continue;
+
+ UpdateKey key = UpdateKey.Parse(rawKey);
+ if (seen.Add(key))
yield return key;
}
}
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs b/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs
index fe8a7a1f..96eca847 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs
@@ -58,6 +58,12 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
/// <summary>The URL for the mod page.</summary>
public string Url { get; set; }
+ /// <summary>The license URL, if available.</summary>
+ public string LicenseUrl { get; set; }
+
+ /// <summary>The license name, if available.</summary>
+ public string LicenseName { get; set; }
+
/*********
** Accessors
@@ -86,12 +92,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
this.MainVersion = mod.Version;
this.PreviewVersion = mod.PreviewVersion;
this.Url = mod.Url;
+ this.LicenseUrl = mod.LicenseUrl;
+ this.LicenseName = mod.LicenseName;
}
/// <summary>Get the API model for the cached data.</summary>
public ModInfoModel GetModel()
{
- return new ModInfoModel(name: this.Name, version: this.MainVersion, previewVersion: this.PreviewVersion, url: this.Url).WithError(this.FetchStatus, this.FetchError);
+ return new ModInfoModel(name: this.Name, version: this.MainVersion, url: this.Url, previewVersion: this.PreviewVersion)
+ .SetLicense(this.LicenseUrl, this.LicenseName)
+ .SetError(this.FetchStatus, this.FetchError);
}
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
index 22950db9..84c20957 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
@@ -12,12 +12,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/*********
** Fields
*********/
- /// <summary>The URL for a GitHub API query for the latest stable release, excluding the base URL, where {0} is the organisation and project name.</summary>
- private readonly string StableReleaseUrlFormat;
-
- /// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the base URL, where {0} is the organisation and project name.</summary>
- private readonly string AnyReleaseUrlFormat;
-
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
@@ -27,17 +21,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
*********/
/// <summary>Construct an instance.</summary>
/// <param name="baseUrl">The base URL for the GitHub API.</param>
- /// <param name="stableReleaseUrlFormat">The URL for a GitHub API query for the latest stable release, excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param>
- /// <param name="anyReleaseUrlFormat">The URL for a GitHub API query for the latest release (including prerelease), excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param>
/// <param name="userAgent">The user agent for the API client.</param>
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
- public GitHubClient(string baseUrl, string stableReleaseUrlFormat, string anyReleaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
+ public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string username, string password)
{
- this.StableReleaseUrlFormat = stableReleaseUrlFormat;
- this.AnyReleaseUrlFormat = anyReleaseUrlFormat;
-
this.Client = new FluentClient(baseUrl)
.SetUserAgent(userAgent)
.AddDefault(req => req.WithHeader("Accept", acceptHeader));
@@ -45,25 +34,43 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
this.Client = this.Client.SetBasicAuthentication(username, password);
}
+ /// <summary>Get basic metadata for a GitHub repository, if available.</summary>
+ /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
+ /// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
+ public async Task<GitRepo> GetRepositoryAsync(string repo)
+ {
+ this.AssertKeyFormat(repo);
+ try
+ {
+ return await this.Client
+ .GetAsync($"repos/{repo}")
+ .As<GitRepo>();
+ }
+ catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+ }
+
/// <summary>Get the latest release for a GitHub repository.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
/// <returns>Returns the release if found, else <c>null</c>.</returns>
public async Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false)
{
- this.AssetKeyFormat(repo);
+ this.AssertKeyFormat(repo);
try
{
if (includePrerelease)
{
GitRelease[] results = await this.Client
- .GetAsync(string.Format(this.AnyReleaseUrlFormat, repo))
+ .GetAsync($"repos/{repo}/releases?per_page=2") // allow for draft release (only visible if GitHub repo is owned by same account as the update check credentials)
.AsArray<GitRelease>();
return results.FirstOrDefault(p => !p.IsDraft);
}
return await this.Client
- .GetAsync(string.Format(this.StableReleaseUrlFormat, repo))
+ .GetAsync($"repos/{repo}/releases/latest")
.As<GitRelease>();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
@@ -85,7 +92,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Assert that a repository key is formatted correctly.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <exception cref="ArgumentException">The repository key is invalid.</exception>
- private void AssetKeyFormat(string repo)
+ private void AssertKeyFormat(string repo)
{
if (repo == null || !repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo));
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs
new file mode 100644
index 00000000..736efbe6
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs
@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+
+namespace StardewModdingAPI.Web.Framework.Clients.GitHub
+{
+ /// <summary>The license info for a GitHub project.</summary>
+ internal class GitLicense
+ {
+ /// <summary>The license display name.</summary>
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ /// <summary>The SPDX ID for the license.</summary>
+ [JsonProperty("spdx_id")]
+ public string SpdxId { get; set; }
+
+ /// <summary>The URL for the license info.</summary>
+ [JsonProperty("url")]
+ public string Url { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
new file mode 100644
index 00000000..7d80576e
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+
+namespace StardewModdingAPI.Web.Framework.Clients.GitHub
+{
+ /// <summary>Basic metadata about a GitHub project.</summary>
+ internal class GitRepo
+ {
+ /// <summary>The full repository name, including the owner.</summary>
+ [JsonProperty("full_name")]
+ public string FullName { get; set; }
+
+ /// <summary>The URL to the repository web page, if any.</summary>
+ [JsonProperty("html_url")]
+ public string WebUrl { get; set; }
+
+ /// <summary>The code license, if any.</summary>
+ [JsonProperty("license")]
+ public GitLicense License { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
index 9519c26f..a34f03bd 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
@@ -9,6 +9,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/*********
** Methods
*********/
+ /// <summary>Get basic metadata for a GitHub repository, if available.</summary>
+ /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
+ /// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
+ Task<GitRepo> GetRepositoryAsync(string repo);
+
/// <summary>Get the latest release for a GitHub repository.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
index d2e9a2fe..a0a1f42a 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
@@ -29,12 +29,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The base URL for the GitHub API.</summary>
public string GitHubBaseUrl { get; set; }
- /// <summary>The URL for a GitHub API query for the latest stable release, excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
- public string GitHubStableReleaseUrlFormat { get; set; }
-
- /// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
- public string GitHubAnyReleaseUrlFormat { get; set; }
-
/// <summary>The Accept header value expected by the GitHub API.</summary>
public string GitHubAcceptHeader { get; set; }
diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
index 04c80dd2..c14fb45d 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
@@ -32,21 +32,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{
// validate ID format
if (!uint.TryParse(id, out uint realID))
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
+ return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
// fetch info
try
{
var mod = await this.Client.GetModAsync(realID);
- if (mod == null)
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID.");
-
- // create model
- return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url);
+ return mod != null
+ ? new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url)
+ : new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID.");
}
catch (Exception ex)
{
- return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
+ return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
}
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
index 614e00c2..0e254b39 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
@@ -30,36 +30,46 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/// <param name="id">The mod ID in this repository.</param>
public override async Task<ModInfoModel> GetModInfoAsync(string id)
{
+ ModInfoModel result = new ModInfoModel().SetBasicInfo(id, $"https://github.com/{id}/releases");
+
// validate ID format
if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'.");
+ return result.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'.");
// fetch info
try
{
+ // fetch repo info
+ GitRepo repository = await this.Client.GetRepositoryAsync(id);
+ if (repository == null)
+ return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID.");
+ result
+ .SetBasicInfo(repository.FullName, $"{repository.WebUrl}/releases")
+ .SetLicense(url: repository.License?.Url, name: repository.License?.Name);
+
// get latest release (whether preview or stable)
GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true);
if (latest == null)
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID.");
+ return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID.");
// split stable/prerelease if applicable
GitRelease preview = null;
if (latest.IsPrerelease)
{
- GitRelease result = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
- if (result != null)
+ GitRelease release = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
+ if (release != null)
{
preview = latest;
- latest = result;
+ latest = release;
}
}
// return data
- return new ModInfoModel(name: id, version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases");
+ return result.SetVersions(version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag));
}
catch (Exception ex)
{
- return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
+ return result.SetError(RemoteModStatus.TemporaryError, ex.ToString());
}
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs
index 5703b34e..62142668 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs
@@ -32,19 +32,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{
// validate ID format
if (!long.TryParse(id, out long modDropID))
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID.");
+ return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID.");
// fetch info
try
{
ModDropMod mod = await this.Client.GetModAsync(modDropID);
- if (mod == null)
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID.");
- return new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url);
+ return mod != null
+ ? new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url)
+ : new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID.");
}
catch (Exception ex)
{
- return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
+ return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
}
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs
index 15e6c213..46b98860 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs
@@ -18,6 +18,12 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/// <summary>The mod's web URL.</summary>
public string Url { get; set; }
+ /// <summary>The license URL, if available.</summary>
+ public string LicenseUrl { get; set; }
+
+ /// <summary>The license name, if available.</summary>
+ public string LicenseName { get; set; }
+
/// <summary>The mod availability status on the remote site.</summary>
public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
@@ -38,16 +44,48 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/// <param name="url">The mod's web URL.</param>
public ModInfoModel(string name, string version, string url, string previewVersion = null)
{
+ this
+ .SetBasicInfo(name, url)
+ .SetVersions(version, previewVersion);
+ }
+
+ /// <summary>Set the basic mod info.</summary>
+ /// <param name="name">The mod name.</param>
+ /// <param name="url">The mod's web URL.</param>
+ public ModInfoModel SetBasicInfo(string name, string url)
+ {
this.Name = name;
+ this.Url = url;
+
+ return this;
+ }
+
+ /// <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>
+ public ModInfoModel SetVersions(string version, string previewVersion = null)
+ {
this.Version = version;
this.PreviewVersion = previewVersion;
- this.Url = url;
+
+ return this;
+ }
+
+ /// <summary>Set the license info, if available.</summary>
+ /// <param name="url">The license URL.</param>
+ /// <param name="name">The license name.</param>
+ public ModInfoModel SetLicense(string url, string name)
+ {
+ this.LicenseUrl = url;
+ this.LicenseName = name;
+
+ return this;
}
/// <summary>Set a mod error.</summary>
/// <param name="status">The mod availability status on the remote site.</param>
/// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
- public ModInfoModel WithError(RemoteModStatus status, string error)
+ public ModInfoModel SetError(RemoteModStatus status, string error)
{
this.Status = status;
this.Error = error;
diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
index a4ae61eb..b4791f56 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
@@ -32,27 +32,27 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{
// validate ID format
if (!uint.TryParse(id, out uint nexusID))
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
+ return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
// fetch info
try
{
NexusMod mod = await this.Client.GetModAsync(nexusID);
if (mod == null)
- return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID.");
+ return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID.");
if (mod.Error != null)
{
RemoteModStatus remoteStatus = mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished
? RemoteModStatus.DoesNotExist
: RemoteModStatus.TemporaryError;
- return new ModInfoModel().WithError(remoteStatus, mod.Error);
+ return new ModInfoModel().SetError(remoteStatus, mod.Error);
}
return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url);
}
catch (Exception ex)
{
- return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
+ return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
}
}
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 4d23fe65..33737235 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -121,8 +121,6 @@ namespace StardewModdingAPI.Web
services.AddSingleton<IGitHubClient>(new GitHubClient(
baseUrl: api.GitHubBaseUrl,
- stableReleaseUrlFormat: api.GitHubStableReleaseUrlFormat,
- anyReleaseUrlFormat: api.GitHubAnyReleaseUrlFormat,
userAgent: userAgent,
acceptHeader: api.GitHubAcceptHeader,
username: api.GitHubUsername,
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 3ea37dea..f9777f87 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -30,8 +30,6 @@
"ChucklefishModPageUrlFormat": "resources/{0}",
"GitHubBaseUrl": "https://api.github.com",
- "GitHubStableReleaseUrlFormat": "repos/{0}/releases/latest",
- "GitHubAnyReleaseUrlFormat": "repos/{0}/releases?per_page=2", // allow for draft release (only visible if GitHub repo is owned by same account as the update check credentials)
"GitHubAcceptHeader": "application/vnd.github.v3+json",
"GitHubUsername": null, // see top note
"GitHubPassword": null, // see top note