Use Octokit for Github lookups.

This commit is contained in:
clienthax 2021-08-30 14:39:05 +01:00
parent bb1bc8e858
commit bacc0ca3a9
11 changed files with 81 additions and 225 deletions

View File

@ -11,7 +11,9 @@ namespace CompatApiClient
public static class ApiConfig
{
public static readonly ProductInfoHeaderValue ProductInfoHeader = new("RPCS3CompatibilityBot", "2.0");
public static readonly string ProductName = "RPCS3CompatibilityBot";
public static readonly string ProductVersion = "2.0";
public static readonly ProductInfoHeaderValue ProductInfoHeader = new(ProductName, ProductVersion);
public static int Version { get; } = 1;
public static Uri BaseUrl { get; } = new("https://rpcs3.net/compatibility");
public static string DateInputFormat { get; } = "yyyy-M-d";

View File

@ -1,25 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CompatApiClient;
using CompatApiClient.Compression;
using CompatApiClient.Formatters;
using CompatApiClient.Utils;
using GithubClient.POCOs;
using Microsoft.Extensions.Caching.Memory;
namespace GithubClient
{
public class Client
{
private readonly HttpClient client;
private readonly JsonSerializerOptions jsonOptions;
private readonly Octokit.GitHubClient client;
private static readonly TimeSpan PrStatusCacheTime = TimeSpan.FromMinutes(3);
private static readonly TimeSpan IssueStatusCacheTime = TimeSpan.FromMinutes(30);
@ -30,40 +20,29 @@ namespace GithubClient
public static int RateLimitRemaining { get; private set; }
public static DateTime RateLimitResetTime { get; private set; }
public Client()
public Client(string? githubToken)
{
client = HttpClientFactory.Create(new CompressionMessageHandler());
jsonOptions = new()
client = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(ApiConfig.ProductName, ApiConfig.ProductVersion));
if (!string.IsNullOrEmpty(githubToken))
{
PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase,
IgnoreNullValues = true,
IncludeFields = true,
};
client.Credentials = new Octokit.Credentials(githubToken);
}
}
public async Task<PrInfo?> GetPrInfoAsync(int pr, CancellationToken cancellationToken)
public async Task<Octokit.PullRequest?> GetPrInfoAsync(int pr, CancellationToken cancellationToken)
{
if (StatusesCache.TryGetValue(pr, out PrInfo? result))
if (StatusesCache.TryGetValue(pr, out Octokit.PullRequest? result))
{
ApiConfig.Log.Debug($"Returned {nameof(PrInfo)} for {pr} from cache");
ApiConfig.Log.Debug($"Returned {nameof(Octokit.PullRequest)} for {pr} from cache");
return result;
}
try
{
using var message = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/RPCS3/rpcs3/pulls/" + pr);
message.Headers.UserAgent.Add(ApiConfig.ProductInfoHeader);
using var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
try
{
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
UpdateRateLimitStats(response.Headers);
result = await response.Content.ReadFromJsonAsync<PrInfo>(jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, response);
}
var request = client.PullRequest.Get("RPCS3", "rpcs3", pr);
request.Wait(cancellationToken);
result = (await request.ConfigureAwait(false));
UpdateRateLimitStats();
}
catch (Exception e)
{
@ -71,38 +50,29 @@ namespace GithubClient
}
if (result == null)
{
ApiConfig.Log.Debug($"Failed to get {nameof(PrInfo)}, returning empty result");
return new() { Number = pr };
ApiConfig.Log.Debug($"Failed to get {nameof(Octokit.PullRequest)}, returning empty result");
return new(pr);
}
StatusesCache.Set(pr, result, PrStatusCacheTime);
ApiConfig.Log.Debug($"Cached {nameof(PrInfo)} for {pr} for {PrStatusCacheTime}");
ApiConfig.Log.Debug($"Cached {nameof(Octokit.PullRequest)} for {pr} for {PrStatusCacheTime}");
return result;
}
public async Task<IssueInfo?> GetIssueInfoAsync(int issue, CancellationToken cancellationToken)
public async Task<Octokit.Issue?> GetIssueInfoAsync(int issue, CancellationToken cancellationToken)
{
if (IssuesCache.TryGetValue(issue, out IssueInfo? result))
if (IssuesCache.TryGetValue(issue, out Octokit.Issue? result))
{
ApiConfig.Log.Debug($"Returned {nameof(IssueInfo)} for {issue} from cache");
ApiConfig.Log.Debug($"Returned {nameof(Octokit.Issue)} for {issue} from cache");
return result;
}
try
{
using var message = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/RPCS3/rpcs3/issues/" + issue);
message.Headers.UserAgent.Add(ApiConfig.ProductInfoHeader);
using var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
try
{
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
UpdateRateLimitStats(response.Headers);
result = await response.Content.ReadFromJsonAsync<IssueInfo>(jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, response);
}
var request = client.Issue.Get("RPCS3", "rpcs3", issue);
request.Wait(cancellationToken);
result = (await request.ConfigureAwait(false));
UpdateRateLimitStats();
}
catch (Exception e)
{
@ -110,22 +80,31 @@ namespace GithubClient
}
if (result == null)
{
ApiConfig.Log.Debug($"Failed to get {nameof(IssueInfo)}, returning empty result");
return new() { Number = issue };
ApiConfig.Log.Debug($"Failed to get {nameof(Octokit.Issue)}, returning empty result");
return new() { };
}
IssuesCache.Set(issue, result, IssueStatusCacheTime);
ApiConfig.Log.Debug($"Cached {nameof(IssueInfo)} for {issue} for {IssueStatusCacheTime}");
ApiConfig.Log.Debug($"Cached {nameof(Octokit.Issue)} for {issue} for {IssueStatusCacheTime}");
return result;
}
public Task<List<PrInfo>?> GetOpenPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync("open", cancellationToken);
public Task<List<PrInfo>?> GetClosedPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync("closed&sort=updated&direction=desc", cancellationToken);
private async Task<List<PrInfo>?> GetPrsWithStatusAsync(string status, CancellationToken cancellationToken)
public Task<IReadOnlyList<Octokit.PullRequest>?> GetOpenPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync(new Octokit.PullRequestRequest
{
var requestUri = "https://api.github.com/repos/RPCS3/rpcs3/pulls?state=" + status;
if (StatusesCache.TryGetValue(requestUri, out List<PrInfo>? result))
State = Octokit.ItemStateFilter.Open
}, cancellationToken);
public Task<IReadOnlyList<Octokit.PullRequest>?> GetClosedPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync(new Octokit.PullRequestRequest
{
State = Octokit.ItemStateFilter.Closed,
SortProperty = Octokit.PullRequestSort.Updated,
SortDirection = Octokit.SortDirection.Descending
}, cancellationToken);
private async Task<IReadOnlyList<Octokit.PullRequest>?> GetPrsWithStatusAsync(Octokit.PullRequestRequest filter, CancellationToken cancellationToken)
{
var statusURI = "https://api.github.com/repos/RPCS3/rpcs3/pulls?state=" + filter.ToString();
if (StatusesCache.TryGetValue(statusURI, out IReadOnlyList<Octokit.PullRequest>? result))
{
ApiConfig.Log.Debug("Returned list of opened PRs from cache");
return result;
@ -133,19 +112,11 @@ namespace GithubClient
try
{
using var message = new HttpRequestMessage(HttpMethod.Get, requestUri);
message.Headers.UserAgent.Add(ApiConfig.ProductInfoHeader);
using var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
try
{
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
UpdateRateLimitStats(response.Headers);
result = await response.Content.ReadFromJsonAsync<List<PrInfo>>(jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, response);
}
var request = client.PullRequest.GetAllForRepository("RPCS3", "rpcs3", filter);
request.Wait(cancellationToken);
result = (await request.ConfigureAwait(false));
UpdateRateLimitStats();
}
catch (Exception e)
{
@ -153,7 +124,7 @@ namespace GithubClient
}
if (result != null)
{
StatusesCache.Set(requestUri, result, PrStatusCacheTime);
StatusesCache.Set(statusURI, result, PrStatusCacheTime);
foreach (var prInfo in result)
StatusesCache.Set(prInfo.Number, prInfo, PrStatusCacheTime);
ApiConfig.Log.Debug($"Cached list of open PRs for {PrStatusCacheTime}");
@ -161,62 +132,22 @@ namespace GithubClient
return result;
}
public async Task<List<StatusInfo>?> GetStatusesAsync(string statusesUrl, CancellationToken cancellationToken)
private void UpdateRateLimitStats()
{
if (StatusesCache.TryGetValue(statusesUrl, out List<StatusInfo>? result))
var apiInfo = client.GetLastApiInfo();
if (apiInfo == null)
{
ApiConfig.Log.Debug($"Returned cached item for {statusesUrl}");
return result;
return;
}
try
{
using var message = new HttpRequestMessage(HttpMethod.Get, statusesUrl);
message.Headers.UserAgent.Add(ApiConfig.ProductInfoHeader);
using var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
try
{
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
UpdateRateLimitStats(response.Headers);
result = await response.Content.ReadFromJsonAsync<List<StatusInfo>>(jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, response);
}
}
catch (Exception e)
{
ApiConfig.Log.Error(e);
}
RateLimit = apiInfo.RateLimit.Limit;
RateLimitRemaining = apiInfo.RateLimit.Remaining;
RateLimitResetTime = DateTimeOffset.FromUnixTimeSeconds(apiInfo.RateLimit.ResetAsUtcEpochSeconds).UtcDateTime;
if (result != null)
{
StatusesCache.Set(statusesUrl, result, PrStatusCacheTime);
ApiConfig.Log.Debug($"Cached item for {statusesUrl} for {PrStatusCacheTime}");
}
return result;
}
private static void UpdateRateLimitStats(HttpResponseHeaders headers)
{
if (headers.TryGetValues("X-RateLimit-Limit", out var rateLimitValues)
&& rateLimitValues.FirstOrDefault() is string limitValue
&& int.TryParse(limitValue, out var limit)
&& limit > 0)
RateLimit = limit;
if (headers.TryGetValues("X-RateLimit-Remaining", out var rateLimitRemainingValues)
&& rateLimitRemainingValues.FirstOrDefault() is string remainingValue
&& int.TryParse(remainingValue, out var remaining)
&& remaining > 0)
RateLimitRemaining = remaining;
if (headers.TryGetValues("X-RateLimit-Reset", out var rateLimitResetValues)
&& rateLimitResetValues.FirstOrDefault() is string resetValue
&& long.TryParse(resetValue, out var resetSeconds)
&& resetSeconds > 0)
RateLimitResetTime = DateTimeOffset.FromUnixTimeSeconds(resetSeconds).UtcDateTime;
if (RateLimitRemaining < 10)
ApiConfig.Log.Warn($"Github rate limit is low: {RateLimitRemaining} out of {RateLimit}, will be reset on {RateLimitResetTime:u}");
}
}
}

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Octokit" Version="0.50.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,64 +0,0 @@
using System;
using System.Diagnostics;
namespace GithubClient.POCOs
{
[DebuggerDisplay("{Body}", Name = "#{Number}")]
public sealed class PrInfo
{
public string? HtmlUrl;
public int Number;
public string? State;
public string? Title;
public GithubUser? User;
public string? Body;
public DateTime CreatedAt;
public DateTime? UpdatedAt;
public DateTime? ClosedAt;
public DateTime? MergedAt;
public string? MergeCommitSha;
public string? StatusesUrl;
public RefInfo? Head;
public RefInfo? Base;
public int Additions;
public int Deletions;
public int ChangedFiles;
public string? Message;
}
public sealed class IssueInfo
{
public string? HtmlUrl;
public int Number;
public string? State;
public string? Title;
public GithubUser? User;
public DateTime CreatedAt;
public DateTime? UpdatedAt;
public DateTime? ClosedAt;
public DateTime? MergedAt;
public string? Body;
public PullRequestReference? PullRequest;
}
public sealed class GithubUser
{
public string? Login;
}
public sealed class PullRequestReference
{
public string? Url;
public string? HtmlUrl;
public string? DiffUrl;
public string? PatchUrl;
}
public sealed class RefInfo
{
public string? Label;
public string? Ref;
public GithubUser? User;
public string? Sha;
}
}

View File

@ -1,14 +0,0 @@
using System;
namespace GithubClient.POCOs
{
public sealed class StatusInfo
{
public string? State; // success
public string? Description;
public string? TargetUrl;
public string? Context; // continuous-integration/appveyor/pr
public DateTime? CreatedAt;
public DateTime? UpdatedAt;
}
}

View File

@ -67,6 +67,7 @@ namespace CompatBot.Commands
.Append(string.IsNullOrEmpty(Config.AzureDevOpsToken) ? "❌" : "✅").AppendLine(" Azure DevOps")
.Append(string.IsNullOrEmpty(Config.AzureComputerVisionKey) ? "❌" : "✅").AppendLine(" Computer Vision")
.Append(string.IsNullOrEmpty(Config.AzureAppInsightsKey) ? "❌" : "✅").AppendLine(" AppInsights")
.Append(string.IsNullOrEmpty(Config.GithubToken) ? "❌" : "✅").AppendLine(" Github")
.ToString()
.Trim();

View File

@ -35,7 +35,7 @@ namespace CompatBot.Commands
internal sealed class CompatList : BaseCommandModuleCustom
{
private static readonly Client Client = new();
private static readonly GithubClient.Client GithubClient = new();
private static readonly GithubClient.Client GithubClient = new(Config.GithubToken);
private static readonly SemaphoreSlim UpdateCheck = new(1, 1);
private static string? lastUpdateInfo, lastFullBuildNumber;
private const string Rpcs3UpdateStateKey = "Rpcs3UpdateState";
@ -338,7 +338,7 @@ namespace CompatBot.Commands
var failedBuilds = await Config.GetAzureDevOpsClient().GetMasterBuildsAsync(
oldestPrCommit.MergeCommitSha,
newestPrCommit.MergeCommitSha,
oldestPrCommit.MergedAt,
oldestPrCommit.MergedAt?.DateTime,
cancellationToken
).ConfigureAwait(false);
foreach (var mergedPr in mergedPrs)

View File

@ -21,7 +21,7 @@ namespace CompatBot.Commands
[Description("Commands to list opened pull requests information")]
internal sealed class Pr: BaseCommandModuleCustom
{
private static readonly GithubClient.Client GithubClient = new();
private static readonly GithubClient.Client GithubClient = new(Config.GithubToken);
private static readonly CompatApiClient.Client CompatApiClient = new();
[GroupCommand]
@ -114,7 +114,7 @@ namespace CompatBot.Commands
var prInfo = await GithubClient.GetPrInfoAsync(pr, Config.Cts.Token).ConfigureAwait(false);
if (prInfo is null or {Number: 0})
{
await message.ReactWithAsync(Config.Reactions.Failure, prInfo?.Message ?? "PR not found").ConfigureAwait(false);
await message.ReactWithAsync(Config.Reactions.Failure, prInfo?.Title ?? "PR not found").ConfigureAwait(false);
return;
}
@ -134,7 +134,7 @@ namespace CompatBot.Commands
{
windowsDownloadText = "⏳ Pending...";
linuxDownloadText = "⏳ Pending...";
var latestBuild = await CirrusCi.GetPrBuildInfoAsync(commit, prInfo.MergedAt, pr, Config.Cts.Token).ConfigureAwait(false);
var latestBuild = await CirrusCi.GetPrBuildInfoAsync(commit, prInfo.MergedAt?.DateTime, pr, Config.Cts.Token).ConfigureAwait(false);
if (latestBuild == null)
{
if (state == "Open")

View File

@ -82,6 +82,7 @@ namespace CompatBot
public static string AzureComputerVisionEndpoint => config.GetValue(nameof(AzureComputerVisionEndpoint), "https://westeurope.api.cognitive.microsoft.com/");
public static Guid AzureDevOpsProjectId => config.GetValue(nameof(AzureDevOpsProjectId), new Guid("3598951b-4d39-4fad-ad3b-ff2386a649de"));
public static string AzureAppInsightsKey => config.GetValue(nameof(AzureAppInsightsKey), "");
public static string GithubToken => config.GetValue(nameof(GithubToken), "");
public static string PreferredFontFamily => config.GetValue(nameof(PreferredFontFamily), "");
public static string LogPath => config.GetValue(nameof(LogPath), "./logs/"); // paths are relative to the working directory
public static string IrdCachePath => config.GetValue(nameof(IrdCachePath), "./ird/");

View File

@ -1,11 +1,10 @@
using DSharpPlus.Entities;
using GithubClient.POCOs;
namespace CompatBot.Utils.ResultFormatters
{
internal static class PrInfoFormatter
{
public static DiscordEmbedBuilder AsEmbed(this PrInfo prInfo)
public static DiscordEmbedBuilder AsEmbed(this Octokit.PullRequest prInfo)
{
var state = prInfo.GetState();
var stateLabel = state.state == null ? null : $"[{state.state}] ";
@ -13,7 +12,7 @@ namespace CompatBot.Utils.ResultFormatters
return new DiscordEmbedBuilder {Title = title, Url = prInfo.HtmlUrl, Description = prInfo.Title, Color = state.color};
}
public static DiscordEmbedBuilder AsEmbed(this IssueInfo issueInfo)
public static DiscordEmbedBuilder AsEmbed(this Octokit.Issue issueInfo)
{
var state = issueInfo.GetState();
var stateLabel = state.state == null ? null : $"[{state.state}] ";
@ -21,12 +20,12 @@ namespace CompatBot.Utils.ResultFormatters
return new DiscordEmbedBuilder {Title = title, Url = issueInfo.HtmlUrl, Description = issueInfo.Title, Color = state.color};
}
public static (string? state, DiscordColor color) GetState(this PrInfo prInfo)
public static (string? state, DiscordColor color) GetState(this Octokit.PullRequest prInfo)
{
if (prInfo.State == "open")
if (prInfo.State == Octokit.ItemState.Open)
return ("Open", Config.Colors.PrOpen);
if (prInfo.State == "closed")
if (prInfo.State == Octokit.ItemState.Closed)
{
if (prInfo.MergedAt.HasValue)
return ("Merged", Config.Colors.PrMerged);
@ -37,12 +36,12 @@ namespace CompatBot.Utils.ResultFormatters
return (null, Config.Colors.DownloadLinks);
}
public static (string? state, DiscordColor color) GetState(this IssueInfo issueInfo)
public static (string? state, DiscordColor color) GetState(this Octokit.Issue issueInfo)
{
if (issueInfo.State == "open")
if (issueInfo.State == Octokit.ItemState.Open)
return ("Open", Config.Colors.PrOpen);
if (issueInfo.State == "closed")
if (issueInfo.State == Octokit.ItemState.Closed)
return ("Closed", Config.Colors.PrClosed);
return (null, Config.Colors.DownloadLinks);

View File

@ -10,15 +10,14 @@ using CompatBot.EventHandlers;
using CompatBot.Utils.Extensions;
using DSharpPlus;
using DSharpPlus.Entities;
using GithubClient.POCOs;
namespace CompatBot.Utils.ResultFormatters
{
internal static class UpdateInfoFormatter
{
private static readonly GithubClient.Client GithubClient = new();
private static readonly GithubClient.Client GithubClient = new(Config.GithubToken);
public static async Task<DiscordEmbedBuilder> AsEmbedAsync(this UpdateInfo? info, DiscordClient client, bool includePrBody = false, DiscordEmbedBuilder? builder = null, PrInfo? currentPrInfo = null)
public static async Task<DiscordEmbedBuilder> AsEmbedAsync(this UpdateInfo? info, DiscordClient client, bool includePrBody = false, DiscordEmbedBuilder? builder = null, Octokit.PullRequest? currentPrInfo = null)
{
if ((info?.LatestBuild?.Windows?.Download ?? info?.LatestBuild?.Linux?.Download) is null)
return builder ?? new DiscordEmbedBuilder {Title = "Error", Description = "Error communicating with the update API. Try again later.", Color = Config.Colors.Maintenance};
@ -28,7 +27,7 @@ namespace CompatBot.Utils.ResultFormatters
var latestPr = latestBuild?.Pr;
var currentPr = info.CurrentBuild?.Pr;
string? url = null;
PrInfo? latestPrInfo = null;
Octokit.PullRequest? latestPrInfo = null;
string prDesc = "";
if (!justAppend)
@ -134,14 +133,14 @@ namespace CompatBot.Utils.ResultFormatters
DateTime? latestBuildTimestamp = null, currentBuildTimestamp = null;
if (Config.GetAzureDevOpsClient() is {} azureClient)
{
var currentAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(currentCommit, currentPrInfo?.MergedAt, Config.Cts.Token).ConfigureAwait(false);
var latestAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(latestCommit, latestPrInfo?.MergedAt, Config.Cts.Token).ConfigureAwait(false);
var currentAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(currentCommit, currentPrInfo?.MergedAt?.DateTime, Config.Cts.Token).ConfigureAwait(false);
var latestAppveyorBuild = await azureClient.GetMasterBuildInfoAsync(latestCommit, latestPrInfo?.MergedAt?.DateTime, Config.Cts.Token).ConfigureAwait(false);
latestBuildTimestamp = latestAppveyorBuild?.FinishTime;
currentBuildTimestamp = currentAppveyorBuild?.FinishTime;
if (!latestBuildTimestamp.HasValue)
{
buildTimestampKind = "Merged";
latestBuildTimestamp = currentPrInfo?.MergedAt;
latestBuildTimestamp = currentPrInfo?.MergedAt?.DateTime;
}
}