From a25adefc6590c99aa92004071972cfb93b4c7a97 Mon Sep 17 00:00:00 2001 From: Kwoth Date: Wed, 1 May 2024 06:26:00 +0000 Subject: [PATCH] Started rewriting all paginted responses into the new system --- .../Modules/Expressions/NadekoExpressions.cs | 95 ++++++----- .../Blacklist/BlacklistCommands.cs | 43 +++-- .../Modules/Searches/Feeds/FeedCommands.cs | 2 +- .../StreamNotificationCommands.cs | 46 +++-- .../Modules/Utility/Info/InviteCommands.cs | 51 +++--- src/NadekoBot/Modules/Xp/Club/Club.cs | 161 ++++++++++-------- .../Sender/MessageChannelExtensions.cs | 2 +- .../ResponseBuilder.PaginationSender.cs | 155 +++++++++++++++++ .../_common/Sender/ResponseBuilder.cs | 140 +++++++++++++-- .../_common/Sender/ResponseMessageModel.cs | 10 ++ 10 files changed, 505 insertions(+), 200 deletions(-) create mode 100644 src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs create mode 100644 src/NadekoBot/_common/Sender/ResponseMessageModel.cs diff --git a/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs b/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs index 9a5f9ee1f..80d6f1df3 100644 --- a/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs +++ b/src/NadekoBot/Modules/Expressions/NadekoExpressions.cs @@ -32,13 +32,15 @@ public partial class NadekoExpressions : NadekoModule var ex = await _service.AddAsync(ctx.Guild?.Id, key, message); - await Response().Embed(new EmbedBuilder() - .WithOkColor() - .WithTitle(GetText(strs.expr_new)) - .WithDescription($"#{new kwum(ex.Id)}") - .AddField(GetText(strs.trigger), key) - .AddField(GetText(strs.response), - message.Length > 1024 ? GetText(strs.redacted_too_long) : message)).SendAsync(); + await Response() + .Embed(new EmbedBuilder() + .WithOkColor() + .WithTitle(GetText(strs.expr_new)) + .WithDescription($"#{new kwum(ex.Id)}") + .AddField(GetText(strs.trigger), key) + .AddField(GetText(strs.response), + message.Length > 1024 ? GetText(strs.redacted_too_long) : message)) + .SendAsync(); } [Cmd] @@ -101,12 +103,12 @@ public partial class NadekoExpressions : NadekoModule if (ex is not null) { await ctx.Channel.EmbedAsync(new EmbedBuilder() - .WithOkColor() - .WithTitle(GetText(strs.expr_edited)) - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), ex.Trigger) - .AddField(GetText(strs.response), - message.Length > 1024 ? GetText(strs.redacted_too_long) : message)); + .WithOkColor() + .WithTitle(GetText(strs.expr_edited)) + .WithDescription($"#{id}") + .AddField(GetText(strs.trigger), ex.Trigger) + .AddField(GetText(strs.response), + message.Length > 1024 ? GetText(strs.redacted_too_long) : message)); } else { @@ -123,33 +125,36 @@ public partial class NadekoExpressions : NadekoModule return; } - var expressions = _service.GetExpressionsFor(ctx.Guild?.Id); + var allExpressions = _service.GetExpressionsFor(ctx.Guild?.Id) + .OrderBy(x => x.Trigger) + .ToArray(); - if (expressions is null || !expressions.Any()) + if (allExpressions is null || !allExpressions.Any()) { await Response().Error(strs.expr_no_found).SendAsync(); return; } - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var desc = expressions.OrderBy(ex => ex.Trigger) - .Skip(curPage * 20) - .Take(20) - .Select(ex => $"{(ex.ContainsAnywhere ? "🗯" : "◾")}" - + $"{(ex.DmResponse ? "✉" : "◾")}" - + $"{(ex.AutoDeleteTrigger ? "❌" : "◾")}" - + $"`{(kwum)ex.Id}` {ex.Trigger}" - + (string.IsNullOrWhiteSpace(ex.Reactions) - ? string.Empty - : " // " + string.Join(" ", ex.GetReactions()))) - .Join('\n'); + await Response() + .Paginated() + .Items(allExpressions) + .PageSize(20) + .CurrentPage(page) + .Page((exprs, _) => + { + var desc = exprs + .Select(ex => $"{(ex.ContainsAnywhere ? "🗯" : "◾")}" + + $"{(ex.DmResponse ? "✉" : "◾")}" + + $"{(ex.AutoDeleteTrigger ? "❌" : "◾")}" + + $"`{(kwum)ex.Id}` {ex.Trigger}" + + (string.IsNullOrWhiteSpace(ex.Reactions) + ? string.Empty + : " // " + string.Join(" ", ex.GetReactions()))) + .Join('\n'); - return new EmbedBuilder().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc); - }, - expressions.Length, - 20); + return new EmbedBuilder().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc); + }) + .SendAsync(); } [Cmd] @@ -164,11 +169,11 @@ public partial class NadekoExpressions : NadekoModule } await ctx.Channel.EmbedAsync(new EmbedBuilder() - .WithOkColor() - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024)) - .AddField(GetText(strs.response), - found.Response.TrimTo(1000).Replace("](", "]\\("))); + .WithOkColor() + .WithDescription($"#{id}") + .AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024)) + .AddField(GetText(strs.response), + found.Response.TrimTo(1000).Replace("](", "]\\("))); } public async Task ExprDeleteInternalAsync(kwum id) @@ -178,11 +183,11 @@ public partial class NadekoExpressions : NadekoModule if (ex is not null) { await ctx.Channel.EmbedAsync(new EmbedBuilder() - .WithOkColor() - .WithTitle(GetText(strs.expr_deleted)) - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), ex.Trigger.TrimTo(1024)) - .AddField(GetText(strs.response), ex.Response.TrimTo(1024))); + .WithOkColor() + .WithTitle(GetText(strs.expr_deleted)) + .WithDescription($"#{id}") + .AddField(GetText(strs.trigger), ex.Trigger.TrimTo(1024)) + .AddField(GetText(strs.response), ex.Response.TrimTo(1024))); } else { @@ -328,8 +333,8 @@ public partial class NadekoExpressions : NadekoModule public async Task ExprClear() { if (await PromptUserConfirmAsync(new EmbedBuilder() - .WithTitle("Expression clear") - .WithDescription("This will delete all expressions on this server."))) + .WithTitle("Expression clear") + .WithDescription("This will delete all expressions on this server."))) { var count = _service.DeleteAllExpressions(ctx.Guild.Id); await Response().Confirm(strs.exprs_cleared(count)).SendAsync(); diff --git a/src/NadekoBot/Modules/Permissions/Blacklist/BlacklistCommands.cs b/src/NadekoBot/Modules/Permissions/Blacklist/BlacklistCommands.cs index 270ea4bb0..a35ad60b3 100644 --- a/src/NadekoBot/Modules/Permissions/Blacklist/BlacklistCommands.cs +++ b/src/NadekoBot/Modules/Permissions/Blacklist/BlacklistCommands.cs @@ -20,7 +20,7 @@ public partial class Permissions throw new ArgumentOutOfRangeException(nameof(page)); var list = _service.GetBlacklist(); - var items = await list.Where(x => x.Type == type) + var allItems = await list.Where(x => x.Type == type) .Select(async i => { try @@ -52,18 +52,25 @@ public partial class Permissions }) .WhenAll(); - await ctx.SendPaginatedConfirmAsync(page, - curPage => - { - var pageItems = items.Skip(10 * curPage).Take(10).ToList(); + await Response() + .Paginated() + .Items(allItems) + .PageSize(10) + .CurrentPage(page) + .Page((pageItems, _) => + { + if (pageItems.Count == 0) + return new EmbedBuilder() + .WithOkColor() + .WithTitle(title) + .WithDescription(GetText(strs.empty_page)); - if (pageItems.Count == 0) - return new EmbedBuilder().WithOkColor().WithTitle(title).WithDescription(GetText(strs.empty_page)); - - return new EmbedBuilder().WithTitle(title).WithDescription(pageItems.Join('\n')).WithOkColor(); - }, - items.Length, - 10); + return new EmbedBuilder() + .WithTitle(title) + .WithDescription(allItems.Join('\n')) + .WithOkColor(); + }) + .SendAsync(); } [Cmd] @@ -130,13 +137,17 @@ public partial class Permissions if (action == AddRemove.Add) { - await Response().Confirm(strs.blacklisted(Format.Code(type.ToString()), - Format.Code(id.ToString()))).SendAsync(); + await Response() + .Confirm(strs.blacklisted(Format.Code(type.ToString()), + Format.Code(id.ToString()))) + .SendAsync(); } else { - await Response().Confirm(strs.unblacklisted(Format.Code(type.ToString()), - Format.Code(id.ToString()))).SendAsync(); + await Response() + .Confirm(strs.unblacklisted(Format.Code(type.ToString()), + Format.Code(id.ToString()))) + .SendAsync(); } } } diff --git a/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs b/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs index 897affa1e..04fe223ae 100644 --- a/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs +++ b/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs @@ -120,7 +120,7 @@ public partial class Searches await Response().Embed(new EmbedBuilder().WithOkColor().WithDescription(GetText(strs.feed_no_feed))).SendAsync(); return; } - + await ctx.SendPaginatedConfirmAsync(0, cur => { diff --git a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs index c7ce854a6..2eeca41f9 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationCommands.cs @@ -70,7 +70,7 @@ public partial class Searches if (page-- < 1) return; - var streams = new List(); + var allStreams = new List(); await using (var uow = _db.GetDbContext()) { var all = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(gc => gc.FollowedStreams)) @@ -83,34 +83,32 @@ public partial class Searches if (((SocketGuild)ctx.Guild).GetTextChannel(fs.ChannelId) is null) await _service.UnfollowStreamAsync(fs.GuildId, index); else - streams.Insert(0, fs); + allStreams.Insert(0, fs); } } - await ctx.SendPaginatedConfirmAsync(page, - cur => - { - var elements = streams - .Skip(cur * 12) - .Take(12) - .ToList(); + await Response() + .Paginated() + .Items(allStreams) + .PageSize(12) + .CurrentPage(page) + .Page((elements, cur) => + { + if (elements.Count == 0) + return new EmbedBuilder().WithDescription(GetText(strs.streams_none)).WithErrorColor(); - if (elements.Count == 0) - return new EmbedBuilder().WithDescription(GetText(strs.streams_none)).WithErrorColor(); + var eb = new EmbedBuilder().WithTitle(GetText(strs.streams_follow_title)).WithOkColor(); + for (var index = 0; index < elements.Count; index++) + { + var elem = elements[index]; + eb.AddField($"**#{index + 1 + (12 * cur)}** {elem.Username.ToLower()}", + $"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}", + true); + } - var eb = new EmbedBuilder().WithTitle(GetText(strs.streams_follow_title)).WithOkColor(); - for (var index = 0; index < elements.Count; index++) - { - var elem = elements[index]; - eb.AddField($"**#{index + 1 + (12 * cur)}** {elem.Username.ToLower()}", - $"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}", - true); - } - - return eb; - }, - streams.Count, - 12); + return eb; + }) + .SendAsync(); } [Cmd] diff --git a/src/NadekoBot/Modules/Utility/Info/InviteCommands.cs b/src/NadekoBot/Modules/Utility/Info/InviteCommands.cs index cbbf0575f..bab37298d 100644 --- a/src/NadekoBot/Modules/Utility/Info/InviteCommands.cs +++ b/src/NadekoBot/Modules/Utility/Info/InviteCommands.cs @@ -37,36 +37,39 @@ public partial class Utility var invites = await channel.GetInvitesAsync(); - await ctx.SendPaginatedConfirmAsync(page, - cur => - { - var i = 1; - var invs = invites.Skip(cur * 9).Take(9).ToList(); - if (!invs.Any()) - return new EmbedBuilder().WithErrorColor().WithDescription(GetText(strs.no_invites)); + await Response() + .Paginated() + .Items(invites) + .PageSize(9) + .Page((invs, _) => + { + var i = 1; - var embed = new EmbedBuilder().WithOkColor(); - foreach (var inv in invites) - { - var expiryString = inv.MaxAge is null or 0 || inv.CreatedAt is null - ? "∞" - : (inv.CreatedAt.Value.AddSeconds(inv.MaxAge.Value).UtcDateTime - DateTime.UtcNow).ToString( - """d\.hh\:mm\:ss"""); - var creator = inv.Inviter.ToString().TrimTo(25); - var usesString = $"{inv.Uses} / {(inv.MaxUses == 0 ? "∞" : inv.MaxUses?.ToString())}"; + if (!invs.Any()) + return new EmbedBuilder().WithErrorColor().WithDescription(GetText(strs.no_invites)); - var desc = $@"`{GetText(strs.inv_uses)}` **{usesString}** + var embed = new EmbedBuilder().WithOkColor(); + foreach (var inv in invs) + { + var expiryString = inv.MaxAge is null or 0 || inv.CreatedAt is null + ? "∞" + : (inv.CreatedAt.Value.AddSeconds(inv.MaxAge.Value).UtcDateTime - DateTime.UtcNow) + .ToString( + """d\.hh\:mm\:ss"""); + var creator = inv.Inviter.ToString().TrimTo(25); + var usesString = $"{inv.Uses} / {(inv.MaxUses == 0 ? "∞" : inv.MaxUses?.ToString())}"; + + var desc = $@"`{GetText(strs.inv_uses)}` **{usesString}** `{GetText(strs.inv_expire)}` **{expiryString}** - + {inv.Url} "; - embed.AddField($"#{i++} {creator}", desc); - } + embed.AddField($"#{i++} {creator}", desc); + } - return embed; - }, - invites.Count, - 9); + return embed; + }) + .SendAsync(); } [Cmd] diff --git a/src/NadekoBot/Modules/Xp/Club/Club.cs b/src/NadekoBot/Modules/Xp/Club/Club.cs index ed8046592..4e50f1a2d 100644 --- a/src/NadekoBot/Modules/Xp/Club/Club.cs +++ b/src/NadekoBot/Modules/Xp/Club/Club.cs @@ -29,12 +29,14 @@ public partial class Xp } else { - await Response().Confirm( - strs.club_transfered( - Format.Bold(club.Name), - Format.Bold(newOwner.ToString()) - ) - ).SendAsync(); + await Response() + .Confirm( + strs.club_transfered( + Format.Bold(club.Name), + Format.Bold(newOwner.ToString()) + ) + ) + .SendAsync(); } } @@ -65,7 +67,7 @@ public partial class Xp await Response().Error(strs.club_name_too_long).SendAsync(); return; } - + if (result == ClubCreateResult.NameTaken) { await Response().Error(strs.club_name_taken).SendAsync(); @@ -110,49 +112,52 @@ public partial class Xp private async Task InternalClubInfoAsync(ClubInfo club) { var lvl = new LevelStats(club.Xp); - var users = club.Members.OrderByDescending(x => - { - var l = new LevelStats(x.TotalXp).Level; - if (club.OwnerId == x.Id) - return int.MaxValue; - if (x.IsClubAdmin) - return (int.MaxValue / 2) + l; - return l; - }); + var allUsers = club.Members.OrderByDescending(x => + { + var l = new LevelStats(x.TotalXp).Level; + if (club.OwnerId == x.Id) + return int.MaxValue; + if (x.IsClubAdmin) + return (int.MaxValue / 2) + l; + return l; + }) + .ToList(); - await ctx.SendPaginatedConfirmAsync(0, - page => - { - var embed = new EmbedBuilder() - .WithOkColor() - .WithTitle($"{club}") - .WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)"))) - .AddField(GetText(strs.desc), - string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description) - .AddField(GetText(strs.owner), club.Owner.ToString(), true) - // .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true) - .AddField(GetText(strs.members), - string.Join("\n", - users.Skip(page * 10) - .Take(10) - .Select(x => - { - var l = new LevelStats(x.TotalXp); - var lvlStr = Format.Bold($" ⟪{l.Level}⟫"); - if (club.OwnerId == x.Id) - return x + "🌟" + lvlStr; - if (x.IsClubAdmin) - return x + "⭐" + lvlStr; - return x + lvlStr; - }))); + await Response() + .Paginated() + .Items(allUsers) + .PageSize(10) + .CurrentPage(0) + .Page((users, _) => + { + var embed = new EmbedBuilder() + .WithOkColor() + .WithTitle($"{club}") + .WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)"))) + .AddField(GetText(strs.desc), + string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description) + .AddField(GetText(strs.owner), club.Owner.ToString(), true) + // .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true) + .AddField(GetText(strs.members), + string.Join("\n", + users + .Select(x => + { + var l = new LevelStats(x.TotalXp); + var lvlStr = Format.Bold($" ⟪{l.Level}⟫"); + if (club.OwnerId == x.Id) + return x + "🌟" + lvlStr; + if (x.IsClubAdmin) + return x + "⭐" + lvlStr; + return x + lvlStr; + }))); - if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute)) - return embed.WithThumbnailUrl(club.ImageUrl); + if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute)) + return embed.WithThumbnailUrl(club.ImageUrl); - return embed; - }, - club.Members.Count, - 10); + return embed; + }) + .SendAsync(); } [Cmd] @@ -204,14 +209,16 @@ public partial class Xp return ctx.SendPaginatedConfirmAsync(page, _ => { - var toShow = string.Join("\n", bans - .Skip(page * 10).Take(10) - .Select(x => x.ToString())); + var toShow = string.Join("\n", + bans + .Skip(page * 10) + .Take(10) + .Select(x => x.ToString())); return new EmbedBuilder() - .WithTitle(GetText(strs.club_bans_for(club.ToString()))) - .WithDescription(toShow) - .WithOkColor(); + .WithTitle(GetText(strs.club_bans_for(club.ToString()))) + .WithDescription(toShow) + .WithOkColor(); }, bans.Length, 10); @@ -235,9 +242,9 @@ public partial class Xp var toShow = string.Join("\n", apps.Skip(page * 10).Take(10).Select(x => x.ToString())); return new EmbedBuilder() - .WithTitle(GetText(strs.club_apps_for(club.ToString()))) - .WithDescription(toShow) - .WithOkColor(); + .WithTitle(GetText(strs.club_apps_for(club.ToString()))) + .WithDescription(toShow) + .WithOkColor(); }, apps.Length, 10); @@ -283,7 +290,7 @@ public partial class Xp else if (result == ClubAcceptResult.NotOwnerOrAdmin) await Response().Error(strs.club_admin_perms).SendAsync(); } - + [Cmd] [Priority(1)] public Task ClubReject(IUser user) @@ -296,9 +303,9 @@ public partial class Xp var result = _service.RejectApplication(ctx.User.Id, userName, out var discordUser); if (result == ClubDenyResult.Rejected) await Response().Confirm(strs.club_rejected(Format.Bold(discordUser.ToString()))).SendAsync(); - else if(result == ClubDenyResult.NoSuchApplicant) + else if (result == ClubDenyResult.NoSuchApplicant) await Response().Error(strs.club_accept_invalid_applicant).SendAsync(); - else if(result == ClubDenyResult.NotOwnerOrAdmin) + else if (result == ClubDenyResult.NotOwnerOrAdmin) await Response().Error(strs.club_admin_perms).SendAsync(); } @@ -327,8 +334,10 @@ public partial class Xp var result = _service.Kick(ctx.User.Id, userName, out var club); if (result == ClubKickResult.Success) { - return Response().Confirm(strs.club_user_kick(Format.Bold(userName), - Format.Bold(club.ToString()))).SendAsync(); + return Response() + .Confirm(strs.club_user_kick(Format.Bold(userName), + Format.Bold(club.ToString()))) + .SendAsync(); } if (result == ClubKickResult.Hierarchy) @@ -352,8 +361,10 @@ public partial class Xp var result = _service.Ban(ctx.User.Id, userName, out var club); if (result == ClubBanResult.Success) { - return Response().Confirm(strs.club_user_banned(Format.Bold(userName), - Format.Bold(club.ToString()))).SendAsync(); + return Response() + .Confirm(strs.club_user_banned(Format.Bold(userName), + Format.Bold(club.ToString()))) + .SendAsync(); } if (result == ClubBanResult.Unbannable) @@ -378,8 +389,10 @@ public partial class Xp if (result == ClubUnbanResult.Success) { - return Response().Confirm(strs.club_user_unbanned(Format.Bold(userName), - Format.Bold(club.ToString()))).SendAsync(); + return Response() + .Confirm(strs.club_user_unbanned(Format.Bold(userName), + Format.Bold(club.ToString()))) + .SendAsync(); } if (result == ClubUnbanResult.WrongUser) @@ -400,10 +413,10 @@ public partial class Xp : desc; var eb = new EmbedBuilder() - .WithAuthor(ctx.User) - .WithTitle(GetText(strs.club_desc_update)) - .WithOkColor() - .WithDescription(desc); + .WithAuthor(ctx.User) + .WithTitle(GetText(strs.club_desc_update)) + .WithOkColor() + .WithDescription(desc); await Response().Embed(eb).SendAsync(); } @@ -450,11 +463,11 @@ public partial class Xp await Response().Error(strs.club_name_too_long).SendAsync(); return; case ClubRenameResult.Success: - { - var embed = new EmbedBuilder().WithTitle(GetText(strs.club_renamed(clubName))).WithOkColor(); - await Response().Embed(embed).SendAsync(); - return; - } + { + var embed = new EmbedBuilder().WithTitle(GetText(strs.club_renamed(clubName))).WithOkColor(); + await Response().Embed(embed).SendAsync(); + return; + } case ClubRenameResult.NameTaken: await Response().Error(strs.club_name_taken).SendAsync(); return; diff --git a/src/NadekoBot/_common/Sender/MessageChannelExtensions.cs b/src/NadekoBot/_common/Sender/MessageChannelExtensions.cs index 78369f8f1..41467af14 100644 --- a/src/NadekoBot/_common/Sender/MessageChannelExtensions.cs +++ b/src/NadekoBot/_common/Sender/MessageChannelExtensions.cs @@ -96,7 +96,7 @@ public static class MessageChannelExtensions embed: embed?.Build(), embeds: embeds?.Map(x => x.Build()), replyTo: replyTo); - + // embed title and optional footer overloads public static Task SendPaginatedConfirmAsync( diff --git a/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs b/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs new file mode 100644 index 000000000..df979d2a0 --- /dev/null +++ b/src/NadekoBot/_common/Sender/ResponseBuilder.PaginationSender.cs @@ -0,0 +1,155 @@ +namespace NadekoBot.Extensions; + +public partial class ResponseBuilder +{ + public class PaginationSender + { + private const string BUTTON_LEFT = "BUTTON_LEFT"; + private const string BUTTON_RIGHT = "BUTTON_RIGHT"; + + private static readonly IEmote _arrowLeft = Emote.Parse("<:x:1232256519844790302>"); + private static readonly IEmote _arrowRight = Emote.Parse("<:x:1232256515298295838>"); + + private readonly SourcedPaginatedResponseBuilder _paginationBuilder; + private readonly ResponseBuilder builder; + private readonly DiscordSocketClient client; + private int currentPage; + + public PaginationSender( + SourcedPaginatedResponseBuilder paginationBuilder, + ResponseBuilder builder + ) + { + this._paginationBuilder = paginationBuilder; + this.builder = builder; + + client = (DiscordSocketClient)builder.ctx.Client; + currentPage = 0; + } + + public async Task SendAsync(bool ephemeral = false) + { + var lastPage = (_paginationBuilder.TotalElements - 1) + / _paginationBuilder.ItemsPerPage; + + var items = (await _paginationBuilder.ItemsFunc(currentPage)).ToArray(); + var embed = await _paginationBuilder.PageFunc(items, currentPage); + + if (_paginationBuilder.AddPaginatedFooter) + embed.AddPaginatedFooter(currentPage, lastPage); + + SimpleInteraction? maybeInter = null; + + async Task GetComponentBuilder() + { + var cb = new ComponentBuilder(); + + cb.WithButton(new ButtonBuilder() + .WithStyle(ButtonStyle.Primary) + .WithCustomId(BUTTON_LEFT) + .WithDisabled(lastPage == 0) + .WithEmote(_arrowLeft) + .WithDisabled(currentPage <= 0)); + // todo + // if (interFactory is not null) + // { + // maybeInter = await interFactory(currentPage); + // + // if (maybeInter is not null) + // cb.WithButton(maybeInter.Button); + // } + + cb.WithButton(new ButtonBuilder() + .WithStyle(ButtonStyle.Primary) + .WithCustomId(BUTTON_RIGHT) + .WithDisabled(lastPage == 0 || currentPage >= lastPage) + .WithEmote(_arrowRight)); + + return cb; + } + + async Task UpdatePageAsync(SocketMessageComponent smc) + { + var pageItems = (await _paginationBuilder.ItemsFunc(currentPage)).ToArray(); + var toSend = await _paginationBuilder.PageFunc(pageItems, currentPage); + if (_paginationBuilder.AddPaginatedFooter) + toSend.AddPaginatedFooter(currentPage, lastPage); + + var component = (await GetComponentBuilder()).Build(); + + await smc.ModifyOriginalResponseAsync(x => + { + x.Embed = toSend.Build(); + x.Components = component; + }); + } + + var model = builder.Build(ephemeral); + + var component = (await GetComponentBuilder()).Build(); + var msg = await model.TargetChannel + .SendMessageAsync(model.Text, + embed: embed.Build(), + components: component, + messageReference: model.MessageReference); + + async Task OnInteractionAsync(SocketInteraction si) + { + try + { + if (si is not SocketMessageComponent smc) + return; + + if (smc.Message.Id != msg.Id) + return; + + await si.DeferAsync(); + if (smc.User.Id != model.User.Id) + return; + + if (smc.Data.CustomId == BUTTON_LEFT) + { + if (currentPage == 0) + return; + + --currentPage; + _ = UpdatePageAsync(smc); + } + else if (smc.Data.CustomId == BUTTON_RIGHT) + { + if (currentPage >= lastPage) + return; + + ++currentPage; + _ = UpdatePageAsync(smc); + } + else if (maybeInter is { } inter && inter.Button.CustomId == smc.Data.CustomId) + { + await inter.TriggerAsync(smc); + _ = UpdatePageAsync(smc); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message); + } + } + // todo re-add + // if (lastPage == 0 && interFactory is null) + // return; + + if (lastPage == 0) + return; + + var client = this.client; + + client.InteractionCreated += OnInteractionAsync; + + await Task.Delay(30_000); + + client.InteractionCreated -= OnInteractionAsync; + + await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build()); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/_common/Sender/ResponseBuilder.cs b/src/NadekoBot/_common/Sender/ResponseBuilder.cs index ec46efa78..a3bb5aed4 100644 --- a/src/NadekoBot/_common/Sender/ResponseBuilder.cs +++ b/src/NadekoBot/_common/Sender/ResponseBuilder.cs @@ -1,6 +1,6 @@ namespace NadekoBot.Extensions; -public sealed class ResponseBuilder +public sealed partial class ResponseBuilder { private ICommandContext? ctx = null; private IMessageChannel? channel = null; @@ -44,33 +44,53 @@ public sealed class ResponseBuilder failIfNotExists: false); } - public async Task SendAsync(bool ephemeral = false) + public ResponseMessageModel Build(bool ephemeral = false) { // todo use ephemeral in interactions var targetChannel = InternalResolveChannel() ?? throw new ArgumentNullException(nameof(channel)); var msgReference = CreateMessageReference(targetChannel); var txt = GetText(locTxt); + // todo check message sanitization - if (sanitizeMentions) - txt = txt?.SanitizeMentions(true); + var buildModel = new ResponseMessageModel() + { + TargetChannel = targetChannel, + MessageReference = msgReference, + Text = txt, + User = ctx?.User, + Embed = embed ?? embedBuilder?.Build(), + Embeds = embeds?.Map(x => x.Build()), + SanitizeMentions = sanitizeMentions ? new(AllowedMentionTypes.Users) : AllowedMentions.All + }; + return buildModel; + } + + public Task SendAsync(bool ephemeral = false) + { + var model = Build(ephemeral); + return SendAsync(model); + } + + public async Task SendAsync(ResponseMessageModel model) + { if (this.fileStream is Stream stream) - return await targetChannel.SendFileAsync(stream, + return await model.TargetChannel.SendFileAsync(stream, filename: fileName, - txt, - embed: embed ?? embedBuilder?.Build(), + model.Text, + embed: model.Embed, components: null, - allowedMentions: sanitizeMentions ? new(AllowedMentionTypes.Users) : AllowedMentions.All, - messageReference: msgReference); + allowedMentions: model.SanitizeMentions, + messageReference: model.MessageReference); - return await targetChannel.SendMessageAsync( - txt, - embed: embed ?? embedBuilder?.Build(), - embeds: embeds?.Map(x => x.Build()), + return await model.TargetChannel.SendMessageAsync( + model.Text, + embed: model.Embed, + embeds: model.Embeds, components: null, - allowedMentions: sanitizeMentions ? new(AllowedMentionTypes.Users) : AllowedMentions.All, - messageReference: msgReference); + allowedMentions: model.SanitizeMentions, + messageReference: model.MessageReference); } private ulong? InternalResolveGuildId(IMessageChannel? targetChannel) @@ -263,4 +283,94 @@ public sealed class ResponseBuilder this.fileName = fileName; return this; } + + public PaginatedResponseBuilder Paginated() + => new(this); +} + +public class PaginatedResponseBuilder +{ + protected readonly ResponseBuilder _builder; + + public PaginatedResponseBuilder(ResponseBuilder builder) + { + _builder = builder; + } + + public SourcedPaginatedResponseBuilder Items(IReadOnlyCollection items) + => new SourcedPaginatedResponseBuilder(_builder) + .Items(items); +} + +public sealed class SourcedPaginatedResponseBuilder : PaginatedResponseBuilder +{ + private IReadOnlyCollection? items; + public Func, int, Task> PageFunc { get; private set; } + public Func>> ItemsFunc { get; set; } + public int TotalElements { get; private set; } = 1; + public int ItemsPerPage { get; private set; } = 9; + public bool AddPaginatedFooter { get; private set; } = true; + public bool IsEphemeral { get; private set; } + + public SourcedPaginatedResponseBuilder(ResponseBuilder builder) + : base(builder) + { + } + + public SourcedPaginatedResponseBuilder Items(IReadOnlyCollection items) + { + this.items = items; + ItemsFunc = (i) => Task.FromResult(this.items.Skip(i * ItemsPerPage).Take(ItemsPerPage)); + return this; + } + + + public SourcedPaginatedResponseBuilder PageSize(int i) + { + ItemsPerPage = i; + return this; + } + + public SourcedPaginatedResponseBuilder CurrentPage(int i) + { + InitialPage = i; + return this; + } + + // todo use it + public int InitialPage { get; set; } + + public SourcedPaginatedResponseBuilder Page(Func, int, EmbedBuilder> pageFunc) + { + this.PageFunc = (xs, x) => Task.FromResult(pageFunc(xs, x)); + return this; + } + + public SourcedPaginatedResponseBuilder Page(Func, int, Task> pageFunc) + { + this.PageFunc = pageFunc; + return this; + } + + public SourcedPaginatedResponseBuilder AddFooter() + { + AddPaginatedFooter = true; + return this; + } + + public SourcedPaginatedResponseBuilder Ephemeral() + { + IsEphemeral = true; + return this; + } + + + public Task SendAsync() + { + var paginationSender = new ResponseBuilder.PaginationSender( + this, + _builder); + + return paginationSender.SendAsync(IsEphemeral); + } } \ No newline at end of file diff --git a/src/NadekoBot/_common/Sender/ResponseMessageModel.cs b/src/NadekoBot/_common/Sender/ResponseMessageModel.cs new file mode 100644 index 000000000..cf34c9cbc --- /dev/null +++ b/src/NadekoBot/_common/Sender/ResponseMessageModel.cs @@ -0,0 +1,10 @@ +public class ResponseMessageModel +{ + public IMessageChannel TargetChannel { get; set; } + public MessageReference MessageReference { get; set; } + public string Text { get; set; } + public Embed Embed { get; set; } + public Embed[] Embeds { get; set; } + public AllowedMentions SanitizeMentions { get; set; } + public IUser User { get; set; } +} \ No newline at end of file