From a4310d28f79181cf2f26352aaca9b6d95e16ca5e Mon Sep 17 00:00:00 2001 From: JustArchi Date: Mon, 8 Mar 2021 22:27:03 +0100 Subject: [PATCH] Add /Api/WWW/GitHub/Wiki/History endpoint --- ArchiSteamFarm/GitHub.cs | 76 +++++++++++++++++++ .../IPC/Controllers/Api/WWWController.cs | 72 ++++++++++++------ 2 files changed, 123 insertions(+), 25 deletions(-) diff --git a/ArchiSteamFarm/GitHub.cs b/ArchiSteamFarm/GitHub.cs index 779c087a1..52c11bb10 100644 --- a/ArchiSteamFarm/GitHub.cs +++ b/ArchiSteamFarm/GitHub.cs @@ -20,10 +20,13 @@ // limitations under the License. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Linq; +using System.Net; using System.Threading.Tasks; using AngleSharp.Dom; using Markdig; @@ -54,6 +57,79 @@ namespace ArchiSteamFarm { return await GetReleaseFromURL(SharedInfo.GithubReleaseURL + "/tags/" + version).ConfigureAwait(false); } + internal static async Task?> GetWikiHistory(string page) { + if (string.IsNullOrEmpty(page)) { + throw new ArgumentNullException(nameof(page)); + } + + if (ASF.WebBrowser == null) { + throw new InvalidOperationException(nameof(ASF.WebBrowser)); + } + + string url = SharedInfo.ProjectURL + "/wiki/" + page + "/_history"; + + using WebBrowser.HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(url, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); + + if (response == null) { + return null; + } + + if (response.StatusCode.IsClientErrorCode()) { + return response.StatusCode switch { + HttpStatusCode.NotFound => new Dictionary(0), + _ => null + }; + } + + List revisionNodes = response.Content.SelectNodes("//li[contains(@class, 'wiki-history-revision')]"); + + Dictionary result = new(revisionNodes.Count); + + foreach (IElement revisionNode in revisionNodes) { + IElement? versionNode = revisionNode.SelectSingleElementNode(".//input/@value"); + + if (versionNode == null) { + ASF.ArchiLogger.LogNullError(nameof(versionNode)); + + return null; + } + + string versionText = versionNode.GetAttribute("value"); + + if (string.IsNullOrEmpty(versionText)) { + ASF.ArchiLogger.LogNullError(nameof(versionText)); + + return null; + } + + IElement? dateTimeNode = revisionNode.SelectSingleElementNode(".//relative-time/@datetime"); + + if (dateTimeNode == null) { + ASF.ArchiLogger.LogNullError(nameof(dateTimeNode)); + + return null; + } + + string dateTimeText = dateTimeNode.GetAttribute("datetime"); + + if (string.IsNullOrEmpty(dateTimeText)) { + ASF.ArchiLogger.LogNullError(nameof(dateTimeText)); + + return null; + } + + if (!DateTime.TryParse(dateTimeText, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out DateTime dateTime)) { + ASF.ArchiLogger.LogNullError(nameof(dateTime)); + + return null; + } + + result[versionText] = dateTime.ToUniversalTime(); + } + + return result; + } + internal static async Task GetWikiPage(string page, string? revision = null) { if (string.IsNullOrEmpty(page)) { throw new ArgumentNullException(nameof(page)); diff --git a/ArchiSteamFarm/IPC/Controllers/Api/WWWController.cs b/ArchiSteamFarm/IPC/Controllers/Api/WWWController.cs index d643559e4..f49e3681a 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/WWWController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/WWWController.cs @@ -20,6 +20,8 @@ // limitations under the License. using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Net; using System.Threading.Tasks; @@ -31,6 +33,51 @@ using Microsoft.AspNetCore.Mvc; namespace ArchiSteamFarm.IPC.Controllers.Api { [Route("Api/WWW")] public sealed class WWWController : ArchiController { + /// + /// Fetches history of specific GitHub page from ASF project. + /// + /// + /// This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime. + /// + [HttpGet("GitHub/Wiki/History/{page:required}")] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)] + public async Task> GitHubWikiHistoryGet(string page) { + if (string.IsNullOrEmpty(page)) { + throw new ArgumentNullException(nameof(page)); + } + + Dictionary? revisions = await GitHub.GetWikiHistory(page).ConfigureAwait(false); + + return revisions != null ? revisions.Count > 0 ? Ok(new GenericResponse>(revisions.ToImmutableDictionary())) : BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(page)))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); + } + + /// + /// Fetches specific GitHub page of ASF project. + /// + /// + /// This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime. + /// Specifying revision is optional - when not specified, will fetch latest available. If specified revision is invalid, GitHub will automatically fetch the latest revision as well. + /// + [HttpGet("GitHub/Wiki/Page/{page:required}")] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)] + public async Task> GitHubWikiPageGet(string page, [FromQuery] string? revision = null) { + if (string.IsNullOrEmpty(page)) { + throw new ArgumentNullException(nameof(page)); + } + + string? html = await GitHub.GetWikiPage(page, revision).ConfigureAwait(false); + + return html switch { + null => StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))), + "" => BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(page)))), + _ => Ok(new GenericResponse(html)) + }; + } + /// /// Fetches the most recent GitHub release of ASF project. /// @@ -81,31 +128,6 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { return releaseResponse != null ? Ok(new GenericResponse(new GitHubReleaseResponse(releaseResponse))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); } - /// - /// Fetches specific GitHub page of ASF project. - /// - /// - /// This is internal API being utilizied by our ASF-ui IPC frontend. You should not depend on existence of any /Api/WWW endpoints as they can disappear and change anytime. - /// Specifying revision is optional - when not specified, will fetch latest available. If specified revision is invalid, GitHub will automatically fetch the latest revision as well. - /// - [HttpGet("GitHub/Wiki/Page/{page:required}")] - [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] - [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)] - public async Task> GitHubWikiPageGet(string page, [FromQuery] string? revision = null) { - if (string.IsNullOrEmpty(page)) { - throw new ArgumentNullException(nameof(page)); - } - - string? html = await GitHub.GetWikiPage(page, revision).ConfigureAwait(false); - - return html switch { - null => StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))), - "" => BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(page)))), - _ => Ok(new GenericResponse(html)) - }; - } - /// /// Sends a HTTPS request through ASF's built-in HttpClient. ///