diff --git a/AppveyorClient/AppveyorClient.csproj b/AppveyorClient/AppveyorClient.csproj new file mode 100644 index 00000000..8d71c3a2 --- /dev/null +++ b/AppveyorClient/AppveyorClient.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + latest + + + + + + + + + + + diff --git a/AppveyorClient/Client.cs b/AppveyorClient/Client.cs new file mode 100644 index 00000000..f44281e4 --- /dev/null +++ b/AppveyorClient/Client.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using AppveyorClient.POCOs; +using CompatApiClient; +using CompatApiClient.Compression; +using CompatApiClient.Utils; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using JsonContractResolver = CompatApiClient.JsonContractResolver; + +namespace AppveyorClient +{ + public class Client + { + private readonly HttpClient client; + private readonly MediaTypeFormatterCollection formatters; + + private static readonly ProductInfoHeaderValue ProductInfoHeader = new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0"); + private static readonly TimeSpan CacheTime = TimeSpan.FromDays(1); + private static readonly MemoryCache ResponseCache = new MemoryCache(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromHours(1) }); + + public Client() + { + client = HttpClientFactory.Create(new CompressionMessageHandler()); + var settings = new JsonSerializerSettings + { + ContractResolver = new JsonContractResolver(NamingStyles.CamelCase), + NullValueHandling = NullValueHandling.Ignore + }; + formatters = new MediaTypeFormatterCollection(new[] { new JsonMediaTypeFormatter { SerializerSettings = settings } }); + } + + public async Task GetPrDownloadAsync(string githubStatusTargetUrl, CancellationToken cancellationToken) + { + try + { + if (!int.TryParse(new Uri(githubStatusTargetUrl).Segments.Last(), out var buildNumber)) + return null; + + var buildInfo = await GetBuildInfoAsync(buildNumber, cancellationToken).ConfigureAwait(false); + var job = buildInfo?.Build.Jobs?.FirstOrDefault(j => j.Status == "success"); + if (string.IsNullOrEmpty(job?.JobId)) + return null; + + var artifacts = await GetJobArtifactsAsync(job.JobId, cancellationToken).ConfigureAwait(false); + var rpcs3Build = artifacts?.FirstOrDefault(a => a.Name == "rpcs3"); + if (rpcs3Build == null) + return null; + + var result = new ArtifactInfo + { + Artifact = rpcs3Build, + DownloadUrl = $"https://ci.appveyor.com/api/buildjobs/{job.JobId}/artifacts/{rpcs3Build.FileName}", + }; + ResponseCache.Set(githubStatusTargetUrl, result, CacheTime); + return result; + } + catch (Exception e) + { + ApiConfig.Log.Error(e); + } + ResponseCache.TryGetValue(githubStatusTargetUrl, out var o); + return o as ArtifactInfo; + } + + public async Task GetBuildInfoAsync(int buildNumber, CancellationToken cancellationToken) + { + var requestUri = "https://ci.appveyor.com/api/projects/rpcs3/rpcs3/builds/" + buildNumber; + try + { + using (var message = new HttpRequestMessage(HttpMethod.Get, requestUri)) + { + message.Headers.UserAgent.Add(ProductInfoHeader); + using (var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false)) + { + try + { + await response.Content.LoadIntoBufferAsync().ConfigureAwait(false); + var result = await response.Content.ReadAsAsync(formatters, cancellationToken).ConfigureAwait(false); + ResponseCache.Set(requestUri, result, CacheTime); + return result; + } + catch (Exception e) + { + ConsoleLogger.PrintError(e, response); + } + } + } + } + catch (Exception e) + { + ApiConfig.Log.Error(e); + } + ResponseCache.TryGetValue(requestUri, out var o); + return o as BuildInfo; + } + + public async Task> GetJobArtifactsAsync(string jobId, CancellationToken cancellationToken) + { + var requestUri = $"https://ci.appveyor.com/api/buildjobs/{jobId}/artifacts"; + try + { + using (var message = new HttpRequestMessage(HttpMethod.Get, requestUri)) + { + message.Headers.UserAgent.Add(ProductInfoHeader); + using (var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false)) + { + try + { + await response.Content.LoadIntoBufferAsync().ConfigureAwait(false); + var result = await response.Content.ReadAsAsync>(formatters, cancellationToken).ConfigureAwait(false); + ResponseCache.Set(requestUri, result, CacheTime); + return result; + } + catch (Exception e) + { + ConsoleLogger.PrintError(e, response); + } + } + } + } + catch (Exception e) + { + ApiConfig.Log.Error(e); + } + ResponseCache.TryGetValue(requestUri, out var o); + return o as List; + } + } +} diff --git a/AppveyorClient/POCOs/Artifact.cs b/AppveyorClient/POCOs/Artifact.cs new file mode 100644 index 00000000..9356f2a1 --- /dev/null +++ b/AppveyorClient/POCOs/Artifact.cs @@ -0,0 +1,13 @@ +using System; + +namespace AppveyorClient.POCOs +{ + public class Artifact + { + public DateTime? Created; + public string FileName; + public string Name; + public long Size; + public string Type; + } +} \ No newline at end of file diff --git a/AppveyorClient/POCOs/ArtifactInfo.cs b/AppveyorClient/POCOs/ArtifactInfo.cs new file mode 100644 index 00000000..367d7e3b --- /dev/null +++ b/AppveyorClient/POCOs/ArtifactInfo.cs @@ -0,0 +1,8 @@ +namespace AppveyorClient.POCOs +{ + public class ArtifactInfo + { + public Artifact Artifact; + public string DownloadUrl; + } +} \ No newline at end of file diff --git a/AppveyorClient/POCOs/Build.cs b/AppveyorClient/POCOs/Build.cs new file mode 100644 index 00000000..fd10c82d --- /dev/null +++ b/AppveyorClient/POCOs/Build.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace AppveyorClient.POCOs +{ + public class Build + { + public string AuthorName; + public string AuthorUsername; + public string Branch; + public int BuildId; + public int BuildNumber; + public DateTime? Created; + public DateTime? Started; + public DateTime? Updated; + public DateTime? Finished; + public List Jobs; + public string Message; + public string PullRequestHeadBranch; + public string PullRequestHeadCommitId; + public string PullRequestHeadRepository; + public string PullRequestId; + public string PullRequestName; + public string Status; + public string Version; + } +} \ No newline at end of file diff --git a/AppveyorClient/POCOs/BuildInfo.cs b/AppveyorClient/POCOs/BuildInfo.cs new file mode 100644 index 00000000..a671020c --- /dev/null +++ b/AppveyorClient/POCOs/BuildInfo.cs @@ -0,0 +1,10 @@ +using System.Text; + +namespace AppveyorClient.POCOs +{ + public class BuildInfo + { + public Build Build; + public Project Project; + } +} diff --git a/AppveyorClient/POCOs/Job.cs b/AppveyorClient/POCOs/Job.cs new file mode 100644 index 00000000..15d4bd58 --- /dev/null +++ b/AppveyorClient/POCOs/Job.cs @@ -0,0 +1,17 @@ +using System; + +namespace AppveyorClient.POCOs +{ + public class Job + { + public int ArtifactsCount; + public int CompilationErrorsCount; + public DateTime? Created; + public DateTime? Started; + public DateTime? Updated; + public DateTime? Finished; + public string OsType; + public string Status; + public string JobId; + } +} \ No newline at end of file diff --git a/AppveyorClient/POCOs/Project.cs b/AppveyorClient/POCOs/Project.cs new file mode 100644 index 00000000..128d5458 --- /dev/null +++ b/AppveyorClient/POCOs/Project.cs @@ -0,0 +1,4 @@ +namespace AppveyorClient.POCOs +{ + public class Project { } +} \ No newline at end of file diff --git a/CompatApiClient/Utils/Utils.cs b/CompatApiClient/Utils/Utils.cs index ae9bc80c..dbbe0bbb 100644 --- a/CompatApiClient/Utils/Utils.cs +++ b/CompatApiClient/Utils/Utils.cs @@ -4,6 +4,10 @@ namespace CompatApiClient.Utils { public static class Utils { + private const long UnderKB = 1000; + private const long UnderMB = 1000 * 1024; + private const long UnderGB = 1000 * 1024 * 1024; + public static string Trim(this string str, int maxLength) { const int minSaneLimit = 4; @@ -43,5 +47,16 @@ namespace CompatApiClient.Utils { return Math.Min(high, Math.Max(amount, low)); } + + public static string AsStorageUnit(this long bytes) + { + if (bytes < UnderKB) + return $"{bytes} byte{(bytes == 1 ? "" : "s")}"; + if (bytes < UnderMB) + return $"{bytes / 1024.0:0.##} KB"; + if (bytes < UnderGB) + return $"{bytes / 1024.0 / 1024:0.##} MB"; + return $"{bytes / 1024.0 / 1024 / 1024:0.##} GB"; + } } } diff --git a/CompatBot/Commands/Pr.cs b/CompatBot/Commands/Pr.cs index 1ac00c27..1d9cff0f 100644 --- a/CompatBot/Commands/Pr.cs +++ b/CompatBot/Commands/Pr.cs @@ -1,5 +1,8 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; +using CompatApiClient.Utils; +using CompatBot.Utils; using CompatBot.Utils.ResultFormatters; using DSharpPlus.CommandsNext; using DSharpPlus.CommandsNext.Attributes; @@ -12,12 +15,19 @@ namespace CompatBot.Commands internal sealed class Pr: BaseCommandModuleCustom { private static readonly GithubClient.Client githubClient = new GithubClient.Client(); + private static readonly AppveyorClient.Client appveyorClient = new AppveyorClient.Client(); private const string appveyorContext = "continuous-integration/appveyor/pr"; [GroupCommand] public async Task List(CommandContext ctx, [Description("Get information for specific PR number")] int pr) { var prInfo = await githubClient.GetPrInfoAsync(pr.ToString(), Config.Cts.Token).ConfigureAwait(false); + if (prInfo.Number == 0) + { + await ctx.ReactWithAsync(Config.Reactions.Failure, prInfo.Message ?? "PR not found").ConfigureAwait(false); + return; + } + var embed = prInfo.AsEmbed(); if (prInfo.State == "open") { @@ -25,16 +35,27 @@ namespace CompatBot.Commands { var statuses = await githubClient.GetStatusesAsync(statusesUrl, Config.Cts.Token).ConfigureAwait(false); statuses = statuses?.Where(s => s.Context == appveyorContext).ToList(); + var downloadHeader = "PR Build Download"; var downloadText = ""; if (statuses?.Count > 0) { if (statuses.FirstOrDefault(s => s.State == "success") is StatusInfo statusSuccess) - downloadText = $"[⏬ {statusSuccess.Description}]({statusSuccess.TargetUrl})"; + { + var artifactInfo = await appveyorClient.GetPrDownloadAsync(statusSuccess.TargetUrl, Config.Cts.Token).ConfigureAwait(false); + if (artifactInfo == null) + downloadText = $"[⏬ {statusSuccess.Description}]({statusSuccess.TargetUrl})"; + else + { + if (artifactInfo.Artifact.Created is DateTime buildTime) + downloadHeader = $"{downloadHeader} ({buildTime:u})"; + downloadText = $"[⏬ {artifactInfo.Artifact.FileName}]({artifactInfo.DownloadUrl})"; + } + } else downloadText = statuses.First().Description; } if (!string.IsNullOrEmpty(downloadText)) - embed.AddField("AppVeyor Download", downloadText); + embed.AddField(downloadHeader, downloadText); } } await ctx.RespondAsync(embed: embed).ConfigureAwait(false); diff --git a/CompatBot/CompatBot.csproj b/CompatBot/CompatBot.csproj index 87c7ff6b..2ed8758c 100644 --- a/CompatBot/CompatBot.csproj +++ b/CompatBot/CompatBot.csproj @@ -32,6 +32,7 @@ + diff --git a/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs b/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs index 992e1ddd..3aa5d6b4 100644 --- a/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs +++ b/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs @@ -13,10 +13,6 @@ namespace CompatBot.Utils.ResultFormatters { internal static class TitlePatchFormatter { - private const long UnderKB = 1000; - private const long UnderMB = 1000 * 1024; - private const long UnderGB = 1000 * 1024 * 1024; - // thanks BCES00569 public static async Task> AsEmbedAsync(this TitlePatch patch, DiscordClient client, string productCode) { @@ -77,16 +73,5 @@ namespace CompatBot.Utils.ResultFormatters catch { } return fname; } - - private static string AsStorageUnit(this long bytes) - { - if (bytes < UnderKB) - return $"{bytes} byte{StringUtils.GetSuffix(bytes)}"; - if (bytes < UnderMB) - return $"{bytes / 1024.0:0.##} KB"; - if (bytes < UnderGB) - return $"{bytes / 1024.0 / 1024:0.##} MB"; - return $"{bytes / 1024.0 / 1024 / 1024:0.##} GB"; - } } } diff --git a/GithubClient/POCOs/PrInfo.cs b/GithubClient/POCOs/PrInfo.cs index ea42c4eb..6ed5aa7f 100644 --- a/GithubClient/POCOs/PrInfo.cs +++ b/GithubClient/POCOs/PrInfo.cs @@ -17,6 +17,7 @@ namespace GithubClient.POCOs public int Additions; public int Deletions; public int ChangedFiles; + public string Message; } public class GithubUser diff --git a/discord-bot-net.sln b/discord-bot-net.sln index 0a9a4d47..8bbce05c 100644 --- a/discord-bot-net.sln +++ b/discord-bot-net.sln @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Clients", "Clients", "{E7FE EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GithubClient", "GithubClient\GithubClient.csproj", "{AF8FDA29-864E-4A1C-9568-99DECB7E4B36}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppveyorClient", "AppveyorClient\AppveyorClient.csproj", "{595ED201-1456-49F9-AD60-54B08499A5C1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +54,10 @@ Global {AF8FDA29-864E-4A1C-9568-99DECB7E4B36}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF8FDA29-864E-4A1C-9568-99DECB7E4B36}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF8FDA29-864E-4A1C-9568-99DECB7E4B36}.Release|Any CPU.Build.0 = Release|Any CPU + {595ED201-1456-49F9-AD60-54B08499A5C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {595ED201-1456-49F9-AD60-54B08499A5C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {595ED201-1456-49F9-AD60-54B08499A5C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {595ED201-1456-49F9-AD60-54B08499A5C1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -61,6 +67,7 @@ Global {AA5FF441-BD1D-4444-9178-7DC7BFF3C139} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2} {AA2A333B-CD30-41A5-A680-CC9BCB2D726B} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2} {AF8FDA29-864E-4A1C-9568-99DECB7E4B36} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2} + {595ED201-1456-49F9-AD60-54B08499A5C1} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D7696F56-AEAC-4D83-9BD8-BE0C122A5DCE}