From 4219147d40119dd1205bfdd5cd499a9bc461b227 Mon Sep 17 00:00:00 2001 From: Abrynos <6608231+Abrynos@users.noreply.github.com> Date: Sat, 22 Sep 2018 01:17:24 +0200 Subject: [PATCH] Add /Api/WWW/GitHub/Releases endpoints (#905) * Add /Api/WWW/GitHub/Releases endpoints * Do as Archi says * Make Changes in GitHubReleaseResponse a html string * Optimize Getter * Cache all the things * Misc. * Fix null error and Refactor a bit * https://www.youtube.com/watch?v=ajaR88AN44Y * Disable pragma for "never" assigned variables --- ArchiSteamFarm/ASF.cs | 34 +-- ArchiSteamFarm/GitHub.cs | 230 ++++++++++++++++++ .../IPC/Controllers/Api/GitHubController.cs | 41 ++++ .../IPC/Responses/GitHubReleaseResponse.cs | 30 +++ ArchiSteamFarm/Json/GitHub.cs | 57 ----- 5 files changed, 307 insertions(+), 85 deletions(-) create mode 100644 ArchiSteamFarm/GitHub.cs create mode 100644 ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs create mode 100644 ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs delete mode 100644 ArchiSteamFarm/Json/GitHub.cs diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs index eebe05a30..aff328001 100644 --- a/ArchiSteamFarm/ASF.cs +++ b/ArchiSteamFarm/ASF.cs @@ -21,13 +21,11 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Threading; using System.Threading.Tasks; -using ArchiSteamFarm.JSON; using ArchiSteamFarm.Localization; using ArchiSteamFarm.NLog; @@ -131,26 +129,10 @@ namespace ArchiSteamFarm { return null; } - string releaseURL = SharedInfo.GithubReleaseURL + (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.Stable ? "/latest" : "?per_page=1"); - - GitHub.ReleaseResponse releaseResponse; - - if (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.Stable) { - WebBrowser.ObjectResponse objectResponse = await Program.WebBrowser.UrlGetToJsonObject(releaseURL).ConfigureAwait(false); - if (objectResponse?.Content == null) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return null; - } - - releaseResponse = objectResponse.Content; - } else { - WebBrowser.ObjectResponse> objectResponse = await Program.WebBrowser.UrlGetToJsonObject>(releaseURL).ConfigureAwait(false); - if ((objectResponse?.Content == null) || (objectResponse.Content.Count == 0)) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return null; - } - - releaseResponse = objectResponse.Content[0]; + GitHub.ReleaseResponse releaseResponse = await GitHub.GetLatestRelease(Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); + if (releaseResponse == null) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); + return null; } if (string.IsNullOrEmpty(releaseResponse.Tag)) { @@ -197,12 +179,8 @@ namespace ArchiSteamFarm { return null; } - if (!string.IsNullOrEmpty(releaseResponse.ReleaseNotesInMarkdown)) { - string plainText = Utilities.MarkdownToText(releaseResponse.ReleaseNotesInMarkdown); - - if (!string.IsNullOrEmpty(plainText)) { - ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateChangelog, plainText)); - } + if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) { + ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateChangelog, releaseResponse.ChangelogPlainText)); } ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024)); diff --git a/ArchiSteamFarm/GitHub.cs b/ArchiSteamFarm/GitHub.cs new file mode 100644 index 000000000..a76f4df2b --- /dev/null +++ b/ArchiSteamFarm/GitHub.cs @@ -0,0 +1,230 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Markdig; +using Markdig.Renderers; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace ArchiSteamFarm { + internal static class GitHub { + private static MarkdownDocument ExtractChangelogFromBody(string markdownText) { + if (string.IsNullOrEmpty(markdownText)) { + ASF.ArchiLogger.LogNullError(nameof(markdownText)); + return null; + } + + MarkdownDocument markdownDocument = Markdown.Parse(markdownText); + bool insideChangelog = false; + + foreach (Block block in markdownDocument.ToList()) { + if (!insideChangelog) { + if (block is HeadingBlock headingBlock && (headingBlock.Inline.FirstChild != null) && headingBlock.Inline.FirstChild is LiteralInline literalInline && (literalInline.Content.ToString() == "Changelog")) { + insideChangelog = true; + } + + markdownDocument.Remove(block); + continue; + } + + if (block is ThematicBreakBlock) { + insideChangelog = false; + markdownDocument.Remove(block); + } + } + + return markdownDocument; + } + + internal static async Task> GetReleases(byte count) { + if (count == 0) { + ASF.ArchiLogger.LogNullError(nameof(count)); + return null; + } + + string releaseURL = SharedInfo.GithubReleaseURL + "?per_page=" + count; + + return await GetReleasesFromURL(releaseURL).ConfigureAwait(false); + } + + internal static async Task GetLatestRelease(bool stable = true) { + string releaseURL = SharedInfo.GithubReleaseURL + (stable ? "/latest" : "?per_page=1"); + + if (stable) { + return await GetReleaseFromURL(releaseURL).ConfigureAwait(false); + } + + List response = await GetReleasesFromURL(releaseURL).ConfigureAwait(false); + if (response == null || response.Count == 0) { + ASF.ArchiLogger.LogNullError(nameof(response)); + return null; + } + + return response.FirstOrDefault(); + } + + internal static async Task GetRelease(string version) { + if (string.IsNullOrEmpty(version)) { + ASF.ArchiLogger.LogNullError(nameof(version)); + return null; + } + + return await GetReleaseFromURL(SharedInfo.GithubReleaseURL + "/tags/" + version).ConfigureAwait(false); + } + + private static async Task GetReleaseFromURL(string releaseURL) { + if (string.IsNullOrEmpty(nameof(releaseURL))) { + ASF.ArchiLogger.LogNullError(nameof(releaseURL)); + return null; + } + + WebBrowser.ObjectResponse objectResponse = await Program.WebBrowser.UrlGetToJsonObject(releaseURL).ConfigureAwait(false); + if (objectResponse == null) { + ASF.ArchiLogger.LogNullError(nameof(objectResponse)); + return null; + } + + return objectResponse.Content; + } + + private static async Task> GetReleasesFromURL(string releaseURL) { + if (string.IsNullOrEmpty(nameof(releaseURL))) { + ASF.ArchiLogger.LogNullError(nameof(releaseURL)); + return null; + } + + WebBrowser.ObjectResponse> objectResponse = await Program.WebBrowser.UrlGetToJsonObject>(releaseURL).ConfigureAwait(false); + if ((objectResponse?.Content == null) || (objectResponse.Content.Count == 0)) { + return null; + } + + return objectResponse.Content; + } + + + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + internal sealed class ReleaseResponse { + [JsonProperty(PropertyName = "assets", Required = Required.Always)] + internal readonly HashSet Assets; + + [JsonProperty(PropertyName = "tag_name", Required = Required.Always)] + internal readonly string Tag; + +#pragma warning disable 649 + [JsonProperty(PropertyName = "body", Required = Required.Always)] + private readonly string MarkdownBody; +#pragma warning restore 649 + + [JsonProperty(PropertyName = "published_at", Required = Required.Always)] + internal readonly DateTime PublishedAt; + + [JsonProperty(PropertyName = "prerelease", Required = Required.Always)] + internal readonly bool IsPreRelease; + + private MarkdownDocument _Changelog; + + private MarkdownDocument Changelog { + get { + if (_Changelog != null) { + return _Changelog; + } + + return _Changelog = ExtractChangelogFromBody(MarkdownBody); + } + } + + private string _ChangelogHTML; + + internal string ChangelogHTML { + get { + if (_ChangelogHTML != null) { + return _ChangelogHTML; + } + + if (Changelog == null) { + ASF.ArchiLogger.LogNullError(nameof(Changelog)); + return null; + } + + using (StringWriter writer = new StringWriter()) { + HtmlRenderer renderer = new HtmlRenderer(writer); + renderer.Render(Changelog); + writer.Flush(); + + return _ChangelogHTML = writer.ToString(); + } + } + } + + internal string _ChangelogPlainText; + + internal string ChangelogPlainText { + get { + if (_ChangelogPlainText != null) { + return _ChangelogPlainText; + } + + if (Changelog == null) { + ASF.ArchiLogger.LogNullError(nameof(Changelog)); + return null; + } + + using (StringWriter writer = new StringWriter()) { + HtmlRenderer renderer = new HtmlRenderer(writer) { + EnableHtmlForBlock = false, + EnableHtmlForInline = false + }; + + renderer.Render(Changelog); + writer.Flush(); + + return _ChangelogPlainText = writer.ToString(); + } + } + } + + + // Deserialized from JSON + private ReleaseResponse() { } + + internal sealed class Asset { + [JsonProperty(PropertyName = "browser_download_url", Required = Required.Always)] + internal readonly string DownloadURL; + + [JsonProperty(PropertyName = "name", Required = Required.Always)] + internal readonly string Name; + + [JsonProperty(PropertyName = "size", Required = Required.Always)] + internal readonly uint Size; + + // Deserialized from JSON + private Asset() { } + } + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs new file mode 100644 index 000000000..e4f3dd80f --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs @@ -0,0 +1,41 @@ +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/WWW/GitHub")] + public sealed class GitHubController : ControllerBase { + [HttpGet("Releases/{version:required}")] + public async Task>> GetRelease(string version) { + if (string.IsNullOrEmpty(version)) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(version)))); + } + + GitHub.ReleaseResponse releaseResponse = await GitHub.GetRelease(version).ConfigureAwait(false); + if (releaseResponse == null) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); + } + + return Ok(new GenericResponse(new GitHubReleaseResponse(releaseResponse))); + } + + [HttpGet("Releases")] + public async Task>>> GetReleases([FromQuery] byte count = 10) { + if (count == 0) { + return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorIsEmpty, nameof(count)))); + } + + List response = await GitHub.GetReleases(count).ConfigureAwait(false); + if (response == null || response.Count == 0) { + return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); + } + + IEnumerable result = response.Select(singleResponse => new GitHubReleaseResponse(singleResponse)); + return Ok(new GenericResponse>(result)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs new file mode 100644 index 000000000..8ce19b4fa --- /dev/null +++ b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using System; + +namespace ArchiSteamFarm.IPC.Responses { + public sealed class GitHubReleaseResponse { + [JsonProperty] + private readonly string ChangelogHTML; + + [JsonProperty] + private readonly DateTime ReleasedAt; + + [JsonProperty] + private readonly bool Stable; + + [JsonProperty] + private readonly string Version; + + + internal GitHubReleaseResponse(GitHub.ReleaseResponse releaseResponse) { + if (releaseResponse == null) { + throw new ArgumentNullException(nameof(releaseResponse)); + } + + ChangelogHTML = releaseResponse.ChangelogHTML; + ReleasedAt = releaseResponse.PublishedAt; + Stable = !releaseResponse.IsPreRelease; + Version = releaseResponse.Tag; + } + } +} diff --git a/ArchiSteamFarm/Json/GitHub.cs b/ArchiSteamFarm/Json/GitHub.cs deleted file mode 100644 index b5d121efe..000000000 --- a/ArchiSteamFarm/Json/GitHub.cs +++ /dev/null @@ -1,57 +0,0 @@ -// _ _ _ ____ _ _____ -// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ -// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ -// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | -// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// -// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki -// Contact: JustArchi@JustArchi.net -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; - -namespace ArchiSteamFarm.JSON { - internal static class GitHub { - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - internal sealed class ReleaseResponse { - [JsonProperty(PropertyName = "assets", Required = Required.Always)] - internal readonly HashSet Assets; - - [JsonProperty(PropertyName = "body", Required = Required.Always)] - internal readonly string ReleaseNotesInMarkdown; - - [JsonProperty(PropertyName = "tag_name", Required = Required.Always)] - internal readonly string Tag; - - // Deserialized from JSON - private ReleaseResponse() { } - - internal sealed class Asset { - [JsonProperty(PropertyName = "browser_download_url", Required = Required.Always)] - internal readonly string DownloadURL; - - [JsonProperty(PropertyName = "name", Required = Required.Always)] - internal readonly string Name; - - [JsonProperty(PropertyName = "size", Required = Required.Always)] - internal readonly uint Size; - - // Deserialized from JSON - private Asset() { } - } - } - } -}