mirror of
https://github.com/RPCS3/discord-bot.git
synced 2025-04-16 19:59:59 +00:00
319 lines
16 KiB
C#
319 lines
16 KiB
C#
using System.Text.Json;
|
|
using CompatApiClient;
|
|
using CompatApiClient.POCOs;
|
|
using CompatBot.Database;
|
|
using CompatBot.EventHandlers;
|
|
using CompatBot.Utils.Extensions;
|
|
using CompatBot.Utils.ResultFormatters;
|
|
using DSharpPlus.Commands.Processors.TextCommands;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.TeamFoundation.Build.WebApi;
|
|
using Octokit;
|
|
|
|
namespace CompatBot.Commands;
|
|
|
|
internal static partial class CompatList
|
|
{
|
|
[Command("latest"), AllowDMUsage]
|
|
public static class LatestBuild
|
|
{
|
|
[Command("build"), AllowDMUsage]
|
|
[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(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")]
|
|
public static ValueTask Clear(TextCommandContext ctx)
|
|
{
|
|
lastUpdateInfo = null;
|
|
lastFullBuildNumber = null;
|
|
return CheckForRpcs3UpdatesAsync(ctx);
|
|
}
|
|
|
|
[Command("set"), RequiresBotModRole]
|
|
[Description("Set the latest build info to the specified PR merge, and post new release announcements after it")]
|
|
public static ValueTask Set(TextCommandContext ctx, string lastUpdatePr)
|
|
{
|
|
lastUpdateInfo = lastUpdatePr;
|
|
lastFullBuildNumber = null;
|
|
return CheckForRpcs3UpdatesAsync(ctx);
|
|
}
|
|
|
|
public static async ValueTask CheckForRpcs3UpdatesAsync(
|
|
CommandContext? ctx = null,
|
|
DiscordClient? discordClient = null,
|
|
bool respond = false,
|
|
string? sinceCommit = null,
|
|
DiscordMessage? emptyBotMsg = null
|
|
)
|
|
{
|
|
if (ctx is null && respond)
|
|
throw new InvalidOperationException($"{nameof(Latest)}: request to respond without ctx");
|
|
|
|
discordClient ??= ctx?.Client;
|
|
if (discordClient is null)
|
|
throw new InvalidOperationException($"{nameof(Latest)}: no discord client provided");
|
|
|
|
var ephemeral = true;
|
|
if (respond)
|
|
{
|
|
ephemeral = !ctx!.Channel.IsSpamChannel();
|
|
if (ctx is SlashCommandContext sctx)
|
|
await sctx.DeferResponseAsync(ephemeral).ConfigureAwait(false);
|
|
}
|
|
|
|
var updateAnnouncementRestore = emptyBotMsg is not null;
|
|
var info = await Client.GetUpdateAsync(Config.Cts.Token, sinceCommit).ConfigureAwait(false);
|
|
if (info.ReturnCode != StatusCode.UpdatesAvailable && sinceCommit is not null)
|
|
info = await Client.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false);
|
|
|
|
if (updateAnnouncementRestore)
|
|
info.SetCurrentAsLatest();
|
|
var embed = await info.AsEmbedAsync(discordClient, !respond).ConfigureAwait(false);
|
|
if (info.ReturnCode < StatusCode.UnknownBuild || embed.Color?.Value == Config.Colors.Maintenance.Value)
|
|
{
|
|
if (updateAnnouncementRestore)
|
|
{
|
|
Config.Log.Debug($"Failed to get update info for commit {sinceCommit}: {JsonSerializer.Serialize(info)}");
|
|
return;
|
|
}
|
|
|
|
embed = await cachedUpdateInfo.AsEmbedAsync(discordClient, !respond).ConfigureAwait(false);
|
|
}
|
|
else if (!updateAnnouncementRestore)
|
|
{
|
|
if (cachedUpdateInfo?.LatestDatetime is DateTime previousBuildTime
|
|
&& info.LatestDatetime is DateTime newBuildTime
|
|
&& newBuildTime > previousBuildTime)
|
|
cachedUpdateInfo = info;
|
|
}
|
|
if (respond)
|
|
{
|
|
if (ctx is SlashCommandContext sctx)
|
|
await sctx.RespondAsync(embed: embed.Build(), ephemeral: ephemeral).ConfigureAwait(false);
|
|
else
|
|
await ctx!.RespondAsync(embed: embed.Build()).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (updateAnnouncementRestore)
|
|
{
|
|
if (embed.Title is "Error")
|
|
return;
|
|
|
|
Config.Log.Debug($"Restoring update announcement for build {sinceCommit}: {embed.Title}\n{embed.Description}");
|
|
await emptyBotMsg!.ModifyAsync(embed: embed.Build()).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var latestUpdatePr = info.LatestPr?.ToString();
|
|
var match = (
|
|
from field in embed.Fields
|
|
let m = UpdateVersionRegex().Match(field.Value)
|
|
where m.Success
|
|
select m
|
|
).FirstOrDefault();
|
|
var latestUpdateBuild = match?.Groups["build"].Value;
|
|
|
|
if (latestUpdatePr is not {Length: >0}
|
|
|| lastUpdateInfo == latestUpdatePr
|
|
|| !await UpdateCheck.WaitAsync(0).ConfigureAwait(false))
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (lastFullBuildNumber is {Length: >0}
|
|
&& latestUpdateBuild is {Length: >0}
|
|
&& int.TryParse(lastFullBuildNumber, out var lastSaveBuild)
|
|
&& int.TryParse(latestUpdateBuild, out var latestBuild)
|
|
&& latestBuild <= lastSaveBuild)
|
|
return;
|
|
|
|
var compatChannel = await discordClient.GetChannelAsync(Config.BotChannelId).ConfigureAwait(false);
|
|
var botMember = await discordClient.GetMemberAsync(compatChannel.Guild, discordClient.CurrentUser).ConfigureAwait(false);
|
|
if (botMember is null)
|
|
return;
|
|
|
|
if (!compatChannel.PermissionsFor(botMember).HasPermission(DiscordPermission.SendMessages))
|
|
{
|
|
NewBuildsMonitor.Reset();
|
|
return;
|
|
}
|
|
|
|
if (embed.Color?.Value == Config.Colors.Maintenance.Value)
|
|
return;
|
|
|
|
await CheckMissedBuildsBetweenAsync(discordClient, compatChannel, lastUpdateInfo, latestUpdatePr, Config.Cts.Token).ConfigureAwait(false);
|
|
|
|
await compatChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
|
|
lastUpdateInfo = latestUpdatePr;
|
|
lastFullBuildNumber = latestUpdateBuild;
|
|
await using (var wdb = await BotDb.OpenWriteAsync().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);
|
|
else
|
|
currentState.Value = latestUpdatePr;
|
|
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);
|
|
else
|
|
savedLastBuild.Value = latestUpdateBuild;
|
|
await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
|
|
}
|
|
NewBuildsMonitor.Reset();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Warn(e, "Failed to check for RPCS3 update info");
|
|
}
|
|
finally
|
|
{
|
|
UpdateCheck.Release();
|
|
}
|
|
}
|
|
|
|
private static async ValueTask CheckMissedBuildsBetweenAsync(DiscordClient discordClient, DiscordChannel compatChannel, string? previousUpdatePr, string? latestUpdatePr, CancellationToken cancellationToken)
|
|
{
|
|
if (!int.TryParse(previousUpdatePr, out var oldestPr)
|
|
|| !int.TryParse(latestUpdatePr, out var newestPr))
|
|
return;
|
|
|
|
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 is null || oldestPrCommit?.MergedAt is null)
|
|
return;
|
|
|
|
mergedPrs = mergedPrs?.Where(pri => pri.MergedAt.HasValue)
|
|
.OrderBy(pri => pri.MergedAt!.Value)
|
|
.SkipWhile(pri => pri.Number != oldestPr)
|
|
.Skip(1)
|
|
.TakeWhile(pri => pri.Number != newestPr)
|
|
.ToList();
|
|
if (mergedPrs is not {Count: >0})
|
|
return;
|
|
|
|
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,
|
|
cancellationToken
|
|
).ConfigureAwait(false);
|
|
foreach (var mergedPr in mergedPrs)
|
|
{
|
|
var updateInfo = await Client.GetUpdateAsync(cancellationToken, mergedPr.MergeCommitSha).ConfigureAwait(false);
|
|
if (updateInfo.ReturnCode >= StatusCode.NoUpdates) // latest or known build
|
|
{
|
|
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 is StatusCode.UnknownBuild)
|
|
{
|
|
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()
|
|
{
|
|
X64 = new()
|
|
{
|
|
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 ?? "" },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
else
|
|
{
|
|
updateInfo = new()
|
|
{
|
|
X64 = new()
|
|
{
|
|
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 = 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",
|
|
BuildResult.Failed => "Failed to build",
|
|
BuildResult.Canceled => "Cancelled",
|
|
_ => 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");
|
|
await compatChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |