From 009a387526ee10b18d0ed3030d6e8868edf17203 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 May 2018 18:44:39 -0400 Subject: unify SMAPI.AssemblyRewriters and SMAPI.Common projects --- src/SMAPI.Web/Controllers/IndexController.cs | 2 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 0464e50a..92b4f2c0 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using StardewModdingAPI.Common; +using StardewModdingAPI.Internal; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 24517263..fc90d067 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Internal.Models; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; -- cgit From 4eebd813f239267d659f3cbf4fa6cf5d47d99c26 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 17 May 2018 19:26:53 -0400 Subject: add beta for-developers download to smapi.io --- src/SMAPI.Web/Controllers/IndexController.cs | 144 ++++++++++++++++----------- 1 file changed, 85 insertions(+), 59 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 92b4f2c0..08b7363a 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -46,24 +48,23 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] public async Task Index() { - // fetch SMAPI releases - IndexVersionModel stableVersion = await this.Cache.GetOrCreateAsync("stable-version", async entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); - return new IndexVersionModel(release.Name, release.Body, this.GetMainDownloadUrl(release), this.GetDevDownloadUrl(release)); - }); - IndexVersionModel betaVersion = await this.Cache.GetOrCreateAsync("beta-version", async entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); - return release.IsPrerelease - ? this.GetBetaDownload(release) - : null; - }); + // choose versions + ReleaseVersion[] versions = await this.GetReleaseVersionsAsync(); + ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsBeta && !version.IsForDevs); + ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => !version.IsBeta && version.IsForDevs); + ReleaseVersion betaVersion = versions.LastOrDefault(version => version.IsBeta && !version.IsForDevs); + ReleaseVersion betaVersionForDevs = versions.LastOrDefault(version => version.IsBeta && version.IsForDevs); // render view - var model = new IndexModel(stableVersion, betaVersion); + IndexVersionModel stableVersionModel = stableVersion != null + ? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl) + : new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong) + IndexVersionModel betaVersionModel = betaVersion != null + ? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl) + : null; + + // render view + var model = new IndexModel(stableVersionModel, betaVersionModel); return this.View(model); } @@ -71,62 +72,87 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Private methods *********/ - /// Get the main download URL for a SMAPI release. - /// The SMAPI release. - private string GetMainDownloadUrl(GitRelease release) + /// Get a sorted, parsed list of SMAPI downloads for the latest releases. + private async Task GetReleaseVersionsAsync() { - // get main download URL - foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + return await this.Cache.GetOrCreateAsync("available-versions", async entry => { - if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer.zip")) - return asset.DownloadUrl; - } + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - // fallback just in case - return "https://github.com/pathoschild/SMAPI/releases"; + // get releases + GitRelease stableRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); + GitRelease betaRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); + if (stableRelease.Tag == betaRelease.Tag) + betaRelease = null; + + // get versions + ReleaseVersion[] stableVersions = this.ParseReleaseVersions(stableRelease).ToArray(); + ReleaseVersion[] betaVersions = this.ParseReleaseVersions(betaRelease).ToArray(); + return stableVersions + .Concat(betaVersions) + .OrderBy(p => p.Version) + .ToArray(); + }); } - /// Get the for-developers download URL for a SMAPI release. - /// The SMAPI release. - private string GetDevDownloadUrl(GitRelease release) + /// Get a parsed list of SMAPI downloads for a release. + /// The GitHub release. + private IEnumerable ParseReleaseVersions(GitRelease release) { - // get dev download URL - foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + if (release?.Assets == null) + yield break; + + foreach (GitAsset asset in release.Assets) { - if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer-for-developers.zip")) - return asset.DownloadUrl; - } + Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); + if (!match.Success || !SemanticVersionImpl.TryParse(match.Groups["version"].Value, out SemanticVersionImpl version)) + continue; + bool isBeta = version.Tag != null; + bool isForDevs = match.Groups["forDevs"].Success; - // fallback just in case - return "https://github.com/pathoschild/SMAPI/releases"; + yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs); + } } - /// Get the latest beta download for a SMAPI release. - /// The SMAPI release. - private IndexVersionModel GetBetaDownload(GitRelease release) + /// A parsed release download. + private class ReleaseVersion { - // get download with the latest version - SemanticVersionImpl latestVersion = null; - string latestUrl = null; - foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + /********* + ** Accessors + *********/ + /// The underlying GitHub release. + public GitRelease Release { get; } + + /// The underlying download asset. + public GitAsset Asset { get; } + + /// The SMAPI version. + public SemanticVersionImpl Version { get; } + + /// Whether this is a beta download. + public bool IsBeta { get; } + + /// Whether this is a 'for developers' download. + public bool IsForDevs { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying GitHub release. + /// The underlying download asset. + /// The SMAPI version. + /// Whether this is a beta download. + /// Whether this is a 'for developers' download. + public ReleaseVersion(GitRelease release, GitAsset asset, SemanticVersionImpl version, bool isBeta, bool isForDevs) { - // parse version - Match versionMatch = Regex.Match(asset.FileName, @"SMAPI-([\d\.]+(?:-.+)?)-installer.zip"); - if (!versionMatch.Success || !SemanticVersionImpl.TryParse(versionMatch.Groups[1].Value, out SemanticVersionImpl version)) - continue; - - // save latest version - if (latestVersion == null || latestVersion.CompareTo(version) < 0) - { - latestVersion = version; - latestUrl = asset.DownloadUrl; - } + this.Release = release; + this.Asset = asset; + this.Version = version; + this.IsBeta = isBeta; + this.IsForDevs = isForDevs; } - - // return if prerelease - return latestVersion?.Tag != null - ? new IndexVersionModel(latestVersion.ToString(), release.Body, latestUrl, null) - : null; } } } -- cgit From 3129f67eb1f162e06d96854f319b10ccf583f0aa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 25 May 2018 01:14:40 -0400 Subject: add semantic version to toolkit (#532) --- src/SMAPI.Web/Controllers/IndexController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 08b7363a..0cc3c37a 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Web.Controllers foreach (GitAsset asset in release.Assets) { Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); - if (!match.Success || !SemanticVersionImpl.TryParse(match.Groups["version"].Value, out SemanticVersionImpl version)) + if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; bool isBeta = version.Tag != null; bool isForDevs = match.Groups["forDevs"].Success; @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Web.Controllers public GitAsset Asset { get; } /// The SMAPI version. - public SemanticVersionImpl Version { get; } + public ISemanticVersion Version { get; } /// Whether this is a beta download. public bool IsBeta { get; } @@ -145,7 +145,7 @@ namespace StardewModdingAPI.Web.Controllers /// The SMAPI version. /// Whether this is a beta download. /// Whether this is a 'for developers' download. - public ReleaseVersion(GitRelease release, GitAsset asset, SemanticVersionImpl version, bool isBeta, bool isForDevs) + public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isBeta, bool isForDevs) { this.Release = release; this.Asset = asset; -- cgit From de74b038e4c15d393de8828c89814ef7eaefe507 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 18:22:04 -0400 Subject: move web API client into toolkit (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index fc90d067..1ec855d5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Internal.Models; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; -- cgit From a463a05607c89922af7e908b39aa897b8d23bfbf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 3 Jun 2018 13:54:26 -0400 Subject: redesign log parser upload page This makes the instructions much more clear and prominent, so it should be more intuitive for players. The previous design often confused users because they saw the big textbox and ignored the little instructions above it. --- src/SMAPI.Web/Controllers/LogParserController.cs | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 62547deb..2bff1392 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.IO.Compression; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -72,13 +73,27 @@ namespace StardewModdingAPI.Web.Controllers ** JSON ***/ /// Save raw log data. - /// The log content to save. - [HttpPost, Produces("application/json"), AllowLargePosts] - [Route("log/save")] - public async Task PostAsync([FromBody] string content) + [HttpPost, AllowLargePosts] + [Route("log")] + public async Task PostAsync() { - content = this.CompressString(content); - return await this.Pastebin.PostAsync(content); + // get raw log text + string input = this.Request.Form["input"].FirstOrDefault(); + if (string.IsNullOrWhiteSpace(input)) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null, null) { UploadError = "The log file seems to be empty." }); + + // upload log + input = this.CompressString(input); + SavePasteResult result = await this.Pastebin.PostAsync(input); + + // handle errors + if (!result.Success) + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID, null) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" }); + + // redirect to view + UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl)); + uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID; + return this.Redirect(uri.Uri.ToString()); } @@ -115,7 +130,7 @@ namespace StardewModdingAPI.Web.Controllers } // prefix length - var zipBuffer = new byte[compressedData.Length + 4]; + byte[] zipBuffer = new byte[compressedData.Length + 4]; Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length); Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4); @@ -151,7 +166,7 @@ namespace StardewModdingAPI.Web.Controllers memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4); // read data - var buffer = new byte[dataLength]; + byte[] buffer = new byte[dataLength]; memoryStream.Position = 0; using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) gZipStream.Read(buffer, 0, buffer.Length); -- cgit From 536d85cf9a55555782765cb9f6ef312cea9a4d5d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Jun 2018 13:12:19 -0400 Subject: optimise index page queries --- src/SMAPI.Web/Controllers/IndexController.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 0cc3c37a..a24b470c 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.Controllers private readonly IGitHubClient GitHub; /// The cache time for release info. - private readonly TimeSpan CacheTime = TimeSpan.FromSeconds(1); + private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10); /// The GitHub repository name to check for update. private readonly string RepositoryName = "Pathoschild/SMAPI"; @@ -79,11 +79,20 @@ namespace StardewModdingAPI.Web.Controllers { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - // get releases - GitRelease stableRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); - GitRelease betaRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); - if (stableRelease.Tag == betaRelease.Tag) - betaRelease = null; + // get latest release (whether preview or stable) + GitRelease stableRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); + + // split stable/prerelease if applicable + GitRelease betaRelease = null; + if (stableRelease.IsPrerelease) + { + GitRelease result = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); + if (result != null) + { + betaRelease = stableRelease; + stableRelease = result; + } + } // get versions ReleaseVersion[] stableVersions = this.ParseReleaseVersions(stableRelease).ToArray(); -- cgit From 6e0dcddeb8dd5bcc51e92a4dead42c6757e543e1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Jun 2018 15:37:17 -0400 Subject: strip 'noinclude' blocks from release descriptions serverside --- src/SMAPI.Web/Controllers/IndexController.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index a24b470c..f4ade7de 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using HtmlAgilityPack; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using StardewModdingAPI.Toolkit; @@ -94,6 +95,19 @@ namespace StardewModdingAPI.Web.Controllers } } + // strip 'noinclude' blocks from release descriptions + foreach (GitRelease release in new[] { stableRelease, betaRelease }) + { + if (release == null) + continue; + + HtmlDocument doc = new HtmlDocument(); + doc.LoadHtml(release.Body); + foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? new HtmlNode[0]) + node.Remove(); + release.Body = doc.DocumentNode.InnerHtml.Trim(); + } + // get versions ReleaseVersion[] stableVersions = this.ParseReleaseVersions(stableRelease).ToArray(); ReleaseVersion[] betaVersions = this.ParseReleaseVersions(betaRelease).ToArray(); -- cgit From d401aff3307f6e2e1641610fdd912b572d6b04c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 22:10:15 -0400 Subject: rewrite update checks (#551) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 180 ++++++++++++++++++------- 1 file changed, 129 insertions(+), 51 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1ec855d5..c5a1705d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -67,70 +68,89 @@ namespace StardewModdingAPI.Web.Controllers } /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search as a comma-delimited array. - /// Whether to allow non-semantic versions, instead of returning an error for those. - [HttpGet] - public async Task> GetAsync(string modKeys, bool allowInvalidVersions = false) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary(); - - return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions)); - } - - /// Fetch version metadata for the given mods. - /// The mod search criteria. + /// The mod search criteria. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) + public async Task> PostAsync([FromBody] ModSearchModel model) { - // parse model - bool allowInvalidVersions = search?.AllowInvalidVersions ?? false; - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) + ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (ModSearchEntryModel mod in searchMods) { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + if (string.IsNullOrWhiteSpace(mod.ID)) continue; - } - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + // get latest versions + ModEntryModel result = new ModEntryModel { ID = mod.ID }; + IList errors = new List(); + foreach (string updateKey in mod.UpdateKeys ?? new string[0]) { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) + { + errors.Add(data.Error); + continue; + } - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - // fetch info - ModInfoModel info = await repository.GetModInfoAsync(modID); + // handle main version + if (data.Version != null) + { + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); + continue; + } + + if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + { + result.Name = data.Name; + result.Url = data.Url; + result.Version = version.ToString(); + } + } - // validate - if (info.Error == null) + // handle optional version + if (data.PreviewVersion != null) { - if (info.Version == null) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number."); - if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'."); + if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + { + errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); + continue; + } + + if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + { + result.Name = result.Name ?? data.Name; + result.PreviewUrl = data.Url; + result.PreviewVersion = version.ToString(); + } } + } + + // fallback to preview if latest is invalid + if (result.Version == null && result.PreviewVersion != null) + { + result.Version = result.PreviewVersion; + result.Url = result.PreviewUrl; + result.PreviewVersion = null; + result.PreviewUrl = null; + } + + // special cases + if (mod.ID == "Pathoschild.SMAPI") + { + result.Name = "SMAPI"; + result.Url = "https://smapi.io/"; + if (result.PreviewUrl != null) + result.PreviewUrl = "https://smapi.io/"; + } - // cache & return - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); - return info; - }); + // add result + result.Errors = errors.ToArray(); + mods[mod.ID] = result; } - return result; + return mods; } @@ -158,5 +178,63 @@ namespace StardewModdingAPI.Web.Controllers modID = parts[1].Trim(); return true; } + + /// Get the mods for which the API should return data. + /// The search model. + private IEnumerable GetSearchMods(ModSearchModel model) + { + if (model == null) + yield break; + + // yield standard entries + if (model.Mods != null) + { + foreach (ModSearchEntryModel mod in model.Mods) + yield return mod; + } + + // yield mod update keys if backwards compatible + if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + { + foreach (string updateKey in model.ModKeys.Distinct()) + yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); + } + } + + /// Get the mod info for an update key. + /// The namespaced update key. + private async Task GetInfoForUpdateKeyAsync(string updateKey) + { + // parse update key + if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID)) + return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + + // fetch mod info + return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + ModInfoModel result = await repository.GetModInfoAsync(modID); + if (result.Error != null) + { + if (result.Version == null) + result.Error = $"The update key '{updateKey}' matches a mod with no version number."; + else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."; + } + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); + return result; + }); + } + + /// Get whether the API should return data in a backwards compatible way. + /// The last version for which data should be backwards compatible. + private bool ShouldBeBackwardsCompatible(string maxVersion) + { + string actualVersion = (string)this.RouteData.Values["version"]; + return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + } } } -- cgit From 316242eeb2b6b6e711ab98f64c147a59c1d0aab8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Jun 2018 21:29:10 -0400 Subject: merge ISemanticVersion interfaces into new project (#532) --- src/SMAPI.Web/Controllers/IndexController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index f4ade7de..09bad112 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Web.Controllers Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; - bool isBeta = version.Tag != null; + bool isBeta = version.IsPrerelease(); bool isForDevs = match.Groups["forDevs"].Success; yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs); -- cgit From 3e5c109df1f90904c2dcb177e35b35f003e90fd9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Jun 2018 09:47:31 -0400 Subject: add log parser option to view raw log --- src/SMAPI.Web/Controllers/LogParserController.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 2bff1392..354bdb06 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -52,21 +52,22 @@ namespace StardewModdingAPI.Web.Controllers ***/ /// Render the log parser UI. /// The paste ID. + /// Whether to display the raw unparsed log. [HttpGet] [Route("log")] [Route("log/{id}")] - public async Task Index(string id = null) + public async Task Index(string id = null, bool raw = false) { // fresh page if (string.IsNullOrWhiteSpace(id)) - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null)); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id)); // log page PasteInfo paste = await this.GetAsync(id); ParsedLog log = paste.Success ? new LogParser().Parse(paste.Content) : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error }; - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log)); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log, raw)); } /*** @@ -80,7 +81,7 @@ namespace StardewModdingAPI.Web.Controllers // get raw log text string input = this.Request.Form["input"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(input)) - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null, null) { UploadError = "The log file seems to be empty." }); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null) { UploadError = "The log file seems to be empty." }); // upload log input = this.CompressString(input); @@ -88,7 +89,7 @@ namespace StardewModdingAPI.Web.Controllers // handle errors if (!result.Success) - return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID, null) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" }); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" }); // redirect to view UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl)); -- cgit From 583cb91f4a3429549b8e56081737e6a410ebd1a4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 20:59:39 -0400 Subject: use mod DB in web API to get default update keys for mod IDs (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index c5a1705d..960602f4 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -39,18 +42,23 @@ namespace StardewModdingAPI.Web.Controllers /// A regex which matches SMAPI-style semantic version. private readonly string VersionRegex; + /// The internal mod metadata list. + private readonly ModDatabase ModDatabase; + /********* ** Public methods *********/ /// Construct an instance. + /// The web hosting environment. /// The cache in which to store mod metadata. /// The config settings for mod update checks. /// The Chucklefish API client. /// The GitHub API client. /// The Nexus API client. - public ModsApiController(IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus) + public ModsApiController(IHostingEnvironment environment, IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus) { + this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; this.Cache = cache; @@ -79,10 +87,20 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; + // resolve update keys + var updateKeys = new HashSet(mod.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); + ModDataRecord record = this.ModDatabase.Get(mod.ID); + if (record?.Fields != null) + { + string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) + updateKeys.Add(defaultUpdateKey); + } + // get latest versions ModEntryModel result = new ModEntryModel { ID = mod.ID }; IList errors = new List(); - foreach (string updateKey in mod.UpdateKeys ?? new string[0]) + foreach (string updateKey in updateKeys) { // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); -- cgit From 3f5a5e54041a641e30fc5cc899046953d9763da4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 22:01:04 -0400 Subject: use more structured API response for update checks (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 44 ++++++++++++++------------ 1 file changed, 23 insertions(+), 21 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 960602f4..c4f1023b 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -80,7 +80,11 @@ namespace StardewModdingAPI.Web.Controllers [HttpPost] public async Task> PostAsync([FromBody] ModSearchModel model) { - ModSearchEntryModel[] searchMods = this.GetSearchMods(model).ToArray(); + // parse request data + ISemanticVersion apiVersion = this.GetApiVersion(); + ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray(); + + // perform checks IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) { @@ -119,11 +123,10 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Version == null || version.IsNewerThan(new SemanticVersion(result.Version))) + if (result.Main == null || result.Main.Version.IsOlderThan(version)) { result.Name = data.Name; - result.Url = data.Url; - result.Version = version.ToString(); + result.Main = new ModEntryVersionModel(version, data.Url); } } @@ -136,35 +139,34 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.PreviewVersion == null || version.IsNewerThan(new SemanticVersion(data.PreviewVersion))) + if (result.Optional == null || result.Optional.Version.IsOlderThan(version)) { result.Name = result.Name ?? data.Name; - result.PreviewUrl = data.Url; - result.PreviewVersion = version.ToString(); + result.Optional = new ModEntryVersionModel(version, data.Url); } } } // fallback to preview if latest is invalid - if (result.Version == null && result.PreviewVersion != null) + if (result.Main == null && result.Optional != null) { - result.Version = result.PreviewVersion; - result.Url = result.PreviewUrl; - result.PreviewVersion = null; - result.PreviewUrl = null; + result.Main = result.Optional; + result.Optional = null; } // special cases if (mod.ID == "Pathoschild.SMAPI") { result.Name = "SMAPI"; - result.Url = "https://smapi.io/"; - if (result.PreviewUrl != null) - result.PreviewUrl = "https://smapi.io/"; + if (result.Main != null) + result.Main.Url = "https://smapi.io/"; + if (result.Optional != null) + result.Optional.Url = "https://smapi.io/"; } // add result result.Errors = errors.ToArray(); + result.SetBackwardsCompatibility(apiVersion); mods[mod.ID] = result; } @@ -199,7 +201,8 @@ namespace StardewModdingAPI.Web.Controllers /// Get the mods for which the API should return data. /// The search model. - private IEnumerable GetSearchMods(ModSearchModel model) + /// The requested API version. + private IEnumerable GetSearchMods(ModSearchModel model, ISemanticVersion apiVersion) { if (model == null) yield break; @@ -212,7 +215,7 @@ namespace StardewModdingAPI.Web.Controllers } // yield mod update keys if backwards compatible - if (model.ModKeys != null && model.ModKeys.Any() && this.ShouldBeBackwardsCompatible("2.6-beta.17")) + if (model.ModKeys != null && model.ModKeys.Any() && !apiVersion.IsNewerThan("2.6-beta.17")) { foreach (string updateKey in model.ModKeys.Distinct()) yield return new ModSearchEntryModel(updateKey, new[] { updateKey }); @@ -247,12 +250,11 @@ namespace StardewModdingAPI.Web.Controllers }); } - /// Get whether the API should return data in a backwards compatible way. - /// The last version for which data should be backwards compatible. - private bool ShouldBeBackwardsCompatible(string maxVersion) + /// Get the requested API version. + private ISemanticVersion GetApiVersion() { string actualVersion = (string)this.RouteData.Values["version"]; - return !new SemanticVersion(actualVersion).IsNewerThan(new SemanticVersion(maxVersion)); + return new SemanticVersion(actualVersion); } } } -- cgit From c9fedebaf3231a2d5a00a95ff1ffd3ac5dac4ae2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Jun 2018 22:30:34 -0400 Subject: add support for unofficial version in update checks (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 47 ++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index c4f1023b..b9af17dc 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -45,6 +46,9 @@ namespace StardewModdingAPI.Web.Controllers /// The internal mod metadata list. private readonly ModDatabase ModDatabase; + /// The web URL for the wiki compatibility list. + private readonly string WikiCompatibilityPageUrl; + /********* ** Public methods @@ -60,6 +64,7 @@ namespace StardewModdingAPI.Web.Controllers { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; + this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl; this.Cache = cache; this.SuccessCacheMinutes = config.SuccessCacheMinutes; @@ -84,6 +89,9 @@ namespace StardewModdingAPI.Web.Controllers ISemanticVersion apiVersion = this.GetApiVersion(); ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray(); + // fetch wiki data + WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync(); + // perform checks IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) @@ -123,7 +131,7 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Main == null || result.Main.Version.IsOlderThan(version)) + if (this.IsNewer(version, result.Main?.Version)) { result.Name = data.Name; result.Main = new ModEntryVersionModel(version, data.Url); @@ -139,7 +147,7 @@ namespace StardewModdingAPI.Web.Controllers continue; } - if (result.Optional == null || result.Optional.Version.IsOlderThan(version)) + if (this.IsNewer(version, result.Optional?.Version)) { result.Name = result.Name ?? data.Name; result.Optional = new ModEntryVersionModel(version, data.Url); @@ -147,6 +155,13 @@ namespace StardewModdingAPI.Web.Controllers } } + // get unofficial version + { + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); + } + // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) { @@ -199,6 +214,14 @@ namespace StardewModdingAPI.Web.Controllers return true; } + /// Get whether a version is newer than an version. + /// The current version. + /// The other version. + private bool IsNewer(ISemanticVersion current, ISemanticVersion other) + { + return current != null && (other == null || other.IsOlderThan(current)); + } + /// Get the mods for which the API should return data. /// The search model. /// The requested API version. @@ -222,6 +245,26 @@ namespace StardewModdingAPI.Web.Controllers } } + /// Get mod data from the wiki compatibility list. + private async Task GetWikiDataAsync() + { + ModToolkit toolkit = new ModToolkit(); + return await this.Cache.GetOrCreateAsync($"_wiki", async entry => + { + try + { + WikiCompatibilityEntry[] entries = await toolkit.GetWikiCompatibilityListAsync(); + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes); + return entries; + } + catch + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes); + return new WikiCompatibilityEntry[0]; + } + }); + } + /// Get the mod info for an update key. /// The namespaced update key. private async Task GetInfoForUpdateKeyAsync(string updateKey) -- cgit From a0888e0ad1bf0ed38982d2aadf78c31d046b061b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:01:57 -0400 Subject: add optional extended metadata to mods API (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index b9af17dc..e5ae3fc7 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -132,10 +132,7 @@ namespace StardewModdingAPI.Web.Controllers } if (this.IsNewer(version, result.Main?.Version)) - { - result.Name = data.Name; result.Main = new ModEntryVersionModel(version, data.Url); - } } // handle optional version @@ -148,19 +145,14 @@ namespace StardewModdingAPI.Web.Controllers } if (this.IsNewer(version, result.Optional?.Version)) - { - result.Name = result.Name ?? data.Name; result.Optional = new ModEntryVersionModel(version, data.Url); - } } } // get unofficial version - { - WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); - if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); - } + WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); + if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) + result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) @@ -172,13 +164,16 @@ namespace StardewModdingAPI.Web.Controllers // special cases if (mod.ID == "Pathoschild.SMAPI") { - result.Name = "SMAPI"; if (result.Main != null) result.Main.Url = "https://smapi.io/"; if (result.Optional != null) result.Optional.Url = "https://smapi.io/"; } + // add extended metadata + if (model.IncludeExtendedMetadata && (wikiEntry != null || record != null)) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + // add result result.Errors = errors.ToArray(); result.SetBackwardsCompatibility(apiVersion); -- cgit From 60b38666e29684650108031f08ca30bfe483ceab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Jun 2018 01:27:31 -0400 Subject: simplify mod API response structure (#532) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 154 ++++++++++++++----------- 1 file changed, 84 insertions(+), 70 deletions(-) (limited to 'src/SMAPI.Web/Controllers') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index e5ae3fc7..b500e19d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -83,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mod search criteria. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel model) + public async Task PostAsync([FromBody] ModSearchModel model) { // parse request data ISemanticVersion apiVersion = this.GetApiVersion(); @@ -92,101 +92,115 @@ namespace StardewModdingAPI.Web.Controllers // fetch wiki data WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync(); - // perform checks + // fetch data IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in searchMods) { if (string.IsNullOrWhiteSpace(mod.ID)) continue; - // resolve update keys - var updateKeys = new HashSet(mod.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); - ModDataRecord record = this.ModDatabase.Get(mod.ID); - if (record?.Fields != null) + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata); + result.SetBackwardsCompatibility(apiVersion); + mods[mod.ID] = result; + } + + // return in expected structure + return apiVersion.IsNewerThan("2.6-beta.18") + ? mods.Values + : (object)mods; + } + + + /********* + ** Private methods + *********/ + /// Get the metadata for a mod. + /// The mod data to match. + /// The wiki data. + /// Whether to include extended metadata for each mod. + /// Returns the mod data if found, else null. + private async Task GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata) + { + // resolve update keys + var updateKeys = new HashSet(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); + ModDataRecord record = this.ModDatabase.Get(search.ID); + if (record?.Fields != null) + { + string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) + updateKeys.Add(defaultUpdateKey); + } + + // get latest versions + ModEntryModel result = new ModEntryModel { ID = search.ID }; + IList errors = new List(); + foreach (string updateKey in updateKeys) + { + // fetch data + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); + if (data.Error != null) { - string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; - if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) - updateKeys.Add(defaultUpdateKey); + errors.Add(data.Error); + continue; } - // get latest versions - ModEntryModel result = new ModEntryModel { ID = mod.ID }; - IList errors = new List(); - foreach (string updateKey in updateKeys) + // handle main version + if (data.Version != null) { - // fetch data - ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); - if (data.Error != null) + if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) { - errors.Add(data.Error); + errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } - // handle main version - if (data.Version != null) - { - if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) - { - errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); - continue; - } - - if (this.IsNewer(version, result.Main?.Version)) - result.Main = new ModEntryVersionModel(version, data.Url); - } + if (this.IsNewer(version, result.Main?.Version)) + result.Main = new ModEntryVersionModel(version, data.Url); + } - // handle optional version - if (data.PreviewVersion != null) + // handle optio