using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ModRepositories; using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] internal class ModsController : 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; /********* ** Public methods *********/ /// Construct an instance. /// The cache in which to store mod metadata. /// The config settings for mod update checks. public ModsController(IMemoryCache cache, IOptions configProvider) { ModUpdateCheckConfig config = configProvider.Value; this.Cache = cache; this.CacheMinutes = config.CacheMinutes; this.Repositories = new IModRepository[] { new GitHubRepository( vendorKey: config.GitHubKey, baseUrl: config.GitHubBaseUrl, releaseUrlFormat: config.GitHubReleaseUrlFormat, userAgent: config.GitHubUserAgent, 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) { // sort & filter keys string[] modKeysArray = (modKeys?.Split(',').Select(p => p.Trim()).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 modKeysArray) { // 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 mod repository 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 repository matching this namespaced mod ID."); continue; } // fetch mod info result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); return await repository.GetModInfoAsync(modID); }); } 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]; modID = parts[1]; return true; } } }