From 95f261b1f30d8c5ad6c179cd75a220dcca3c6395 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 22:11:51 -0400 Subject: fetch mod info from Nexus API if the web page is hidden due to adult content (#651) --- src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs') diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index c27cadab..d2e9a2fe 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -65,6 +65,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The URL for a Nexus mod page to scrape for versions, excluding the , where {0} is the mod ID. public string NexusModScrapeUrlFormat { get; set; } + /// The Nexus API authentication key. + public string NexusApiKey { get; set; } + /**** ** Pastebin ****/ -- cgit From 1d085df5b796e02b3e9e6874bd4e5684e840cb92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 29 Jul 2019 16:43:25 -0400 Subject: track license info for mod GitHub repos (#651) --- docs/release-notes.md | 1 + .../Framework/Clients/Wiki/WikiModEntry.cs | 1 - .../Framework/UpdateData/UpdateKey.cs | 32 ++++++++++++- src/SMAPI.Web/BackgroundService.cs | 7 +-- src/SMAPI.Web/Controllers/ModsApiController.cs | 54 ++++++++++++++-------- src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs | 12 ++++- .../Framework/Clients/GitHub/GitHubClient.cs | 39 +++++++++------- .../Framework/Clients/GitHub/GitLicense.cs | 20 ++++++++ src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs | 20 ++++++++ .../Framework/Clients/GitHub/IGitHubClient.cs | 5 ++ .../Framework/ConfigModels/ApiClientsConfig.cs | 6 --- .../ModRepositories/ChucklefishRepository.cs | 12 ++--- .../Framework/ModRepositories/GitHubRepository.cs | 24 +++++++--- .../Framework/ModRepositories/ModDropRepository.cs | 10 ++-- .../Framework/ModRepositories/ModInfoModel.cs | 42 ++++++++++++++++- .../Framework/ModRepositories/NexusRepository.cs | 8 ++-- src/SMAPI.Web/Startup.cs | 2 - src/SMAPI.Web/appsettings.json | 2 - 18 files changed, 220 insertions(+), 77 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs (limited to 'src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs') 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 { /// A namespaced mod ID which uniquely identifies a mod within a mod repository. - public class UpdateKey + public class UpdateKey : IEquatable { /********* ** Accessors @@ -38,6 +38,12 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData && !string.IsNullOrWhiteSpace(id); } + /// Construct an instance. + /// The mod repository containing the mod. + /// The mod ID within the repository. + public UpdateKey(ModRepositoryKey repository, string id) + : this($"{repository}:{id}", repository, id) { } + /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string raw) @@ -69,5 +75,29 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData ? $"{this.Repository}:{this.ID}" : this.RawText; } + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + public bool Equals(UpdateKey other) + { + return + other != null + && this.Repository == other.Repository + && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase); + } + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + public override bool Equals(object obj) + { + return obj is UpdateKey other && this.Equals(other); + } + + /// Serves as the default hash function. + /// A hash code for the current object. + 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 } /// Remove mods which haven't been requested in over 48 hours. - 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 errors = new List(); - 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 /// Get the mod info for an update key. /// The namespaced update key. - private async Task GetInfoForUpdateKeyAsync(string updateKey) + private async Task 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 /// The specified update keys. /// The mod's entry in SMAPI's internal database. /// The mod's entry in the wiki list. - public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { IEnumerable GetRaw() { @@ -283,10 +293,14 @@ namespace StardewModdingAPI.Web.Controllers } } - HashSet seen = new HashSet(StringComparer.InvariantCulture); - foreach (string key in GetRaw()) + HashSet seen = new HashSet(); + 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 /// The URL for the mod page. public string Url { get; set; } + /// The license URL, if available. + public string LicenseUrl { get; set; } + + /// The license name, if available. + 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; } /// Get the API model for the cached data. 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 *********/ - /// The URL for a GitHub API query for the latest stable release, excluding the base URL, where {0} is the organisation and project name. - private readonly string StableReleaseUrlFormat; - - /// 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. - private readonly string AnyReleaseUrlFormat; - /// The underlying HTTP client. private readonly IClient Client; @@ -27,17 +21,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// Construct an instance. /// The base URL for the GitHub API. - /// The URL for a GitHub API query for the latest stable release, excluding the , where {0} is the organisation and project name. - /// The URL for a GitHub API query for the latest release (including prerelease), excluding the , where {0} is the organisation and project name. /// The user agent for the API client. /// The Accept header value expected by the GitHub API. /// The username with which to authenticate to the GitHub API. /// The password with which to authenticate to the GitHub API. - 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); } + /// Get basic metadata for a GitHub repository, if available. + /// The repository key (like Pathoschild/SMAPI). + /// Returns the repository info if it exists, else null. + public async Task GetRepositoryAsync(string repo) + { + this.AssertKeyFormat(repo); + try + { + return await this.Client + .GetAsync($"repos/{repo}") + .As(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return null; + } + } + /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). /// Whether to return a prerelease version if it's latest. /// Returns the release if found, else null. public async Task 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(); return results.FirstOrDefault(p => !p.IsDraft); } return await this.Client - .GetAsync(string.Format(this.StableReleaseUrlFormat, repo)) + .GetAsync($"repos/{repo}/releases/latest") .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -85,7 +92,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Assert that a repository key is formatted correctly. /// The repository key (like Pathoschild/SMAPI). /// The repository key is invalid. - 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 +{ + /// The license info for a GitHub project. + internal class GitLicense + { + /// The license display name. + [JsonProperty("name")] + public string Name { get; set; } + + /// The SPDX ID for the license. + [JsonProperty("spdx_id")] + public string SpdxId { get; set; } + + /// The URL for the license info. + [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 +{ + /// Basic metadata about a GitHub project. + internal class GitRepo + { + /// The full repository name, including the owner. + [JsonProperty("full_name")] + public string FullName { get; set; } + + /// The URL to the repository web page, if any. + [JsonProperty("html_url")] + public string WebUrl { get; set; } + + /// The code license, if any. + [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 *********/ + /// Get basic metadata for a GitHub repository, if available. + /// The repository key (like Pathoschild/SMAPI). + /// Returns the repository info if it exists, else null. + Task GetRepositoryAsync(string repo); + /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). /// Whether to return a prerelease version if it's latest. 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 /// The base URL for the GitHub API. public string GitHubBaseUrl { get; set; } - /// The URL for a GitHub API query for the latest stable release, excluding the , where {0} is the organisation and project name. - public string GitHubStableReleaseUrlFormat { get; set; } - - /// The URL for a GitHub API query for the latest release (including prerelease), excluding the , where {0} is the organisation and project name. - public string GitHubAnyReleaseUrlFormat { get; set; } - /// The Accept header value expected by the GitHub API. 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 /// The mod ID in this repository. public override async Task 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 /// The mod's web URL. public string Url { get; set; } + /// The license URL, if available. + public string LicenseUrl { get; set; } + + /// The license name, if available. + public string LicenseName { get; set; } + /// The mod availability status on the remote site. public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; @@ -37,17 +43,49 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The semantic version for the mod's latest preview release, if available and different from . /// The mod's web URL. public ModInfoModel(string name, string version, string url, string previewVersion = null) + { + this + .SetBasicInfo(name, url) + .SetVersions(version, previewVersion); + } + + /// Set the basic mod info. + /// The mod name. + /// The mod's web URL. + public ModInfoModel SetBasicInfo(string name, string url) { this.Name = name; + this.Url = url; + + return this; + } + + /// Set the mod version info. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + public ModInfoModel SetVersions(string version, string previewVersion = null) + { this.Version = version; this.PreviewVersion = previewVersion; - this.Url = url; + + return this; + } + + /// Set the license info, if available. + /// The license URL. + /// The license name. + public ModInfoModel SetLicense(string url, string name) + { + this.LicenseUrl = url; + this.LicenseName = name; + + return this; } /// Set a mod error. /// The mod availability status on the remote site. /// The error message indicating why the mod is invalid (if applicable). - 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(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 -- cgit From 8b09a2776d9c0faf96fa90c923952033ce659477 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 7 Nov 2019 13:51:45 -0500 Subject: add support for CurseForge update keys (#605) --- docs/release-notes.md | 1 + .../Framework/UpdateData/ModRepositoryKey.cs | 3 + src/SMAPI.Web/Controllers/ModsApiController.cs | 5 +- .../Clients/CurseForge/CurseForgeClient.cs | 113 +++++++++++++++++++++ .../Framework/Clients/CurseForge/CurseForgeMod.cs | 23 +++++ .../Clients/CurseForge/ICurseForgeClient.cs | 17 ++++ .../CurseForge/ResponseModels/ModFileModel.cs | 12 +++ .../Clients/CurseForge/ResponseModels/ModModel.cs | 18 ++++ .../Framework/ConfigModels/ApiClientsConfig.cs | 7 ++ .../ModRepositories/CurseForgeRepository.cs | 63 ++++++++++++ src/SMAPI.Web/Startup.cs | 5 + src/SMAPI.Web/appsettings.json | 2 + 12 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs create mode 100644 src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs (limited to 'src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 1d933f96..5c12c4cc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -47,6 +47,7 @@ For modders: * Now ignores metadata files and folders (like `__MACOSX` and `__folder_managed_by_vortex`) and content files (like `.txt` or `.png`), which avoids missing-manifest errors in some common cases. * Now detects XNB mods more accurately, and consolidates multi-folder XNB mods in logged messages. * SMAPI now automatically removes invalid content when loading a save to prevent crashes. A warning is shown in-game when this happens. This applies for locations and NPCs. + * Added update checks for CurseForge mods. * Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles). * Added support for specifying SMAPI command-line arguments as environment variables for Linux/Mac compatibility. * Improved launch script compatibility on Linux (thanks to kurumushi and toastal!). diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs index f6c402d5..765ca334 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs @@ -9,6 +9,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The Chucklefish mod repository. Chucklefish, + /// The CurseForge mod repository. + CurseForge, + /// A GitHub project containing releases. GitHub, diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 8419b220..1412105a 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -14,6 +14,7 @@ using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; +using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -61,10 +62,11 @@ namespace StardewModdingAPI.Web.Controllers /// The cache in which to store mod metadata. /// The config settings for mod update checks. /// The Chucklefish API client. + /// The CurseForge API client. /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions configProvider, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; @@ -78,6 +80,7 @@ namespace StardewModdingAPI.Web.Controllers new IModRepository[] { new ChucklefishRepository(chucklefish), + new CurseForgeRepository(curseForge), new GitHubRepository(github), new ModDropRepository(modDrop), new NexusRepository(nexus) diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs new file mode 100644 index 00000000..140b854e --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -0,0 +1,113 @@ +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels; + +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge +{ + /// An HTTP client for fetching mod metadata from the CurseForge API. + internal class CurseForgeClient : ICurseForgeClient + { + /********* + ** Fields + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + /// A regex pattern which matches a version number in a CurseForge mod file name. + private readonly Regex VersionInNamePattern = new Regex(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the API client. + /// The base URL for the CurseForge API. + public CurseForgeClient(string userAgent, string apiUrl) + { + this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod. + /// The CurseForge mod ID. + /// Returns the mod info if found, else null. + public async Task GetModAsync(long id) + { + // get raw data + ModModel mod = await this.Client + .GetAsync($"addon/{id}") + .As(); + if (mod == null) + return null; + + // get latest versions + string invalidVersion = null; + ISemanticVersion latest = null; + foreach (ModFileModel file in mod.LatestFiles) + { + // extract version + ISemanticVersion version; + { + string raw = this.GetRawVersion(file); + if (raw == null) + continue; + + if (!SemanticVersion.TryParse(raw, out version)) + { + if (invalidVersion == null) + invalidVersion = raw; + continue; + } + } + + // track latest version + if (latest == null || version.IsNewerThan(latest)) + latest = version; + } + + // get error + string error = null; + if (latest == null && invalidVersion == null) + { + error = mod.LatestFiles.Any() + ? $"CurseForge mod {id} has no downloads which specify the version in a recognised format." + : $"CurseForge mod {id} has no downloads."; + } + + // generate result + return new CurseForgeMod + { + Name = mod.Name, + LatestVersion = latest?.ToString() ?? invalidVersion, + Url = mod.WebsiteUrl, + Error = error + }; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get a raw version string for a mod file, if available. + /// The file whose version to get. + private string GetRawVersion(ModFileModel file) + { + Match match = this.VersionInNamePattern.Match(file.DisplayName); + if (!match.Success) + match = this.VersionInNamePattern.Match(file.FileName); + + return match.Success + ? match.Groups[1].Value + : null; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs new file mode 100644 index 00000000..e5bb8cf1 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge +{ + /// Mod metadata from the CurseForge API. + internal class CurseForgeMod + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The latest file version. + public string LatestVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// A user-friendly error which indicates why fetching the mod info failed (if applicable). + public string Error { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs new file mode 100644 index 00000000..907b4087 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge +{ + /// An HTTP client for fetching mod metadata from the CurseForge API. + internal interface ICurseForgeClient : IDisposable + { + /********* + ** Methods + *********/ + /// Get metadata about a mod. + /// The CurseForge mod ID. + /// Returns the mod info if found, else null. + Task GetModAsync(long id); + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs new file mode 100644 index 00000000..9de74847 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels +{ + /// Metadata from the CurseForge API about a mod file. + public class ModFileModel + { + /// The file name as downloaded. + public string FileName { get; set; } + + /// The file display name. + public string DisplayName { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs new file mode 100644 index 00000000..48cd185b --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels +{ + /// An mod from the CurseForge API. + public class ModModel + { + /// The mod's unique ID on CurseForge. + public int ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The web URL for the mod page. + public string WebsiteUrl { get; set; } + + /// The available file downloads. + public ModFileModel[] LatestFiles { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index a0a1f42a..121690c5 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -23,6 +23,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels public string ChucklefishModPageUrlFormat { get; set; } + /**** + ** CurseForge + ****/ + /// The base URL for the CurseForge API. + public string CurseForgeBaseUrl { get; set; } + + /**** ** GitHub ****/ diff --git a/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs new file mode 100644 index 00000000..93ddc1eb --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.CurseForge; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from CurseForge. + internal class CurseForgeRepository : RepositoryBase + { + /********* + ** Fields + *********/ + /// The underlying CurseForge API client. + private readonly ICurseForgeClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying CurseForge API client. + public CurseForgeRepository(ICurseForgeClient client) + : base(ModRepositoryKey.CurseForge) + { + this.Client = client; + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!uint.TryParse(id, out uint curseID)) + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); + + // fetch info + try + { + CurseForgeMod mod = await this.Client.GetModAsync(curseID); + if (mod == null) + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); + if (mod.Error != null) + { + RemoteModStatus remoteStatus = RemoteModStatus.InvalidData; + return new ModInfoModel().SetError(remoteStatus, mod.Error); + } + + return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.LatestVersion), url: mod.Url); + } + catch (Exception ex) + { + return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index bf69d543..8110b696 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -16,6 +16,7 @@ using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; +using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -119,6 +120,10 @@ namespace StardewModdingAPI.Web baseUrl: api.ChucklefishBaseUrl, modPageUrlFormat: api.ChucklefishModPageUrlFormat )); + services.AddSingleton(new CurseForgeClient( + userAgent: userAgent, + apiUrl: api.CurseForgeBaseUrl + )); services.AddSingleton(new GitHubClient( baseUrl: api.GitHubBaseUrl, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index a440cf42..674bb672 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -30,6 +30,8 @@ "ChucklefishBaseUrl": "https://community.playstarbound.com", "ChucklefishModPageUrlFormat": "resources/{0}", + "CurseForgeBaseUrl": "https://addons-ecs.forgesvc.net/api/v2/", + "GitHubBaseUrl": "https://api.github.com", "GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubUsername": null, // see top note -- cgit