track license info for mod GitHub repos (#651)

This commit is contained in:
Jesse Plamondon-Willard 2019-07-29 16:43:25 -04:00
parent 2b4bc2c282
commit 1d085df5b7
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
18 changed files with 220 additions and 77 deletions

View File

@ -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).

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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));

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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; }

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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;
@ -37,17 +43,49 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
/// <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;

View File

@ -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());
}
}

View File

@ -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,

View File

@ -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