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("")]
[Route("install")]
internal class IndexController : Controller
{
/*********
** Properties
*********/
/// 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.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
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 && this.SiteConfig.BetaEnabled
? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl)
: null;
// render view
var model = new IndexModel(stableVersionModel, betaVersionModel, this.SiteConfig.BetaBlurb);
return this.View(model);
}
/*********
** 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 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;
}
}
// 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();
return stableVersions
.Concat(betaVersions)
.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)
{
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.IsPrerelease();
bool isForDevs = match.Groups["forDevs"].Success;
yield return new ReleaseVersion(release, asset, version, isBeta, 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 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, ISemanticVersion version, bool isBeta, bool isForDevs)
{
this.Release = release;
this.Asset = asset;
this.Version = version;
this.IsBeta = isBeta;
this.IsForDevs = isForDevs;
}
}
}
}