using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.LogParsing; using StardewModdingAPI.Web.Framework.LogParsing.Models; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers { /// <summary>Provides a web UI and API for parsing SMAPI log files.</summary> internal class LogParserController : Controller { /********* ** Fields *********/ /// <summary>The site config settings.</summary> private readonly SiteConfig Config; /// <summary>The underlying Pastebin client.</summary> private readonly IPastebinClient Pastebin; /// <summary>The underlying text compression helper.</summary> private readonly IGzipHelper GzipHelper; /********* ** Public methods *********/ /*** ** Constructor ***/ /// <summary>Construct an instance.</summary> /// <param name="siteConfig">The context config settings.</param> /// <param name="pastebin">The Pastebin API client.</param> /// <param name="gzipHelper">The underlying text compression helper.</param> public LogParserController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) { this.Config = siteConfig.Value; this.Pastebin = pastebin; this.GzipHelper = gzipHelper; } /*** ** Web UI ***/ /// <summary>Render the log parser UI.</summary> /// <param name="id">The paste ID.</param> /// <param name="raw">Whether to display the raw unparsed log.</param> [HttpGet] [Route("log")] [Route("log/{id}")] public async Task<ViewResult> Index(string id = null, bool raw = false) { // fresh page if (string.IsNullOrWhiteSpace(id)) return this.View("Index", this.GetModel(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", this.GetModel(id).SetResult(log, raw)); } /*** ** JSON ***/ /// <summary>Save raw log data.</summary> [HttpPost, AllowLargePosts] [Route("log")] public async Task<ActionResult> PostAsync() { // get raw log text string input = this.Request.Form["input"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(input)) return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty.")); // upload log input = this.GzipHelper.CompressString(input); SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", input); // handle errors if (!result.Success) return this.View("Index", this.GetModel(result.ID, 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()); } /********* ** Private methods *********/ /// <summary>Fetch raw text from Pastebin.</summary> /// <param name="id">The Pastebin paste ID.</param> private async Task<PasteInfo> GetAsync(string id) { PasteInfo response = await this.Pastebin.GetAsync(id); response.Content = this.GzipHelper.DecompressString(response.Content); return response; } /// <summary>Build a log parser model.</summary> /// <param name="pasteID">The paste ID.</param> /// <param name="uploadError">An error which occurred while uploading the log to Pastebin.</param> private LogParserModel GetModel(string pasteID, string uploadError = null) { string sectionUrl = this.Config.LogParserUrl; Platform? platform = this.DetectClientPlatform(); return new LogParserModel(sectionUrl, pasteID, platform) { UploadError = uploadError }; } /// <summary>Detect the viewer's OS.</summary> /// <returns>Returns the viewer OS if known, else null.</returns> private Platform? DetectClientPlatform() { string userAgent = this.Request.Headers["User-Agent"]; switch (userAgent) { case string ua when ua.Contains("Windows"): return Platform.Windows; case string ua when ua.Contains("Android"): // check for Android before Linux because Android user agents also contain Linux return Platform.Android; case string ua when ua.Contains("Linux"): return Platform.Linux; case string ua when ua.Contains("Mac"): return Platform.Mac; default: return null; } } } }