using System; 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 Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers { /// Provides an info/download page about SMAPI. [Route("")] internal class IndexController : Controller { /********* ** Fields *********/ /// The site config settings. private readonly SiteConfig SiteConfig; /// The cache in which to store release data. private readonly IMemoryCache Cache; /// The GitHub API client. private readonly IGitHubClient GitHub; /// The cache time for release info. private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10); /// The GitHub repository name to check for update. private readonly string RepositoryName = "Pathoschild/SMAPI"; /********* ** Public methods *********/ /// Construct an instance. /// The cache in which to store release data. /// The GitHub API client. /// The context config settings. public IndexController(IMemoryCache cache, IGitHubClient github, IOptions siteConfig) { this.Cache = cache; this.GitHub = github; this.SiteConfig = siteConfig.Value; } /// Display the index page. [HttpGet] public async Task Index() { // choose versions ReleaseVersion[] versions = await this.GetReleaseVersionsAsync(); ReleaseVersion? stableVersion = versions.LastOrDefault(version => !version.IsForDevs); ReleaseVersion? stableVersionForDevs = versions.LastOrDefault(version => version.IsForDevs); // render view 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 // render view var model = new IndexModel(stableVersionModel, this.SiteConfig.OtherBlurb, this.SiteConfig.SupporterList); return this.View(model); } /// Display the index page. [HttpGet("/privacy")] public ViewResult Privacy() { return this.View(); } /********* ** Private methods *********/ /// Get a sorted, parsed list of SMAPI downloads for the latest releases. private async Task GetReleaseVersionsAsync() { return await this.Cache.GetOrCreateAsync("available-versions", async entry => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); // get latest stable release GitRelease? release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); // strip 'noinclude' blocks from release description if (release != null) { HtmlDocument doc = new(); doc.LoadHtml(release.Body); foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? Array.Empty()) node.Remove(); release.Body = doc.DocumentNode.InnerHtml.Trim(); } // get versions return this .ParseReleaseVersions(release) .OrderBy(p => p.Version) .ToArray(); }); } /// Get a parsed list of SMAPI downloads for a release. /// The GitHub release. private IEnumerable ParseReleaseVersions(GitRelease? release) { if (release?.Assets == null) yield break; foreach (GitAsset asset in release.Assets) { if (asset.FileName.StartsWith("Z_")) continue; 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 isForDevs = match.Groups["forDevs"].Success; yield return new ReleaseVersion(release, asset, version, isForDevs); } } /// A parsed release download. private class ReleaseVersion { /********* ** Accessors *********/ /// The underlying GitHub release. public GitRelease Release { get; } /// The underlying download asset. public GitAsset Asset { get; } /// The SMAPI version. public ISemanticVersion Version { 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 'for developers' download. public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isForDevs) { this.Release = release; this.Asset = asset; this.Version = version; this.IsForDevs = isForDevs; } } } }