add arm build downloads

This commit is contained in:
13xforever
2025-04-05 16:57:21 +05:00
parent 933b2248de
commit afbd39e7dd
15 changed files with 423 additions and 181 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
@@ -16,6 +17,7 @@ namespace CompatApiClient;
public class Client: IDisposable
{
private static readonly MemoryCache ResponseCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromHours(1) });
private static readonly string[] BuildArchList = [ArchType.X64, ArchType.Arm];
private readonly HttpClient client = HttpClientFactory.Create(new CompressionMessageHandler());
private readonly JsonSerializerOptions jsonOptions = new()
@@ -100,33 +102,46 @@ public class Client: IDisposable
}
// https://github.com/AniLeo/rpcs3-compatibility/wiki/API:-Update
public async ValueTask<UpdateInfo?> GetUpdateAsync(CancellationToken cancellationToken, string? commit = null)
public async ValueTask<UpdateInfo> GetUpdateAsync(CancellationToken cancellationToken, string? commit = null)
{
if (string.IsNullOrEmpty(commit))
if (commit is not {Length: >6})
commit = "somecommit";
var tries = 3;
do
var result = new UpdateInfo();
foreach (var arch in BuildArchList)
{
try
var tries = 3;
do
{
using var message = new HttpRequestMessage(HttpMethod.Get, "https://update.rpcs3.net/?api=v1&c=" + commit);
using var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
try
{
return await response.Content.ReadFromJsonAsync<UpdateInfo>(jsonOptions, cancellationToken).ConfigureAwait(false);
using var message = new HttpRequestMessage(
HttpMethod.Get,
$"https://update.rpcs3.net/?api=v3&os_arch={arch}&os_type=all&c={commit}"
);
using var response = await client
.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken)
.ConfigureAwait(false);
try
{
var info = await response.Content
.ReadFromJsonAsync<UpdateCheckResult>(jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (info is not null)
result[arch] = info;
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, response, false);
}
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, response, false);
ApiConfig.Log.Warn(e);
}
}
catch (Exception e)
{
ApiConfig.Log.Warn(e);
}
tries++;
} while (tries < 3);
return null;
tries++;
} while (tries < 3);
}
return result;
}
public void Dispose()

View File

@@ -0,0 +1,47 @@
namespace CompatApiClient.POCOs;
public class UpdateCheckResult
{
public StatusCode ReturnCode;
public BuildInfo LatestBuild = null!;
public BuildInfo? CurrentBuild;
public VersionInfo[]? Changelog;
}
public class BuildInfo
{
public int? Pr;
public string Datetime = null!;
public string Version = null!;
public BuildLink? Windows;
public BuildLink? Linux;
public BuildLink? Mac;
}
public class BuildLink
{
public string Download = null!;
public int? Size;
public string? Checksum;
}
public class VersionInfo
{
public string Verison = null!;
public string? Title;
}
public enum StatusCode
{
IllegalSearch = -3,
Maintenance = -2,
UnknownBuild = -1,
NoUpdates = 0,
UpdatesAvailable = 1,
}
public static class ArchType
{
public const string X64 = "x64";
public const string Arm = "arm64";
}

View File

@@ -1,27 +0,0 @@
namespace CompatApiClient.POCOs;
#nullable disable
public class UpdateInfo
{
public int ReturnCode;
public BuildInfo LatestBuild;
public BuildInfo CurrentBuild;
}
public class BuildInfo
{
public int? Pr;
public string Datetime;
public BuildLink Windows;
public BuildLink Linux;
public BuildLink Mac;
}
public class BuildLink
{
public string Download;
public int? Size;
public string Checksum;
}
#nullable restore

View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using CompatApiClient.POCOs;
namespace CompatApiClient;
public class UpdateInfo
{
public UpdateCheckResult? X64;
public UpdateCheckResult? Arm;
public UpdateCheckResult? this[string key]
{
get => key switch
{
ArchType.X64 => X64,
ArchType.Arm => Arm,
_ => throw new KeyNotFoundException($"Unknown {nameof(ArchType)} '{key}'")
};
set
{
if (key is ArchType.X64)
X64 = value;
else if (key is ArchType.Arm)
Arm = value;
else
throw new KeyNotFoundException($"Unknown {nameof(ArchType)} '{key}'");
}
}
public void SetCurrentAsLatest()
{
if (this is {X64.CurrentBuild: not null, Arm.CurrentBuild: not null})
{
X64.LatestBuild = X64.CurrentBuild;
X64.CurrentBuild = null;
Arm.LatestBuild = Arm.CurrentBuild;
Arm.CurrentBuild = null;
}
else if (X64?.CurrentBuild is not null)
{
X64.LatestBuild = X64.CurrentBuild;
X64.CurrentBuild = null;
Arm = null;
}
else if (Arm?.CurrentBuild is not null)
{
Arm.LatestBuild = Arm.CurrentBuild;
Arm.CurrentBuild = null;
X64 = null;
}
}
public StatusCode ReturnCode => (X64?.ReturnCode, Arm?.ReturnCode) switch
{
({ } v1, { } v2) when v1 == v2 => v1,
({ } v1 and >= StatusCode.UnknownBuild, { } v2 and >= StatusCode.UnknownBuild) => (StatusCode)Math.Max((int)v1,
(int)v2),
({ }, { }) => StatusCode.Maintenance,
({ } v, null) => v,
(null, { } v) => v,
_ => StatusCode.Maintenance,
};
public DateTime? LatestDatetime => ((X64?.LatestBuild.Datetime, Arm?.LatestBuild.Datetime) switch
{
({ Length: > 0 } d1, { Length: > 0 } d2) => StringComparer.Ordinal.Compare(d1, d1) >= 0 ? d1 : d2,
({ Length: > 0 } d, _) => d,
(_, { Length: > 0 } d) => d,
_ => null,
}) switch
{
{ Length: > 0 } v when DateTime.TryParse(v, out var result) => result,
_ => null,
};
public DateTime? CurrentDatetime => ((X64?.CurrentBuild?.Datetime, Arm?.CurrentBuild?.Datetime) switch
{
({ Length: > 0 } d1, { Length: > 0 } d2) => StringComparer.Ordinal.Compare(d1, d1) >= 0 ? d1 : d2,
({ Length: > 0 } d, _) => d,
(_, { Length: > 0 } d) => d,
_ => null,
}) switch
{
{ Length: > 0 } v when DateTime.TryParse(v, out var result) => result,
_ => null,
};
public int? LatestPr => (X64?.LatestBuild.Pr, Arm?.LatestBuild.Pr) switch
{
//(int pr1, int pr2) when pr1 != pr2 => throw new InvalidDataException($"Expected the same PR for both {nameof(ArchType)}, but got {pr1} and {pr2}"),
(int pr, _) => pr,
(_, int pr) => pr,
_ => null,
};
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@@ -90,7 +91,6 @@ public partial class Client
ExcludePullRequests = false,
Event = "pull_request",
HeadSha = commit,
//Branch = $"refs/pull/{pr}/merge",
};
var runsList = await client.Actions.Workflows.Runs.ListByWorkflow(OwnerId, RepoId, wfId, wfrRequest).ConfigureAwait(false);
var builds = runsList.WorkflowRuns
@@ -299,4 +299,72 @@ public partial class Client
BuildInfoCache.Set(cacheKey, result, TimeSpan.FromDays(1));
return result;
}
public async ValueTask<List<BuildInfo>?> GetMasterBuildsAsync(string? oldestMergeCommit, string? newestMergeCommit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
{
if (oldestMergeCommit is not {Length: >=6} || newestMergeCommit is not {Length: >=6})
return null;
if (await GetWorkflowIdAsync().ConfigureAwait(false) is not long wfId)
return null;
oldestMergeCommit = oldestMergeCommit.ToLower();
newestMergeCommit = newestMergeCommit.ToLower();
var wfrRequest = new WorkflowRunsRequest
{
ExcludePullRequests = true,
Event = "push",
Created = $"{oldestTimestamp:yyyy-MM-dd}..*",
Status = CheckRunStatusFilter.Completed,
Branch = "master",
};
var runsList = await client.Actions.Workflows.Runs.ListByWorkflow(OwnerId, RepoId, wfId, wfrRequest).ConfigureAwait(false);
var builds = runsList.WorkflowRuns
.OrderByDescending(r => r.CreatedAt)
.SkipWhile(b => !newestMergeCommit.Equals(b.HeadSha, StringComparison.OrdinalIgnoreCase))
.Skip(1)
.TakeWhile(b => !oldestMergeCommit.Equals(b.HeadSha, StringComparison.OrdinalIgnoreCase))
.ToList();
return await builds
.ToAsyncEnumerable()
.SelectAwait(async b => await GetArtifactsInfoAsync(b.HeadSha, b, cancellationToken).ConfigureAwait(false))
.ToListAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
public async ValueTask<BuildInfo?> GetMasterBuildInfoAsync(string? commit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
{
if (commit is not {Length: >=6})
return null;
if (await GetWorkflowIdAsync().ConfigureAwait(false) is not long wfId)
return null;
commit = commit.ToLower();
if (BuildInfoCache.TryGetValue(commit, out BuildInfo? result) && result is not null)
return result;
var wfrRequest = new WorkflowRunsRequest
{
ExcludePullRequests = true,
Event = "push",
Created = $"{oldestTimestamp:yyyy-MM-dd}..*",
Status = CheckRunStatusFilter.Completed,
Branch = "master",
};
var runsList = await client.Actions.Workflows.Runs.ListByWorkflow(OwnerId, RepoId, wfId, wfrRequest).ConfigureAwait(false);
var builds = runsList.WorkflowRuns
.Where(b => commit.Equals(b.HeadSha, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(b => b.CreatedAt)
.ToList();
if (builds.FirstOrDefault() is not {} latestBuild)
return null;
result = await GetArtifactsInfoAsync(commit, latestBuild, cancellationToken).ConfigureAwait(false);
if (result is { Status: WorkflowRunStatus.Completed, Result: WorkflowRunConclusion.Success })
BuildInfoCache.Set(commit, result, TimeSpan.FromHours(1));
return result;
}
}

View File

@@ -8,6 +8,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.3" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="SharpCompress" Version="0.39.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CompatApiClient\CompatApiClient.csproj" />

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using CompatApiClient;
using CompatApiClient.POCOs;
using CompatBot.Database;
using CompatBot.EventHandlers;
@@ -7,6 +8,7 @@ using CompatBot.Utils.ResultFormatters;
using DSharpPlus.Commands.Processors.TextCommands;
using Microsoft.EntityFrameworkCore;
using Microsoft.TeamFoundation.Build.WebApi;
using Octokit;
namespace CompatBot.Commands;
@@ -19,12 +21,12 @@ internal static partial class CompatList
[Description("Link to the latest RPCS3 build")]
public static ValueTask Latest(SlashCommandContext ctx) => CheckForRpcs3UpdatesAsync(ctx, respond: true);
/*
#if DEBUG
[Command("since")]
[Description("Show additional info about changes since specified update")]
public static ValueTask Since(TextCommandContext ctx, [Description("Commit hash of the update, such as `46abe0f31`")] string commit)
public static ValueTask Since(SlashCommandContext ctx, [Description("Commit hash of the update, such as `46abe0f31`")] string commit)
=> CheckForRpcs3UpdatesAsync(ctx, respond: true, sinceCommit: commit);
*/
#endif
[Command("clear"), RequiresBotModRole]
[Description("Clear the update info cache and post the latest RPCS3 build announcement")]
@@ -69,13 +71,13 @@ internal static partial class CompatList
var updateAnnouncementRestore = emptyBotMsg is not null;
var info = await Client.GetUpdateAsync(Config.Cts.Token, sinceCommit).ConfigureAwait(false);
if (info?.ReturnCode != 1 && sinceCommit != null)
if (info.ReturnCode != StatusCode.UpdatesAvailable && sinceCommit is not null)
info = await Client.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false);
if (updateAnnouncementRestore && info?.CurrentBuild != null)
info.LatestBuild = info.CurrentBuild;
if (updateAnnouncementRestore)
info.SetCurrentAsLatest();
var embed = await info.AsEmbedAsync(discordClient, !respond).ConfigureAwait(false);
if (info is null || embed.Color!.Value.Value == Config.Colors.Maintenance.Value)
if (info.ReturnCode < StatusCode.UnknownBuild || embed.Color?.Value == Config.Colors.Maintenance.Value)
{
if (updateAnnouncementRestore)
{
@@ -87,10 +89,8 @@ internal static partial class CompatList
}
else if (!updateAnnouncementRestore)
{
if (cachedUpdateInfo?.LatestBuild?.Datetime is string previousBuildTimeStr
&& info.LatestBuild?.Datetime is string newBuildTimeStr
&& DateTime.TryParse(previousBuildTimeStr, out var previousBuildTime)
&& DateTime.TryParse(newBuildTimeStr, out var newBuildTime)
if (cachedUpdateInfo?.LatestDatetime is DateTime previousBuildTime
&& info.LatestDatetime is DateTime newBuildTime
&& newBuildTime > previousBuildTime)
cachedUpdateInfo = info;
}
@@ -113,7 +113,7 @@ internal static partial class CompatList
return;
}
var latestUpdatePr = info?.LatestBuild?.Pr?.ToString();
var latestUpdatePr = info.LatestPr?.ToString();
var match = (
from field in embed.Fields
let m = UpdateVersionRegex().Match(field.Value)
@@ -122,15 +122,15 @@ internal static partial class CompatList
).FirstOrDefault();
var latestUpdateBuild = match?.Groups["build"].Value;
if (string.IsNullOrEmpty(latestUpdatePr)
if (latestUpdatePr is not {Length: >0}
|| lastUpdateInfo == latestUpdatePr
|| !await UpdateCheck.WaitAsync(0).ConfigureAwait(false))
return;
try
{
if (!string.IsNullOrEmpty(lastFullBuildNumber)
&& !string.IsNullOrEmpty(latestUpdateBuild)
if (lastFullBuildNumber is {Length: >0}
&& latestUpdateBuild is {Length: >0}
&& int.TryParse(lastFullBuildNumber, out var lastSaveBuild)
&& int.TryParse(latestUpdateBuild, out var latestBuild)
&& latestBuild <= lastSaveBuild)
@@ -147,7 +147,7 @@ internal static partial class CompatList
return;
}
if (embed.Color!.Value.Value == Config.Colors.Maintenance.Value)
if (embed.Color?.Value == Config.Colors.Maintenance.Value)
return;
await CheckMissedBuildsBetweenAsync(discordClient, compatChannel, lastUpdateInfo, latestUpdatePr, Config.Cts.Token).ConfigureAwait(false);
@@ -157,18 +157,14 @@ internal static partial class CompatList
lastFullBuildNumber = latestUpdateBuild;
await using (var wdb = await BotDb.OpenWriteAsync().ConfigureAwait(false))
{
var currentState = await wdb.BotState.FirstOrDefaultAsync(k => k.Key == Rpcs3UpdateStateKey)
.ConfigureAwait(false);
var currentState = await wdb.BotState.FirstOrDefaultAsync(k => k.Key == Rpcs3UpdateStateKey).ConfigureAwait(false);
if (currentState == null)
await wdb.BotState.AddAsync(new() { Key = Rpcs3UpdateStateKey, Value = latestUpdatePr })
.ConfigureAwait(false);
await wdb.BotState.AddAsync(new() { Key = Rpcs3UpdateStateKey, Value = latestUpdatePr }).ConfigureAwait(false);
else
currentState.Value = latestUpdatePr;
var savedLastBuild = await wdb.BotState.FirstOrDefaultAsync(k => k.Key == Rpcs3UpdateBuildKey)
.ConfigureAwait(false);
var savedLastBuild = await wdb.BotState.FirstOrDefaultAsync(k => k.Key == Rpcs3UpdateBuildKey).ConfigureAwait(false);
if (savedLastBuild == null)
await wdb.BotState.AddAsync(new() { Key = Rpcs3UpdateBuildKey, Value = latestUpdateBuild })
.ConfigureAwait(false);
await wdb.BotState.AddAsync(new() { Key = Rpcs3UpdateBuildKey, Value = latestUpdateBuild }).ConfigureAwait(false);
else
savedLastBuild.Value = latestUpdateBuild;
await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
@@ -194,7 +190,7 @@ internal static partial class CompatList
var mergedPrs = await GithubClient.GetClosedPrsAsync(cancellationToken).ConfigureAwait(false); // this will cache 30 latest PRs
var newestPrCommit = await GithubClient.GetPrInfoAsync(newestPr, cancellationToken).ConfigureAwait(false);
var oldestPrCommit = await GithubClient.GetPrInfoAsync(oldestPr, cancellationToken).ConfigureAwait(false);
if (newestPrCommit?.MergedAt == null || oldestPrCommit?.MergedAt == null)
if (newestPrCommit?.MergedAt is null || oldestPrCommit?.MergedAt is null)
return;
mergedPrs = mergedPrs?.Where(pri => pri.MergedAt.HasValue)
@@ -203,10 +199,16 @@ internal static partial class CompatList
.Skip(1)
.TakeWhile(pri => pri.Number != newestPr)
.ToList();
if (mergedPrs is null or {Count: 0})
if (mergedPrs is not {Count: >0})
return;
var failedBuilds = await Config.GetAzureDevOpsClient().GetMasterBuildsAsync(
var failedAzureBuilds = await Config.GetAzureDevOpsClient().GetMasterBuildsAsync(
oldestPrCommit.MergeCommitSha,
newestPrCommit.MergeCommitSha,
oldestPrCommit.MergedAt?.DateTime,
cancellationToken
).ConfigureAwait(false);
var failedGhBuilds = await GithubClient.GetMasterBuildsAsync(
oldestPrCommit.MergeCommitSha,
newestPrCommit.MergeCommitSha,
oldestPrCommit.MergedAt?.DateTime,
@@ -214,31 +216,62 @@ internal static partial class CompatList
).ConfigureAwait(false);
foreach (var mergedPr in mergedPrs)
{
var updateInfo = await Client.GetUpdateAsync(cancellationToken, mergedPr.MergeCommitSha).ConfigureAwait(false)
?? new UpdateInfo {ReturnCode = -1};
if (updateInfo.ReturnCode is 0 or 1) // latest or known build
var updateInfo = await Client.GetUpdateAsync(cancellationToken, mergedPr.MergeCommitSha).ConfigureAwait(false);
if (updateInfo.ReturnCode >= StatusCode.NoUpdates) // latest or known build
{
updateInfo.LatestBuild = updateInfo.CurrentBuild;
updateInfo.CurrentBuild = null;
if (updateInfo is { X64.CurrentBuild: not null, Arm.CurrentBuild: not null })
{
updateInfo.X64.LatestBuild = updateInfo.X64.CurrentBuild;
updateInfo.X64.CurrentBuild = null;
updateInfo.Arm.LatestBuild = updateInfo.Arm.CurrentBuild;
updateInfo.Arm.CurrentBuild = null;
}
else if (updateInfo.X64?.CurrentBuild is not null)
{
updateInfo.X64.LatestBuild = updateInfo.X64.CurrentBuild;
updateInfo.X64.CurrentBuild = null;
updateInfo.Arm = null;
}
else if (updateInfo.Arm?.CurrentBuild is not null)
{
updateInfo.Arm.LatestBuild = updateInfo.Arm.CurrentBuild;
updateInfo.Arm.CurrentBuild = null;
updateInfo.X64 = null;
}
var embed = await updateInfo.AsEmbedAsync(discordClient, true).ConfigureAwait(false);
await compatChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
}
else if (updateInfo.ReturnCode == -1) // unknown build
else if (updateInfo.ReturnCode is StatusCode.UnknownBuild)
{
var masterBuildInfo = failedBuilds?.FirstOrDefault(b => b.Commit?.Equals(mergedPr.MergeCommitSha, StringComparison.InvariantCultureIgnoreCase) is true);
var buildTime = masterBuildInfo?.FinishTime;
if (masterBuildInfo != null)
var masterBuildInfoAzure = failedAzureBuilds?.FirstOrDefault(b => b.Commit?.Equals(mergedPr.MergeCommitSha, StringComparison.OrdinalIgnoreCase) is true);
var masterBuildInfoGh = failedGhBuilds?.FirstOrDefault(b => b.Commit?.Equals(mergedPr.MergeCommitSha, StringComparison.OrdinalIgnoreCase) is true);
var buildTime = masterBuildInfoGh?.FinishTime ?? masterBuildInfoAzure?.FinishTime;
if (masterBuildInfoAzure is not null || masterBuildInfoGh is not null)
{
updateInfo = new()
{
ReturnCode = 1,
LatestBuild = new()
X64 = new()
{
Datetime = buildTime?.ToString("yyyy-MM-dd HH:mm:ss"),
Pr = mergedPr.Number,
Windows = new() {Download = masterBuildInfo.WindowsBuildDownloadLink ?? ""},
Linux = new() { Download = masterBuildInfo.LinuxBuildDownloadLink ?? "" },
Mac = new() { Download = masterBuildInfo.MacBuildDownloadLink ?? "" },
ReturnCode = StatusCode.UpdatesAvailable,
LatestBuild = new()
{
Datetime = buildTime!.Value.ToString("yyyy-MM-dd HH:mm:ss"),
Pr = mergedPr.Number,
Windows = new() { Download = masterBuildInfoAzure?.WindowsBuildDownloadLink ?? masterBuildInfoGh?.WindowsBuildDownloadLink ?? "" },
Linux = new() { Download = masterBuildInfoAzure?.LinuxBuildDownloadLink ?? masterBuildInfoGh?.LinuxBuildDownloadLink ?? "" },
Mac = new() { Download = masterBuildInfoAzure?.MacBuildDownloadLink ?? masterBuildInfoGh?.MacBuildDownloadLink ?? "" },
},
},
Arm = new()
{
ReturnCode = StatusCode.UpdatesAvailable,
LatestBuild = new()
{
Datetime = buildTime!.Value.ToString("yyyy-MM-dd HH:mm:ss"),
Pr = mergedPr.Number,
Linux = new() { Download = masterBuildInfoAzure?.LinuxArmBuildDownloadLink ?? masterBuildInfoGh?.LinuxArmBuildDownloadLink ?? "" },
Mac = new() { Download = masterBuildInfoAzure?.MacArmBuildDownloadLink ?? masterBuildInfoGh?.MacArmBuildDownloadLink ?? "" },
},
},
};
}
@@ -246,20 +279,27 @@ internal static partial class CompatList
{
updateInfo = new()
{
ReturnCode = 1,
LatestBuild = new()
X64 = new()
{
Pr = mergedPr.Number,
Windows = new() {Download = ""},
Linux = new() { Download = "" },
Mac = new() { Download = "" },
ReturnCode = StatusCode.UpdatesAvailable,
LatestBuild = new()
{
Pr = mergedPr.Number,
},
},
};
}
var embed = await updateInfo.AsEmbedAsync(discordClient, true).ConfigureAwait(false);
embed.Color = Config.Colors.PrClosed;
embed.ClearFields();
var reason = masterBuildInfo?.Result switch
var reason = masterBuildInfoGh?.Result switch
{
WorkflowRunConclusion.Success => "Built",
WorkflowRunConclusion.Failure => "Failed to build",
WorkflowRunConclusion.Cancelled => "Cancelled",
WorkflowRunConclusion.TimedOut => "Timed out",
_ => null,
} ?? masterBuildInfoAzure?.Result switch
{
BuildResult.Succeeded => "Built",
BuildResult.PartiallySucceeded => "Built",
@@ -267,7 +307,7 @@ internal static partial class CompatList
BuildResult.Canceled => "Cancelled",
_ => null,
};
if (buildTime.HasValue && reason != null)
if (buildTime.HasValue && reason is not null)
embed.WithFooter($"{reason} on {buildTime:u} ({(DateTime.UtcNow - buildTime.Value).AsTimeDeltaDescription()} ago)");
else
embed.WithFooter(reason ?? "Never built");

View File

@@ -33,9 +33,11 @@ internal static partial class CompatList
static CompatList()
{
using var db = BotDb.OpenRead();
lastUpdateInfo = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateStateKey)?.Value;
lastFullBuildNumber = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateBuildKey)?.Value;
using (var db = BotDb.OpenRead())
{
lastUpdateInfo = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateStateKey)?.Value;
lastFullBuildNumber = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateBuildKey)?.Value;
}
//lastUpdateInfo = "8022";
if (lastUpdateInfo is {Length: >0} strPr && int.TryParse(strPr, out var pr))
{
@@ -43,11 +45,10 @@ internal static partial class CompatList
{
var prInfo = GithubClient.GetPrInfoAsync(pr, Config.Cts.Token).ConfigureAwait(false).GetAwaiter().GetResult();
cachedUpdateInfo = Client.GetUpdateAsync(Config.Cts.Token, prInfo?.MergeCommitSha).ConfigureAwait(false).GetAwaiter().GetResult();
if (cachedUpdateInfo?.CurrentBuild == null)
if ((cachedUpdateInfo.X64 ?? cachedUpdateInfo.Arm)?.CurrentBuild is null)
return;
cachedUpdateInfo.LatestBuild = cachedUpdateInfo.CurrentBuild;
cachedUpdateInfo.CurrentBuild = null;
cachedUpdateInfo.SetCurrentAsLatest();
}
catch { }
}

View File

@@ -135,13 +135,13 @@ internal sealed partial class ContentFilters
if (explanation is { Length: > 0 } &&
!await wdb.Explanation.AnyAsync(e => e.Keyword == explanation).ConfigureAwait(false))
{
await ctx.RespondAsync($"❌ Unknown explanation term: {explanation}", ephemeral: ephemeral)
.ConfigureAwait(false);
await ctx.RespondAsync($"❌ Unknown explanation term: {explanation}", ephemeral: ephemeral).ConfigureAwait(false);
return;
}
var isNewFilter = true;
var filter = await wdb.Piracystring.FirstOrDefaultAsync(ps => ps.String == trigger && ps.Disabled)
var filter = await wdb.Piracystring
.FirstOrDefaultAsync(ps => ps.String == trigger && ps.Disabled)
.ConfigureAwait(false);
if (filter is null)
filter = new() { String = trigger };
@@ -184,8 +184,7 @@ internal sealed partial class ContentFilters
$"{member?.GetMentionWithNickname()} added a new content filter: `{filter.String.Sanitize()}`";
if (!string.IsNullOrEmpty(filter.ValidatingRegex))
reportMsg += $"\nValidation: `{filter.ValidatingRegex}`";
await ctx.Client.ReportAsync("🆕 Content filter created", reportMsg, null, ReportSeverity.Low)
.ConfigureAwait(false);
await ctx.Client.ReportAsync("🆕 Content filter created", reportMsg, null, ReportSeverity.Low).ConfigureAwait(false);
}
ContentFilter.RebuildMatcher();
}
@@ -327,8 +326,7 @@ internal sealed partial class ContentFilters
if (explanation is { Length: > 0 } &&
!await wdb.Explanation.AnyAsync(e => e.Keyword == explanation).ConfigureAwait(false))
{
await ctx.RespondAsync($"❌ Unknown explanation term: {explanation}", ephemeral: ephemeral)
.ConfigureAwait(false);
await ctx.RespondAsync($"❌ Unknown explanation term: {explanation}", ephemeral: ephemeral).ConfigureAwait(false);
return;
}

View File

@@ -249,8 +249,7 @@ internal static partial class Misc
var count = await db.Thumbnail.CountAsync().ConfigureAwait(false);
if (count is 0)
{
await ctx.RespondAsync("Sorry, I have no information about a single game yet", ephemeral: true)
.ConfigureAwait(false);
await ctx.RespondAsync("Sorry, I have no information about a single game yet", ephemeral: true).ConfigureAwait(false);
return;
}
@@ -265,8 +264,7 @@ internal static partial class Misc
return;
}
var result = await ProductCodeLookup.LookupProductCodeAndFormatAsync(ctx.Client, [productCode.ProductCode])
.ConfigureAwait(false);
var result = await ProductCodeLookup.LookupProductCodeAndFormatAsync(ctx.Client, [productCode.ProductCode]).ConfigureAwait(false);
await ctx.RespondAsync(result[0].builder, ephemeral: ephemeral).ConfigureAwait(false);
}
}

View File

@@ -365,21 +365,21 @@ internal sealed class Pr
var mergeTime = prInfo.MergedAt.GetValueOrDefault();
var now = DateTime.UtcNow;
var updateInfo = await CompatApiClient.GetUpdateAsync(Config.Cts.Token, linkOld ? prInfo.MergeCommitSha : null).ConfigureAwait(false);
if (updateInfo is not null)
if (updateInfo.LatestDatetime is DateTime masterBuildTime && masterBuildTime.Ticks >= mergeTime.Ticks)
embed = await updateInfo.AsEmbedAsync(client, false, embed, prInfo, linkOld).ConfigureAwait(false);
else
{
if (DateTime.TryParse(updateInfo.LatestBuild?.Datetime, out var masterBuildTime) && masterBuildTime.Ticks >= mergeTime.Ticks)
embed = await updateInfo.AsEmbedAsync(client, false, embed, prInfo, linkOld).ConfigureAwait(false);
else
{
var waitTime = TimeSpan.FromMinutes(5);
var avgBuildTime = (await GithubClient.GetPipelineDurationAsync(Config.Cts.Token).ConfigureAwait(false)).Mean;
if (now < mergeTime + avgBuildTime)
waitTime = mergeTime + avgBuildTime - now;
embed.AddField("Latest master build", $"""
This pull request has been merged, and will be part of `master` very soon.
Please check again in {waitTime.AsTimeDeltaDescription()}.
""");
}
var waitTime = TimeSpan.FromMinutes(5);
var avgBuildTime = (await GithubClient.GetPipelineDurationAsync(Config.Cts.Token).ConfigureAwait(false)).Mean;
if (now < mergeTime + avgBuildTime)
waitTime = mergeTime + avgBuildTime - now;
embed.AddField(
"Latest master build",
$"""
This pull request has been merged, and will be part of `master` very soon.
Please check again in {waitTime.AsTimeDeltaDescription()}.
"""
);
}
}
return result.AddEmbed(embed);

View File

@@ -112,8 +112,7 @@ public static class LogParsingHandler
$">>>>>>> {message.Id % 100} Parsing log '{source.FileName}' from {message.Author.Username}#{message.Author.Discriminator} ({message.Author.Id}) using {source.GetType().Name} ({source.SourceFileSize} bytes)…");
var analyzingProgressEmbed = GetAnalyzingMsgEmbed(client);
var msgBuilder = new DiscordMessageBuilder()
.AddEmbed(await analyzingProgressEmbed.AddAuthorAsync(client, message, source)
.ConfigureAwait(false))
.AddEmbed(await analyzingProgressEmbed.AddAuthorAsync(client, message, source).ConfigureAwait(false))
.WithReply(message.Id);
botMsg = await channel.SendMessageAsync(msgBuilder).ConfigureAwait(false);
parsedLog = true;
@@ -130,8 +129,7 @@ public static class LogParsingHandler
source,
async () => botMsg = await botMsg.UpdateOrCreateMessageAsync(
channel,
embed: await analyzingProgressEmbed.AddAuthorAsync(client, message, source)
.ConfigureAwait(false)
embed: await analyzingProgressEmbed.AddAuthorAsync(client, message, source).ConfigureAwait(false)
).ConfigureAwait(false),
combinedTokenSource.Token
).ConfigureAwait(false);
@@ -175,15 +173,12 @@ public static class LogParsingHandler
}
var yarr = client.GetEmoji(":piratethink:", "☠");
result.ReadBytes = 0;
if (await message.Author.IsWhitelistedAsync(client, channel.Guild)
.ConfigureAwait(false))
if (await message.Author.IsWhitelistedAsync(client, channel.Guild).ConfigureAwait(false))
{
var piracyWarning = await result.AsEmbedAsync(client, message, source)
.ConfigureAwait(false);
var piracyWarning = await result.AsEmbedAsync(client, message, source).ConfigureAwait(false);
piracyWarning = piracyWarning.WithDescription(
"Please remove the log and issue warning to the original author of the log");
botMsg = await botMsg.UpdateOrCreateMessageAsync(channel, embed: piracyWarning)
.ConfigureAwait(false);
botMsg = await botMsg.UpdateOrCreateMessageAsync(channel, embed: piracyWarning).ConfigureAwait(false);
var matchedOn = ContentFilter.GetMatchedScope(result.SelectedFilter,
result.SelectedFilterContext);
await client.ReportAsync(yarr + " Pirated Release (whitelisted by role)", message,
@@ -269,19 +264,16 @@ public static class LogParsingHandler
if (!force
&& string.IsNullOrEmpty(message.Content)
&& !isSpamChannel
&& !await message.Author.IsSmartlistedAsync(client, message.Channel.Guild)
.ConfigureAwait(false))
&& !await message.Author.IsSmartlistedAsync(client, message.Channel.Guild).ConfigureAwait(false))
{
var threshold = DateTime.UtcNow.AddMinutes(-15);
var previousMessages = await channel.GetMessagesBeforeCachedAsync(message.Id)
.ConfigureAwait(false);
var previousMessages = await channel.GetMessagesBeforeCachedAsync(message.Id).ConfigureAwait(false);
previousMessages = previousMessages.TakeWhile((msg, num) =>
num < 15 || msg.Timestamp.UtcDateTime > threshold).ToList();
if (!previousMessages.Any(m =>
m.Author == message.Author && !string.IsNullOrEmpty(m.Content)))
{
var botSpamChannel = await client.GetChannelAsync(Config.BotSpamId)
.ConfigureAwait(false);
var botSpamChannel = await client.GetChannelAsync(Config.BotSpamId).ConfigureAwait(false);
if (isHelpChannel)
await botMsg.UpdateOrCreateMessageAsync(
channel,

View File

@@ -411,9 +411,10 @@ internal static partial class LogParserResult
var updateInfo = await CheckForUpdateAsync(items).ConfigureAwait(false);
var buildBranch = items["build_branch"]?.ToLowerInvariant();
if (updateInfo != null
if (updateInfo is not null
&& (buildBranch is "master" or "head" or "spu_perf"
|| string.IsNullOrEmpty(buildBranch) && updateInfo.CurrentBuild != null))
|| buildBranch is not {Length: >0}
&& (updateInfo.X64?.CurrentBuild is not null || updateInfo.Arm?.CurrentBuild is not null)))
{
string prefix = "⚠️";
string timeDeltaStr;

View File

@@ -778,10 +778,15 @@ internal static partial class LogParserResult
if (string.IsNullOrEmpty(currentBuildCommit))
currentBuildCommit = null;
var updateInfo = await CompatClient.GetUpdateAsync(Config.Cts.Token, currentBuildCommit).ConfigureAwait(false);
if (updateInfo?.ReturnCode != 1 && currentBuildCommit != null)
if (updateInfo.ReturnCode != StatusCode.UpdatesAvailable && currentBuildCommit is not null)
updateInfo = await CompatClient.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false);
var link = updateInfo?.LatestBuild?.Windows?.Download ?? updateInfo?.LatestBuild?.Linux?.Download ?? updateInfo?.LatestBuild?.Mac?.Download;
if (string.IsNullOrEmpty(link))
var link = updateInfo.X64?.LatestBuild.Windows?.Download
?? updateInfo.X64?.LatestBuild.Linux?.Download
?? updateInfo.X64?.LatestBuild.Mac?.Download
??updateInfo.Arm?.LatestBuild.Windows?.Download
?? updateInfo.Arm?.LatestBuild.Linux?.Download
?? updateInfo.Arm?.LatestBuild.Mac?.Download;
if (updateInfo.ReturnCode is not StatusCode.UpdatesAvailable || link is null)
return null;
var latestBuildInfo = BuildInfoInUpdate().Match(link.ToLowerInvariant());
@@ -791,7 +796,7 @@ internal static partial class LogParserResult
return null;
}
private static bool VersionIsTooOld(NameValueCollection items, Match update, UpdateInfo? updateInfo)
private static bool VersionIsTooOld(NameValueCollection items, Match update, UpdateInfo updateInfo)
{
if (updateInfo.GetUpdateDelta() is TimeSpan updateTimeDelta
&& updateTimeDelta < Config.BuildTimeDifferenceForOutdatedBuildsInDays)

View File

@@ -1,5 +1,6 @@
using System.IO;
using System.Text.RegularExpressions;
using CompatApiClient;
using CompatApiClient.POCOs;
using CompatApiClient.Utils;
using CompatBot.EventHandlers;
@@ -13,18 +14,23 @@ internal static class UpdateInfoFormatter
public static async Task<DiscordEmbedBuilder> AsEmbedAsync(this UpdateInfo? info, DiscordClient client, bool includePrBody = false, DiscordEmbedBuilder? builder = null, Octokit.PullRequest? currentPrInfo = null, bool useCurrent = false)
{
if ((info?.LatestBuild?.Windows?.Download ?? info?.LatestBuild?.Linux?.Download ?? info?.LatestBuild?.Mac?.Download) is null)
return builder ?? new DiscordEmbedBuilder {Title = "Error", Description = "Error communicating with the update API. Try again later.", Color = Config.Colors.Maintenance};
if ( info is not {ReturnCode: >=StatusCode.UnknownBuild})
return builder ?? new DiscordEmbedBuilder
{
Title = "Error",
Description = "Error communicating with the update API. Try again later.",
Color = Config.Colors.Maintenance,
};
var justAppend = builder != null;
var latestBuild = info!.LatestBuild;
var currentBuild = info.CurrentBuild;
var justAppend = builder is not null;
var latestBuild = info.X64?.LatestBuild ?? info.Arm?.LatestBuild;
var currentBuild = info.X64?.CurrentBuild ?? info.Arm?.CurrentBuild;
var latestPr = latestBuild?.Pr;
var currentPr = currentBuild?.Pr;
string? url = null;
Octokit.PullRequest? latestPrInfo = null;
string prDesc = "";
var prDesc = "";
if (!justAppend)
{
if (latestPr > 0)
@@ -46,7 +52,7 @@ internal static class UpdateInfoFormatter
if (includePrBody && latestPrInfo?.Body is { Length: >0 } prInfoBody)
desc = $"**{desc?.TrimEnd()}**\n\n{prInfoBody}";
desc = desc?.Trim();
if (!string.IsNullOrEmpty(desc))
if (desc is {Length: >0})
{
if (GithubLinksHandler.IssueMention().Matches(desc) is { Count: >0 } issueMatches)
{
@@ -63,14 +69,14 @@ internal static class UpdateInfoFormatter
{
num = m.Groups["another_number"].Value;
name = "#" + num;
if (!string.IsNullOrEmpty(m.Groups["comment_id"].Value))
if (m.Groups["comment_id"].Value is {Length: >0})
name += " comment";
}
if (string.IsNullOrEmpty(num))
if (num is not {Length: >0})
continue;
var commentLink = "";
if (!string.IsNullOrEmpty(m.Groups["comment_id"].Value))
if (m.Groups["comment_id"].Value is {Length: >0})
commentLink = "#issuecomment-" + m.Groups["comment_id"].Value;
var newLink = $"[{name}](https://github.com/RPCS3/rpcs3/issues/{num}{commentLink})";
desc = desc.Replace(str, newLink);
@@ -85,7 +91,7 @@ internal static class UpdateInfoFormatter
if (m.Groups["commit_mention"].Value is { Length: >0 } lnk && uniqueLinks.Add(lnk))
{
var num = m.Groups["commit_hash"].Value;
if (string.IsNullOrEmpty(num))
if (num is not {Length: >0})
continue;
if (num.Length > 7)
@@ -95,7 +101,7 @@ internal static class UpdateInfoFormatter
}
}
}
if (!string.IsNullOrEmpty(desc) && GithubLinksHandler.ImageMarkup().Matches(desc) is {Count: >0} imgMatches)
if (desc is {Length: >0} && GithubLinksHandler.ImageMarkup().Matches(desc) is {Count: >0} imgMatches)
{
var uniqueLinks = new HashSet<string>(10);
foreach (Match m in imgMatches)
@@ -104,9 +110,9 @@ internal static class UpdateInfoFormatter
{
var caption = m.Groups["img_caption"].Value;
var link = m.Groups["img_link"].Value;
if (!string.IsNullOrEmpty(caption))
if (caption is {Length: >0})
caption = " " + caption;
desc = desc.Replace(str, $"[🖼{caption}]({link})");
desc = desc.Replace(str, $"[🖼{caption}]({link})");
}
}
}
@@ -115,11 +121,11 @@ internal static class UpdateInfoFormatter
var currentCommit = currentPrInfo?.MergeCommitSha;
var latestCommit = latestPrInfo?.MergeCommitSha;
var buildTimestampKind = "Built";
DateTime? latestBuildTimestamp = null, currentBuildTimestamp = null;
if (Config.GetAzureDevOpsClient() is {} azureClient)
DateTimeOffset? latestBuildTimestamp = null, currentBuildTimestamp = null;
//if (Config.GetAzureDevOpsClient() is {} azureClient)
{
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);
var currentAppveyorBuild = await GithubClient.GetMasterBuildInfoAsync(currentCommit, currentPrInfo?.MergedAt?.DateTime, Config.Cts.Token).ConfigureAwait(false);
var latestAppveyorBuild = await GithubClient.GetMasterBuildInfoAsync(latestCommit, latestPrInfo?.MergedAt?.DateTime, Config.Cts.Token).ConfigureAwait(false);
latestBuildTimestamp = latestAppveyorBuild?.FinishTime;
currentBuildTimestamp = currentAppveyorBuild?.FinishTime;
if (!latestBuildTimestamp.HasValue)
@@ -129,10 +135,11 @@ internal static class UpdateInfoFormatter
}
}
var linkedBuild = useCurrent ? currentBuild : latestBuild;
if (!string.IsNullOrEmpty(linkedBuild?.Datetime))
var linkedX64Build = useCurrent ? info.X64?.CurrentBuild : info.X64?.LatestBuild;
var linkedArmBuild = useCurrent ? info.Arm?.CurrentBuild : info.Arm?.LatestBuild;
if ((linkedX64Build ?? linkedArmBuild)?.Datetime is {Length: >0} dateTime)
{
var timestampInfo = (useCurrent ? currentBuildTimestamp : latestBuildTimestamp)?.ToString("u") ?? linkedBuild.Datetime;
var timestampInfo = (useCurrent ? currentBuildTimestamp : latestBuildTimestamp)?.ToString("u") ?? dateTime;
if (!useCurrent
&& currentPr > 0
&& currentPr != latestPr
@@ -153,14 +160,17 @@ internal static class UpdateInfoFormatter
builder.WithFooter($"{buildTimestampKind} on {timestampInfo}");
}
return builder
.AddField("Windows download", GetLinkMessage(linkedBuild?.Windows, true), true)
.AddField("Linux download", GetLinkMessage(linkedBuild?.Linux, true), true)
.AddField("Mac download", GetLinkMessage(linkedBuild?.Mac, true), true);
.AddField("Windows x64", GetLinkMessage(linkedX64Build?.Windows, true), true)
.AddField("Linux x64", GetLinkMessage(linkedX64Build?.Linux, true), true)
.AddField("Mac Intel", GetLinkMessage(linkedX64Build?.Mac, true), true)
.AddField("Windows ARM64", "-", true)
.AddField("Linux ARM64", GetLinkMessage(linkedArmBuild?.Linux, true), true)
.AddField("Mac Apple Silicon", GetLinkMessage(linkedArmBuild?.Mac, true), true);
}
private static string GetLinkMessage(BuildLink? link, bool simpleName)
{
if (link is null or { Download: null or "" } or { Size: null or 0 })
if (link is not {Download.Length: >0, Size: >0})
return "No link available";
var text = new Uri(link.Download).Segments.Last();
@@ -203,19 +213,16 @@ internal static class UpdateInfoFormatter
#endif
});
public static TimeSpan? GetUpdateDelta(DateTime? latest, DateTime? current)
public static TimeSpan? GetUpdateDelta(DateTimeOffset? latest, DateTimeOffset? current)
{
if (latest.HasValue && current.HasValue)
return latest - current;
return null;
}
public static TimeSpan? GetUpdateDelta(this UpdateInfo? updateInfo)
public static TimeSpan? GetUpdateDelta(this UpdateInfo updateInfo)
{
if (updateInfo?.LatestBuild?.Datetime is string latestDateTimeStr
&& DateTime.TryParse(latestDateTimeStr, out var latestDateTime)
&& updateInfo.CurrentBuild?.Datetime is string dateTimeBuildStr
&& DateTime.TryParse(dateTimeBuildStr, out var dateTimeBuild))
if (updateInfo is { LatestDatetime: DateTime latestDateTime, CurrentDatetime: DateTime dateTimeBuild })
return latestDateTime - dateTimeBuild;
return null;
}