From 65f0fa625575592639a24a9b39330e4a6b500f22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:36:31 -0400 Subject: add scaffolding for web UI (#358) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 162 +++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/SMAPI.Web/Controllers/ModsApiController.cs (limited to 'src/SMAPI.Web/Controllers/ModsApiController.cs') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs new file mode 100644 index 00000000..1db5b59e --- /dev/null +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides an API to perform mod update checks. + [Produces("application/json")] + [Route("api/{version:semanticVersion}/mods")] + internal class ModsApiController : Controller + { + /********* + ** Properties + *********/ + /// The mod repositories which provide mod metadata. + private readonly IDictionary Repositories; + + /// The cache in which to store mod metadata. + private readonly IMemoryCache Cache; + + /// The number of minutes update checks should be cached before refetching them. + private readonly int CacheMinutes; + + /// A regex which matches SMAPI-style semantic version. + private readonly string VersionRegex; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cache in which to store mod metadata. + /// The config settings for mod update checks. + public ModsApiController(IMemoryCache cache, IOptions configProvider) + { + ModUpdateCheckConfig config = configProvider.Value; + + this.Cache = cache; + this.CacheMinutes = config.CacheMinutes; + this.VersionRegex = config.SemanticVersionRegex; + + string version = this.GetType().Assembly.GetName().Version.ToString(3); + this.Repositories = + new IModRepository[] + { + new ChucklefishRepository( + vendorKey: config.ChucklefishKey, + userAgent: string.Format(config.ChucklefishUserAgent, version), + baseUrl: config.ChucklefishBaseUrl, + modPageUrlFormat: config.ChucklefishModPageUrlFormat + ), + new GitHubRepository( + vendorKey: config.GitHubKey, + baseUrl: config.GitHubBaseUrl, + releaseUrlFormat: config.GitHubReleaseUrlFormat, + userAgent: string.Format(config.GitHubUserAgent, version), + acceptHeader: config.GitHubAcceptHeader, + username: config.GitHubUsername, + password: config.GitHubPassword + ), + new NexusRepository( + vendorKey: config.NexusKey, + userAgent: config.NexusUserAgent, + baseUrl: config.NexusBaseUrl, + modUrlFormat: config.NexusModUrlFormat + ) + } + .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + } + + /// Fetch version metadata for the given mods. + /// The namespaced mod keys to search as a comma-delimited array. + [HttpGet] + public async Task> GetAsync(string modKeys) + { + string[] modKeysArray = modKeys?.Split(',').ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// Fetch version metadata for the given mods. + /// The mod search criteria. + [HttpPost] + public async Task> PostAsync([FromBody] ModSearchModel search) + { + // sort & filter keys + 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) + { + // 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'."); + continue; + } + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + { + result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + continue; + } + + // fetch mod info + result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); + + ModInfoModel info = await repository.GetModInfoAsync(modID); + if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) + info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); + + return info; + }); + } + + return result; + } + + + /********* + ** Private methods + *********/ + /// Parse a namespaced mod ID. + /// The raw mod ID to parse. + /// The parsed vendor key. + /// The parsed mod ID. + /// Returns whether the value could be parsed. + private bool TryParseModKey(string raw, out string vendorKey, out string modID) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + { + vendorKey = null; + modID = null; + return false; + } + + // parse + vendorKey = parts[0].Trim(); + modID = parts[1].Trim(); + return true; + } + } +} -- cgit From 9a091bd961ed6dca221316a50c04b91318514ce3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 11:51:25 -0400 Subject: fix API version format --- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- src/SMAPI.Web/Properties/launchSettings.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/SMAPI.Web/Controllers/ModsApiController.cs') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1db5b59e..a600662c 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] - [Route("api/{version:semanticVersion}/mods")] + [Route("api/v{version:semanticVersion}/mods")] internal class ModsApiController : Controller { /********* diff --git a/src/SMAPI.Web/Properties/launchSettings.json b/src/SMAPI.Web/Properties/launchSettings.json index a0760365..75c36ee2 100644 --- a/src/SMAPI.Web/Properties/launchSettings.json +++ b/src/SMAPI.Web/Properties/launchSettings.json @@ -11,7 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "api/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods", + "launchUrl": "api/v1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -19,7 +19,7 @@ "Dewdrop": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "api/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods", + "launchUrl": "api/v1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, -- cgit