From 12143b48897f9ad0de42f5d2461ba1bbb620a0d9 Mon Sep 17 00:00:00 2001 From: 13xforever Date: Sat, 15 Mar 2025 20:55:14 +0500 Subject: [PATCH] fix compatibility commands --- CompatBot/Commands/BotMath.cs | 2 +- CompatBot/Commands/BotStatus.cs | 2 +- CompatBot/Commands/CompatList.Top.cs | 103 +++++++++ CompatBot/Commands/CompatList.cs | 207 ++++++------------ .../EventHandlers/DiscordInviteFilter.cs | 4 +- .../EventHandlers/GlobalButtonHandler.cs | 2 +- CompatBot/EventHandlers/ProductCodeLookup.cs | 47 ++-- .../Extensions/AutosplitResponseHelper.cs | 33 ++- ExternalAnnotations/DSharpPlus.Commands.xml | 5 + 9 files changed, 235 insertions(+), 170 deletions(-) create mode 100644 CompatBot/Commands/CompatList.Top.cs create mode 100644 ExternalAnnotations/DSharpPlus.Commands.xml diff --git a/CompatBot/Commands/BotMath.cs b/CompatBot/Commands/BotMath.cs index afd8cea4..d6d07fa8 100644 --- a/CompatBot/Commands/BotMath.cs +++ b/CompatBot/Commands/BotMath.cs @@ -13,7 +13,7 @@ internal sealed class BotMath [Command("calculate"), DefaultGroupCommand] [Description("Math; there you go, Juhn")] - public async ValueTask Calc(SlashCommandContext ctx, [RemainingText, Description("Math expression or `help` for syntax link")] string expression) + public async ValueTask Calc(SlashCommandContext ctx, [Description("Math expression or `help` for syntax link")] string expression) { var ephemeral = !ctx.Channel.IsSpamChannel(); if (expression.Equals("help", StringComparison.OrdinalIgnoreCase)) diff --git a/CompatBot/Commands/BotStatus.cs b/CompatBot/Commands/BotStatus.cs index 04cf5509..42683cef 100644 --- a/CompatBot/Commands/BotStatus.cs +++ b/CompatBot/Commands/BotStatus.cs @@ -14,7 +14,7 @@ internal sealed class BotStatus { [Command("status")] [Description("Bot subsystem configuration status and various runtime stats")] - public async Task Show(SlashCommandContext ctx) + public async ValueTask Show(SlashCommandContext ctx) { var latency = ctx.Client.GetConnectionLatency(Config.BotGuildId); var embed = new DiscordEmbedBuilder diff --git a/CompatBot/Commands/CompatList.Top.cs b/CompatBot/Commands/CompatList.Top.cs new file mode 100644 index 00000000..35e6c489 --- /dev/null +++ b/CompatBot/Commands/CompatList.Top.cs @@ -0,0 +1,103 @@ +using CompatApiClient.Utils; +using CompatBot.Database; +using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; +using Microsoft.EntityFrameworkCore; + +namespace CompatBot.Commands; + +internal sealed partial class CompatList +{ + public sealed class Top + { + [Command("top")] + [Description("Top game lists based on Metacritic scores and compatibility status")] + public async ValueTask Show(SlashCommandContext ctx, + [Description("Number of entries in the list")] int number = 10, + [Description("Filter by compatibility status"), SlashChoiceProvider] string status = "playable", + [Description("Listing type"), SlashChoiceProvider] string type = "both") + { + var ephemeral = !ctx.Channel.IsSpamChannel() && !ctx.Channel.IsOfftopicChannel(); + await ctx.DeferResponseAsync(ephemeral).ConfigureAwait(false); + + status = status.ToLowerInvariant(); + type = type.ToLowerInvariant(); + number = number.Clamp(1, 100); + var exactStatus = status.EndsWith("only"); + if (exactStatus) + status = status[..^4]; + if (!Enum.TryParse(status, true, out CompatStatus s)) + s = CompatStatus.Playable; + + await using var db = new ThumbnailDb(); + var queryBase = db.Thumbnail.AsNoTracking(); + if (exactStatus) + queryBase = queryBase.Where(g => g.CompatibilityStatus == s); + else + queryBase = queryBase.Where(g => g.CompatibilityStatus >= s); + queryBase = queryBase.Where(g => g.Metacritic != null).Include(t => t.Metacritic); + var query = type switch + { + "critic" => queryBase.Where(t => t.Metacritic!.CriticScore > 0).AsEnumerable().Select(t => + (title: t.Metacritic!.Title, score: t.Metacritic!.CriticScore!.Value, + second: t.Metacritic.UserScore ?? t.Metacritic.CriticScore.Value)), + "user" => queryBase.Where(t => t.Metacritic!.UserScore > 0).AsEnumerable().Select(t => + (title: t.Metacritic!.Title, score: t.Metacritic!.UserScore!.Value, + second: t.Metacritic.CriticScore ?? t.Metacritic.UserScore.Value)), + _ => queryBase.AsEnumerable().Select(t => (title: t.Metacritic!.Title, + score: Math.Max(t.Metacritic.CriticScore ?? 0, t.Metacritic.UserScore ?? 0), second: (byte)0)), + }; + var resultList = query.Where(i => i.score > 0) + .OrderByDescending(i => i.score) + .ThenByDescending(i => i.second) + .Distinct() + .Take(number) + .ToList(); + if (resultList.Count > 0) + { + var result = new StringBuilder($"Best {s.ToString().ToLower()}"); + if (exactStatus) + result.Append(" only"); + result.Append(" games"); + if (type is "critic" or "user") + result.Append($" according to {type}s"); + result.AppendLine(":"); + foreach (var (title, score, _) in resultList) + result.AppendLine($"`{score:00}` {title}"); + var formattedResults = AutosplitResponseHelper.AutosplitMessage(result.ToString(), blockStart: null, blockEnd: null); + await ctx.RespondAsync(formattedResults[0], ephemeral).ConfigureAwait(false); + } + else + await ctx.RespondAsync("Failed to generate list", ephemeral).ConfigureAwait(false); + } + + public class CompatListStatusChoiceProvider : IChoiceProvider + { + private static readonly IReadOnlyList compatListStatus = + [ + new("playable", "playable"), + new("ingame or better", "ingame"), + new("intro or better", "intro"), + new("loadable or better", "loadable"), + new("only ingame", "ingameOnly"), + new("only intro", "introOnly"), + new("only loadable", "loadableOnly"), + ]; + + public ValueTask> ProvideAsync(CommandParameter parameter) + => ValueTask.FromResult>(compatListStatus); + } + + public class ScoreTypeChoiceProvider : IChoiceProvider + { + private static readonly IReadOnlyList scoreType = + [ + new("combined", "both"), + new("critic score", "critic"), + new("user score", "user"), + ]; + + public ValueTask> ProvideAsync(CommandParameter parameter) + => ValueTask.FromResult>(scoreType); + } + } +} \ No newline at end of file diff --git a/CompatBot/Commands/CompatList.cs b/CompatBot/Commands/CompatList.cs index 98375185..cc052601 100644 --- a/CompatBot/Commands/CompatList.cs +++ b/CompatBot/Commands/CompatList.cs @@ -39,140 +39,70 @@ internal sealed partial class CompatList lastUpdateInfo = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateStateKey)?.Value; lastFullBuildNumber = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateBuildKey)?.Value; //lastUpdateInfo = "8022"; - if (lastUpdateInfo is string strPr - && int.TryParse(strPr, out var pr)) + if (lastUpdateInfo is {Length: >0} strPr && int.TryParse(strPr, out var pr)) { try { 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) - { - cachedUpdateInfo.LatestBuild = cachedUpdateInfo.CurrentBuild; - cachedUpdateInfo.CurrentBuild = null; - } + if (cachedUpdateInfo?.CurrentBuild == null) + return; + + cachedUpdateInfo.LatestBuild = cachedUpdateInfo.CurrentBuild; + cachedUpdateInfo.CurrentBuild = null; } catch { } } } - /* - [Command("compatibility"), TextAlias("c", "compat")] - [Description("Searches the compatibility database, USE: !compat search term")] - public async Task Compat(CommandContext ctx, [RemainingText, Description("Game title to look up")] string? title) + [Command("compatibility")] + [Description("Search the game compatibility list")] + public async ValueTask Compat(SlashCommandContext ctx, [Description("Game title or product code to look up")] string title) { - title = title?.TrimEager().Truncate(40); - if (string.IsNullOrEmpty(title)) + if (await ContentFilter.FindTriggerAsync(FilterContext.Chat, title).ConfigureAwait(false) is not null) { - var prompt = await ctx.Channel.SendMessageAsync($"{ctx.Message.Author.Mention} what game do you want to check?").ConfigureAwait(false); - var interact = ctx.Client.GetInteractivity(); - var response = await interact.WaitForMessageAsync(m => m.Author == ctx.Message.Author && m.Channel == ctx.Channel).ConfigureAwait(false); - if (string.IsNullOrEmpty(response.Result?.Content) || response.Result.Content.StartsWith(Config.CommandPrefix)) - { - await prompt.ModifyAsync("You should specify what you're looking for").ConfigureAwait(false); - return; - } - - DeletedMessagesMonitor.RemovedByBotCache.Set(prompt.Id, true, DeletedMessagesMonitor.CacheRetainTime); - await prompt.DeleteAsync().ConfigureAwait(false); - title = response.Result.Content.TrimEager().Truncate(40); + await ctx.RespondAsync("Invalid game title or product code.", true).ConfigureAwait(false); + return; } + var ephemeral = !ctx.Channel.IsSpamChannel(); + await ctx.DeferResponseAsync(ephemeral).ConfigureAwait(false); - if (!await DiscordInviteFilter.CheckMessageInvitesAreSafeAsync(ctx.Client, ctx.Message).ConfigureAwait(false)) - return; - - if (!await ContentFilter.IsClean(ctx.Client, ctx.Message).ConfigureAwait(false)) - return; - - var productCodes = ProductCodeLookup.GetProductIds(ctx.Message.Content); - if (productCodes.Any()) + var productCodes = ProductCodeLookup.GetProductIds(title); + if (productCodes.Count > 0) { - await ProductCodeLookup.LookupAndPostProductCodeEmbedAsync(ctx.Client, ctx.Message, ctx.Channel, productCodes).ConfigureAwait(false); + var formattedResults = await ProductCodeLookup.LookupProductCodeAndFormatAsync(ctx.Client, productCodes).ConfigureAwait(false); + await ctx.RespondAsync(embed: formattedResults[0].builder, ephemeral: ephemeral).ConfigureAwait(false); return; } try { + title = title.TrimEager().Truncate(40); var requestBuilder = RequestBuilder.Start().SetSearch(title); - await DoRequestAndRespond(ctx, requestBuilder).ConfigureAwait(false); + await DoRequestAndRespondAsync(ctx, ephemeral, requestBuilder).ConfigureAwait(false); } catch (Exception e) { Config.Log.Error(e, "Failed to get compat list info"); } } - */ - [Command("top"), LimitedToOfftopicChannel] - //[Cooldown(1, 5, CooldownBucketType.Channel)] - [Description("Provides top game lists based on Metacritic and compatibility lists")] - public async Task Top(CommandContext ctx, - [Description("Number of entries in the list")] int number = 10, - [Description("One of `playable`, `ingame`, `intro`, `loadable`, or `Only`")] string status = "playable", - [Description("One of `both`, `critic`, or `user`")] string scoreType = "both") - { - status = status.ToLowerInvariant(); - scoreType = scoreType.ToLowerInvariant(); - - number = number.Clamp(1, 100); - var exactStatus = status.EndsWith("only"); - if (exactStatus) - status = status[..^4]; - if (!Enum.TryParse(status, true, out CompatStatus s)) - s = CompatStatus.Playable; - - await using var db = new ThumbnailDb(); - var queryBase = db.Thumbnail.AsNoTracking(); - if (exactStatus) - queryBase = queryBase.Where(g => g.CompatibilityStatus == s); - else - queryBase = queryBase.Where(g => g.CompatibilityStatus >= s); - queryBase = queryBase.Where(g => g.Metacritic != null).Include(t => t.Metacritic); - var query = scoreType switch - { - "critic" => queryBase.Where(t => t.Metacritic!.CriticScore > 0).AsEnumerable().Select(t => (title: t.Metacritic!.Title, score: t.Metacritic!.CriticScore!.Value, second: t.Metacritic.UserScore ?? t.Metacritic.CriticScore.Value)), - "user" => queryBase.Where(t => t.Metacritic!.UserScore > 0).AsEnumerable().Select(t => (title: t.Metacritic!.Title, score: t.Metacritic!.UserScore!.Value, second: t.Metacritic.CriticScore ?? t.Metacritic.UserScore.Value)), - _ => queryBase.AsEnumerable().Select(t => (title: t.Metacritic!.Title, score: Math.Max(t.Metacritic.CriticScore ?? 0, t.Metacritic.UserScore ?? 0), second: (byte)0)), - }; - var resultList = query.Where(i => i.score > 0) - .OrderByDescending(i => i.score) - .ThenByDescending(i => i.second) - .Distinct() - .Take(number) - .ToList(); - if (resultList.Count > 0) - { - var result = new StringBuilder($"Best {s.ToString().ToLower()}"); - if (exactStatus) - result.Append(" only"); - result.Append(" games"); - if (scoreType is "critic" or "user") - result.Append($" according to {scoreType}s"); - result.AppendLine(":"); - foreach (var (title, score, _) in resultList) - result.AppendLine($"`{score:00}` {title}"); - await ctx.SendAutosplitMessageAsync(result, blockStart: null, blockEnd: null).ConfigureAwait(false); - } - else - await ctx.Channel.SendMessageAsync("Failed to generate list").ConfigureAwait(false); - } - - [Command("latest"), TriggersTyping] - [Description("Provides links to the latest RPCS3 build")] - //[Cooldown(1, 30, CooldownBucketType.Channel)] + //[Command("latest")] + [Description("Links to the latest RPCS3 build")] public sealed class UpdatesCheck { + /* [Command("build"), DefaultGroupCommand] - public Task Latest(CommandContext ctx) => CheckForRpcs3Updates(ctx.Client, ctx.Channel); + public Task Latest(SlashCommandContext ctx) => CheckForRpcs3Updates(ctx.Client, ctx.Channel); [Command("since")] [Description("Show additional info about changes since specified update")] - public Task Since(CommandContext ctx, [Description("Commit hash of the update, such as `46abe0f31`")] string commit) + public Task Since(SlashCommandContext ctx, [Description("Commit hash of the update, such as `46abe0f31`")] string commit) => CheckForRpcs3Updates(ctx.Client, ctx.Channel, commit); [Command("clear"), RequiresBotModRole] [Description("Clears update info cache that is used to post new build announcements")] - public Task Clear(CommandContext ctx) + public Task Clear(SlashCommandContext ctx) { lastUpdateInfo = null; lastFullBuildNumber = null; @@ -181,14 +111,15 @@ internal sealed partial class CompatList [Command("set"), RequiresBotModRole] [Description("Sets update info cache that is used to check if new updates are available")] - public Task Set(CommandContext ctx, string lastUpdatePr) + public Task Set(SlashCommandContext ctx, string lastUpdatePr) { lastUpdateInfo = lastUpdatePr; lastFullBuildNumber = null; return CheckForRpcs3Updates(ctx.Client, null); } + */ - public static async Task CheckForRpcs3Updates(DiscordClient discordClient, DiscordChannel? channel, string? sinceCommit = null, DiscordMessage? emptyBotMsg = null) + public static async ValueTask CheckForRpcs3Updates(DiscordClient discordClient, DiscordChannel? channel, string? sinceCommit = null, DiscordMessage? emptyBotMsg = null) { var updateAnnouncement = channel is null; var updateAnnouncementRestore = emptyBotMsg != null; @@ -271,7 +202,7 @@ internal sealed partial class CompatList if (embed.Color.Value.Value == Config.Colors.Maintenance.Value) return false; - await CheckMissedBuildsBetween(discordClient, compatChannel, lastUpdateInfo, latestUpdatePr, Config.Cts.Token).ConfigureAwait(false); + await CheckMissedBuildsBetweenAsync(discordClient, compatChannel, lastUpdateInfo, latestUpdatePr, Config.Cts.Token).ConfigureAwait(false); await compatChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false); lastUpdateInfo = latestUpdatePr; @@ -302,7 +233,7 @@ internal sealed partial class CompatList return false; } - private static async Task CheckMissedBuildsBetween(DiscordClient discordClient, DiscordChannel compatChannel, string? previousUpdatePr, string? latestUpdatePr, CancellationToken cancellationToken) + 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)) @@ -394,8 +325,7 @@ internal sealed partial class CompatList } } - /* - private static async Task DoRequestAndRespond(CommandContext ctx, RequestBuilder requestBuilder) + private static async ValueTask DoRequestAndRespondAsync(SlashCommandContext ctx, bool ephemeral, RequestBuilder requestBuilder) { Config.Log.Info(requestBuilder.Build()); CompatResult? result = null; @@ -411,7 +341,7 @@ internal sealed partial class CompatList { if (result == null) { - await ctx.Channel.SendMessageAsync(embed: TitleInfo.CommunicationError.AsEmbed(null)).ConfigureAwait(false); + await ctx.RespondAsync(embed: TitleInfo.CommunicationError.AsEmbed(null), ephemeral).ConfigureAwait(false); return; } } @@ -419,14 +349,20 @@ internal sealed partial class CompatList #if DEBUG await Task.Delay(5_000).ConfigureAwait(false); #endif - var channel = await ctx.GetChannelForSpamAsync().ConfigureAwait(false); if (result?.Results?.Count == 1) - await ProductCodeLookup.LookupAndPostProductCodeEmbedAsync(ctx.Client, ctx.Message, ctx.Channel, [..result.Results.Keys]).ConfigureAwait(false); + { + var formattedResults = await ProductCodeLookup.LookupProductCodeAndFormatAsync(ctx.Client, [..result.Results.Keys]).ConfigureAwait(false); + await ctx.RespondAsync(embed: formattedResults[0].builder, ephemeral: ephemeral).ConfigureAwait(false); + } else if (result != null) + { + var builder = new StringBuilder(); foreach (var msg in FormatSearchResults(ctx, result)) - await channel.SendAutosplitMessageAsync(msg, blockStart: "", blockEnd: "").ConfigureAwait(false); + builder.AppendLine(msg); + var formattedResults = AutosplitResponseHelper.AutosplitMessage(builder.ToString(), blockStart: null, blockEnd: null); + await ctx.RespondAsync(formattedResults[0], ephemeral).ConfigureAwait(false); + } } - */ internal static CompatResult GetLocalCompatResult(RequestBuilder requestBuilder) { @@ -457,62 +393,51 @@ internal sealed partial class CompatList return result; } - /* - private static IEnumerable FormatSearchResults(CommandContext ctx, CompatResult compatResult) + private static IEnumerable FormatSearchResults(SlashCommandContext ctx, CompatResult compatResult) { var returnCode = ApiConfig.ReturnCodes[compatResult.ReturnCode]; var request = compatResult.RequestBuilder; if (returnCode.overrideAll) - yield return string.Format(returnCode.info, ctx.Message.Author.Mention); + yield return string.Format(returnCode.info, ctx.User.Mention); else { - var authorMention = ctx.Channel.IsPrivate ? "You" : ctx.Message.Author.Mention; + var authorMention = ctx.Channel.IsPrivate ? "You" : ctx.User.Mention; var result = new StringBuilder(); result.AppendLine($"{authorMention} searched for: ***{request.Search?.Sanitize(replaceBackTicks: true)}***"); if (request.Search?.Contains("persona", StringComparison.InvariantCultureIgnoreCase) is true || request.Search?.Contains("p5", StringComparison.InvariantCultureIgnoreCase) is true) result.AppendLine("Did you try searching for **__Unnamed__** instead?"); - else if (ctx.IsOnionLike() - && compatResult.Results.Values.Any(i => - i.Title.Contains("afrika", StringComparison.InvariantCultureIgnoreCase) - || i.Title.Contains("africa", StringComparison.InvariantCultureIgnoreCase)) - ) - { - var sqvat = ctx.Client.GetEmoji(":sqvat:", Config.Reactions.No); - result.AppendLine($"One day this meme will die {sqvat}"); - } result.AppendFormat(returnCode.info, compatResult.SearchTerm); yield return result.ToString(); result.Clear(); - if (returnCode.displayResults) - { - var sortedList = compatResult.GetSortedList(); - var trimmedList = sortedList.Where(i => i.score > 0).ToList(); - if (trimmedList.Count > 0) - sortedList = trimmedList; + if (!returnCode.displayResults) + yield break; + + var sortedList = compatResult.GetSortedList(); + var trimmedList = sortedList.Where(i => i.score > 0).ToList(); + if (trimmedList.Count > 0) + sortedList = trimmedList; - var searchTerm = request.Search ?? @"¯\\\_(ツ)\_/¯"; - var searchHits = sortedList.Where(t => t.score > 0.5 - || (t.info.Title?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false) - || (t.info.AlternativeTitle?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false)); - foreach (var title in searchHits.Select(t => t.info.Title).Distinct()) - StatsStorage.IncGameStat(title); - foreach (var resultInfo in sortedList.Take(request.AmountRequested)) - { - var info = resultInfo.AsString(); + var searchTerm = request.Search ?? @"¯\\\_(ツ)\_/¯"; + var searchHits = sortedList.Where(t => t.score > 0.5 + || (t.info.Title?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false) + || (t.info.AlternativeTitle?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false)); + foreach (var title in searchHits.Select(t => t.info.Title).Distinct()) + StatsStorage.IncGameStat(title); + foreach (var resultInfo in sortedList.Take(request.AmountRequested)) + { + var info = resultInfo.AsString(); #if DEBUG - info = $"{StringUtils.InvisibleSpacer}`{CompatApiResultUtils.GetScore(request.Search, resultInfo.info):0.000000}` {info}"; + info = $"{StringUtils.InvisibleSpacer}`{CompatApiResultUtils.GetScore(request.Search, resultInfo.info):0.000000}` {info}"; #endif - result.AppendLine(info); - } - yield return result.ToString(); + result.AppendLine(info); } + yield return result.ToString(); } } - */ public static string FixGameTitleSearch(string title) { @@ -570,7 +495,7 @@ internal sealed partial class CompatList await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } - public static async Task ImportMetacriticScoresAsync() + public static async ValueTask ImportMetacriticScoresAsync() { var scoreJson = "metacritic_ps3.json"; string json; diff --git a/CompatBot/EventHandlers/DiscordInviteFilter.cs b/CompatBot/EventHandlers/DiscordInviteFilter.cs index a4cdf962..36d795ac 100644 --- a/CompatBot/EventHandlers/DiscordInviteFilter.cs +++ b/CompatBot/EventHandlers/DiscordInviteFilter.cs @@ -33,8 +33,8 @@ internal static partial class DiscordInviteFilter return true; #if !DEBUG - if (await message.Author.IsWhitelistedAsync(client, message.Channel.Guild).ConfigureAwait(false)) - return true; + if (await message.Author.IsWhitelistedAsync(client, message.Channel.Guild).ConfigureAwait(false)) + return true; #endif if (message.Reactions.Any(r => r.Emoji == Config.Reactions.Moderated && r.IsMe)) diff --git a/CompatBot/EventHandlers/GlobalButtonHandler.cs b/CompatBot/EventHandlers/GlobalButtonHandler.cs index 1b09e3b6..4eff0fab 100644 --- a/CompatBot/EventHandlers/GlobalButtonHandler.cs +++ b/CompatBot/EventHandlers/GlobalButtonHandler.cs @@ -4,7 +4,7 @@ namespace CompatBot.EventHandlers; internal static class GlobalButtonHandler { - private const string ReplaceWithUpdatesPrefix = "replace with game updates:"; + internal const string ReplaceWithUpdatesPrefix = "replace with game updates:"; public static async Task OnComponentInteraction(DiscordClient sender, ComponentInteractionCreatedEventArgs e) { diff --git a/CompatBot/EventHandlers/ProductCodeLookup.cs b/CompatBot/EventHandlers/ProductCodeLookup.cs index 3c1d4b00..f31a6f79 100644 --- a/CompatBot/EventHandlers/ProductCodeLookup.cs +++ b/CompatBot/EventHandlers/ProductCodeLookup.cs @@ -46,35 +46,21 @@ internal static partial class ProductCodeLookup await LookupAndPostProductCodeEmbedAsync(c, args.Message, args.Channel, codesToLookup).ConfigureAwait(false); } - - public static async Task LookupAndPostProductCodeEmbedAsync(DiscordClient client, DiscordMessage message, DiscordChannel channel, List codesToLookup) + + public static async ValueTask LookupAndPostProductCodeEmbedAsync(DiscordClient client, DiscordMessage message, DiscordChannel channel, List codesToLookup) { await message.ReactWithAsync(Config.Reactions.PleaseWait).ConfigureAwait(false); try { - var results = new List<(string code, Task task)>(codesToLookup.Count); - foreach (var code in codesToLookup) - results.Add((code, client.LookupGameInfoAsync(code))); - var formattedResults = new List<(string code, DiscordEmbedBuilder builder)>(results.Count); - foreach (var (code, task) in results) - try - { - formattedResults.Add((code, await task.ConfigureAwait(false))); - } - catch (Exception e) - { - Config.Log.Warn(e, $"Couldn't get product code info for {code}"); - } - - // get only results with unique titles - formattedResults = formattedResults.DistinctBy(e => e.builder.Title).ToList(); var lookupEmoji = new DiscordComponentEmoji(DiscordEmoji.FromUnicode("🔍")); + var formattedResults = await LookupProductCodeAndFormatAsync(client, codesToLookup).ConfigureAwait(false); foreach (var result in formattedResults) try { var messageBuilder = new DiscordMessageBuilder().AddEmbed(result.builder); - if (channel.IsSpamChannel()) - messageBuilder.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Secondary, $"replace with game updates:{message.Author.Id}:{message.Id}:{result.code}", "Check for updates", emoji: lookupEmoji)); + //todo: pass author from context and update OnCheckUpdatesButtonClick in psn check updates + if (message is {Author: not null} && channel.IsSpamChannel()) + messageBuilder.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Secondary, $"{GlobalButtonHandler.ReplaceWithUpdatesPrefix}{message.Author.Id}:{message.Id}:{result.code}", "Check for updates", emoji: lookupEmoji)); await DiscordMessageExtensions.UpdateOrCreateMessageAsync(null, channel, messageBuilder).ConfigureAwait(false); } catch (Exception e) @@ -88,10 +74,27 @@ internal static partial class ProductCodeLookup } } + internal static async ValueTask> LookupProductCodeAndFormatAsync(DiscordClient client, List codesToLookup) + { + var results = codesToLookup.Select(code => (code, client.LookupGameInfoAsync(code))).ToList(); + var formattedResults = new List<(string code, DiscordEmbedBuilder builder)>(results.Count); + foreach (var (code, task) in results) + try + { + formattedResults.Add((code, await task.ConfigureAwait(false))); + } + catch (Exception e) + { + Config.Log.Warn(e, $"Couldn't get product code info for {code}"); + } + // get only results with unique titles + return formattedResults.DistinctBy(e => e.builder.Title).ToList(); + } + public static List GetProductIds(string? input) { if (string.IsNullOrEmpty(input)) - return new(0); + return []; return Pattern().Matches(input) .Select(match => (match.Groups["letters"].Value + match.Groups["numbers"]).ToUpper()) @@ -102,7 +105,7 @@ internal static partial class ProductCodeLookup public static async Task LookupGameInfoAsync(this DiscordClient client, string? code, string? gameTitle = null, bool forLog = false, string? category = null) => (await LookupGameInfoWithEmbedAsync(client, code, gameTitle, forLog, category).ConfigureAwait(false)).embedBuilder; - public static async Task<(DiscordEmbedBuilder embedBuilder, CompatResult? compatResult)> LookupGameInfoWithEmbedAsync(this DiscordClient client, string? code, string? gameTitle = null, bool forLog = false, string? category = null) + public static async ValueTask<(DiscordEmbedBuilder embedBuilder, CompatResult? compatResult)> LookupGameInfoWithEmbedAsync(this DiscordClient client, string? code, string? gameTitle = null, bool forLog = false, string? category = null) { if (string.IsNullOrEmpty(code)) return (TitleInfo.Unknown.AsEmbed(code, gameTitle, forLog), null); diff --git a/CompatBot/Utils/Extensions/AutosplitResponseHelper.cs b/CompatBot/Utils/Extensions/AutosplitResponseHelper.cs index 4df0b9c5..56a47926 100644 --- a/CompatBot/Utils/Extensions/AutosplitResponseHelper.cs +++ b/CompatBot/Utils/Extensions/AutosplitResponseHelper.cs @@ -7,13 +7,13 @@ public static class AutosplitResponseHelper public static Task SendAutosplitMessageAsync(this CommandContext ctx, StringBuilder message, int blockSize = EmbedPager.MaxMessageLength, string? blockEnd = "\n```", string? blockStart = "```\n") => ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart); - public static Task SendAutosplitMessageAsync(this CommandContext ctx, string message, int blockSize = EmbedPager.MaxMessageLength, string? blockEnd = "\n```", string? blockStart = "```\n") + public static ValueTask SendAutosplitMessageAsync(this CommandContext ctx, string message, int blockSize = EmbedPager.MaxMessageLength, string? blockEnd = "\n```", string? blockStart = "```\n") => ctx.Channel.SendAutosplitMessageAsync(message, blockSize, blockEnd, blockStart); public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, StringBuilder message, int blockSize = EmbedPager.MaxMessageLength, string? blockEnd = "\n```", string? blockStart = "```\n") => await SendAutosplitMessageAsync(channel, message.ToString(), blockSize, blockEnd, blockStart).ConfigureAwait(false); - public static async Task SendAutosplitMessageAsync(this DiscordChannel channel, string message, int blockSize = EmbedPager.MaxMessageLength, string? blockEnd = "\n```", string? blockStart = "```\n") + public static async ValueTask SendAutosplitMessageAsync(this DiscordChannel channel, string message, int blockSize = EmbedPager.MaxMessageLength, string? blockEnd = "\n```", string? blockStart = "```\n") { if (string.IsNullOrEmpty(message)) return; @@ -55,4 +55,33 @@ public static class AutosplitResponseHelper Config.Log.Warn(e, "And yet the message length was " + remainingContent.Length); } } + + public static List AutosplitMessage(string message, int blockSize = EmbedPager.MaxMessageLength, string? blockEnd = "\n```", string? blockStart = "```\n") + { + var result = new List(); + if (string.IsNullOrEmpty(message)) + return []; + + blockEnd ??= ""; + blockStart ??= ""; + var maxContentSize = blockSize - blockEnd.Length - blockStart.Length; + var buffer = new StringBuilder(); + foreach (var line in message.Split(Environment.NewLine).Select(l => l.Trim(maxContentSize))) + { + if (buffer.Length + line.Length + blockEnd.Length > blockSize) + { + var content = buffer.ToString().Trim(blockSize - blockEnd.Length) + blockEnd; + if (content.Length > blockSize) + Config.Log.Error($"Somehow managed to go over {blockSize} characters in a message"); + result.Add(content); + buffer.Clear().Append(blockStart); + } + else + buffer.Append('\n'); + buffer.Append(line); + } + var remainingContent = buffer.ToString().Trim(blockSize); + result.Add(remainingContent); + return result; + } } \ No newline at end of file diff --git a/ExternalAnnotations/DSharpPlus.Commands.xml b/ExternalAnnotations/DSharpPlus.Commands.xml new file mode 100644 index 00000000..6b9eb572 --- /dev/null +++ b/ExternalAnnotations/DSharpPlus.Commands.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file