public static async Task Paginate <T>(this Context ctx, IAsyncEnumerable <T> items, int totalCount, int itemsPerPage, string title, string color, Func <EmbedBuilder, IEnumerable <T>, Task> renderer) { // TODO: make this generic enough we can use it in Choose<T> below var buffer = new List <T>(); await using var enumerator = items.GetAsyncEnumerator(); var pageCount = (int)Math.Ceiling(totalCount / (double)itemsPerPage); async Task <Embed> MakeEmbedForPage(int page) { var bufferedItemsNeeded = (page + 1) * itemsPerPage; while (buffer.Count < bufferedItemsNeeded && await enumerator.MoveNextAsync()) { buffer.Add(enumerator.Current); } var eb = new EmbedBuilder(); eb.Title(pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title); if (color != null) { eb.Color(color.ToDiscordColor()); } await renderer(eb, buffer.Skip(page *itemsPerPage).Take(itemsPerPage)); return(eb.Build()); } try { var msg = await ctx.Reply(embed : await MakeEmbedForPage(0)); if (pageCount <= 1) { return; // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol } string[] botEmojis = { "\u23EA", "\u2B05", "\u27A1", "\u23E9", Emojis.Error }; var _ = ctx.Rest.CreateReactionsBulk(msg, botEmojis); // Again, "fork" try { var currentPage = 0; while (true) { var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout : Duration.FromMinutes(5)); // Increment/decrement page counter based on which reaction was clicked if (reaction.Emoji.Name == "\u23EA") { currentPage = 0; // << } if (reaction.Emoji.Name == "\u2B05") { currentPage = (currentPage - 1) % pageCount; // < } if (reaction.Emoji.Name == "\u27A1") { currentPage = (currentPage + 1) % pageCount; // > } if (reaction.Emoji.Name == "\u23E9") { currentPage = pageCount - 1; // >> } if (reaction.Emoji.Name == Emojis.Error) { break; // X } // C#'s % operator is dumb and wrong, so we fix negative numbers if (currentPage < 0) { currentPage += pageCount; } // If we can, remove the user's reaction (so they can press again quickly) if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) { await ctx.Rest.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId); } // Edit the embed with the new page var embed = await MakeEmbedForPage(currentPage); await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest { Embed = embed }); } } catch (TimeoutException) { // "escape hatch", clean up as if we hit X } // todo: re-check if (ctx.BotPermissions.HasFlag(PermissionSet.ManageMessages)) { await ctx.Rest.DeleteAllReactions(msg.ChannelId, msg.Id); } } // If we get a "NotFound" error, the message has been deleted and thus not our problem catch (NotFoundException) { } // If we get an "Unauthorized" error, we don't have permissions to remove our reaction // which means we probably didn't add it in the first place, or permissions changed since then // either way, nothing to do here catch (UnauthorizedException) { } }
public static async Task Paginate <T>(this Context ctx, IAsyncEnumerable <T> items, int totalCount, int itemsPerPage, string title, string color, Func <EmbedBuilder, IEnumerable <T>, Task> renderer) { // TODO: make this generic enough we can use it in Choose<T> below var buffer = new List <T>(); await using var enumerator = items.GetAsyncEnumerator(); var pageCount = (int)Math.Ceiling(totalCount / (double)itemsPerPage); async Task <Embed> MakeEmbedForPage(int page) { var bufferedItemsNeeded = (page + 1) * itemsPerPage; while (buffer.Count < bufferedItemsNeeded && await enumerator.MoveNextAsync()) { buffer.Add(enumerator.Current); } var eb = new EmbedBuilder(); eb.Title(pageCount > 1 ? $"[{page + 1}/{pageCount}] {title}" : title); if (color != null) { eb.Color(color.ToDiscordColor()); } await renderer(eb, buffer.Skip(page *itemsPerPage).Take(itemsPerPage)); return(eb.Build()); } async Task <int> PromptPageNumber() { bool Predicate(MessageCreateEvent e) => e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id; var msg = await ctx.Services.Resolve <HandlerQueue <MessageCreateEvent> >() .WaitFor(Predicate, Duration.FromMinutes(0.5)); int.TryParse(msg.Content, out var num); return(num); } try { var msg = await ctx.Reply(embed : await MakeEmbedForPage(0)); // If we only have one (or no) page, don't bother with the reaction/pagination logic, lol if (pageCount <= 1) { return; } string[] botEmojis = { "\u23EA", "\u2B05", "\u27A1", "\u23E9", "\uD83D\uDD22", Emojis.Error }; var _ = ctx.Rest.CreateReactionsBulk(msg, botEmojis); // Again, "fork" try { var currentPage = 0; while (true) { var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout : Duration.FromMinutes(5)); // Increment/decrement page counter based on which reaction was clicked if (reaction.Emoji.Name == "\u23EA") { currentPage = 0; // << } else if (reaction.Emoji.Name == "\u2B05") { currentPage = (currentPage - 1) % pageCount; // < } else if (reaction.Emoji.Name == "\u27A1") { currentPage = (currentPage + 1) % pageCount; // > } else if (reaction.Emoji.Name == "\u23E9") { currentPage = pageCount - 1; // >> } else if (reaction.Emoji.Name == Emojis.Error) { break; // X } else if (reaction.Emoji.Name == "\u0031\uFE0F\u20E3") { currentPage = 0; } else if (reaction.Emoji.Name == "\u0032\uFE0F\u20E3") { currentPage = 1; } else if (reaction.Emoji.Name == "\u0033\uFE0F\u20E3") { currentPage = 2; } else if (reaction.Emoji.Name == "\u0034\uFE0F\u20E3" && pageCount >= 3) { currentPage = 3; } else if (reaction.Emoji.Name == "\u0035\uFE0F\u20E3" && pageCount >= 4) { currentPage = 4; } else if (reaction.Emoji.Name == "\u0036\uFE0F\u20E3" && pageCount >= 5) { currentPage = 5; } else if (reaction.Emoji.Name == "\u0037\uFE0F\u20E3" && pageCount >= 6) { currentPage = 6; } else if (reaction.Emoji.Name == "\u0038\uFE0F\u20E3" && pageCount >= 7) { currentPage = 7; } else if (reaction.Emoji.Name == "\u0039\uFE0F\u20E3" && pageCount >= 8) { currentPage = 8; } else if (reaction.Emoji.Name == "\U0001f51f" && pageCount >= 9) { currentPage = 9; } else if (reaction.Emoji.Name == "\uD83D\uDD22") { try { await ctx.Reply("What page would you like to go to?"); var repliedNum = await PromptPageNumber(); if (repliedNum < 1) { await ctx.Reply($"{Emojis.Error} Operation canceled (invalid number)."); continue; } if (repliedNum > pageCount) { await ctx.Reply( $"{Emojis.Error} That page number is too high (page count is {pageCount})."); continue; } currentPage = repliedNum - 1; } catch (TimeoutException) { await ctx.Reply($"{Emojis.Error} Operation timed out, sorry. Try again, perhaps?"); continue; } } // C#'s % operator is dumb and wrong, so we fix negative numbers if (currentPage < 0) { currentPage += pageCount; } // If we can, remove the user's reaction (so they can press again quickly) if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages)) { await ctx.Rest.DeleteUserReaction(msg.ChannelId, msg.Id, reaction.Emoji, reaction.UserId); } // Edit the embed with the new page var embed = await MakeEmbedForPage(currentPage); await ctx.Rest.EditMessage(msg.ChannelId, msg.Id, new MessageEditRequest { Embeds = new[] { embed } }); } } catch (TimeoutException) { // "escape hatch", clean up as if we hit X } // todo: re-check if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages)) { await ctx.Rest.DeleteAllReactions(msg.ChannelId, msg.Id); } } // If we get a "NotFound" error, the message has been deleted and thus not our problem catch (NotFoundException) { } // If we get an "Unauthorized" error, we don't have permissions to remove our reaction // which means we probably didn't add it in the first place, or permissions changed since then // either way, nothing to do here catch (ForbiddenException) { } }
public async Task PermCheckGuild(Context ctx) { Guild guild; GuildMemberPartial senderGuildUser = null; if (ctx.Guild != null && !ctx.HasNext()) { guild = ctx.Guild; senderGuildUser = ctx.Member; } else { var guildIdStr = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a server ID or run this command in a server."); if (!ulong.TryParse(guildIdStr, out var guildId)) { throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID."); } guild = await _rest.GetGuild(guildId); if (guild != null) { senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id); } if (guild == null || senderGuildUser == null) { throw Errors.GuildNotFound(guildId); } } var requiredPermissions = new [] { PermissionSet.ViewChannel, PermissionSet.SendMessages, PermissionSet.AddReactions, PermissionSet.AttachFiles, PermissionSet.EmbedLinks, PermissionSet.ManageMessages, PermissionSet.ManageWebhooks }; // Loop through every channel and group them by sets of permissions missing var permissionsMissing = new Dictionary <ulong, List <Channel> >(); var hiddenChannels = 0; foreach (var channel in await _rest.GetGuildChannels(guild.Id)) { var botPermissions = _bot.PermissionsIn(channel.Id); var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser.Roles); if ((userPermissions & PermissionSet.ViewChannel) == 0) { // If the user can't see this channel, don't calculate permissions for it // (to prevent info-leaking, mostly) // Instead, count how many hidden channels and show the user (so they don't get confused) hiddenChannels++; continue; } // We use a bitfield so we can set individual permission bits in the loop // TODO: Rewrite with proper bitfield math ulong missingPermissionField = 0; foreach (var requiredPermission in requiredPermissions) { if ((botPermissions & requiredPermission) == 0) { missingPermissionField |= (ulong)requiredPermission; } } // If we're not missing any permissions, don't bother adding it to the dict // This means we can check if the dict is empty to see if all channels are proxyable if (missingPermissionField != 0) { permissionsMissing.TryAdd(missingPermissionField, new List <Channel>()); permissionsMissing[missingPermissionField].Add(channel); } } // Generate the output embed var eb = new EmbedBuilder() .Title($"Permission check for **{guild.Name}**"); if (permissionsMissing.Count == 0) { eb.Description($"No errors found, all channels proxyable :)").Color(DiscordUtils.Green); } else { foreach (var(missingPermissionField, channels) in permissionsMissing) { // Each missing permission field can have multiple missing channels // so we extract them all and generate a comma-separated list var missingPermissionNames = ((PermissionSet)missingPermissionField).ToPermissionString(); var channelsList = string.Join("\n", channels .OrderBy(c => c.Position) .Select(c => $"#{c.Name}")); eb.Field(new($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000))); eb.Color(DiscordUtils.Red); } } if (hiddenChannels > 0) { eb.Footer(new($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them.")); } // Send! :) await ctx.Reply(embed : eb.Build()); }