Esempio n. 1
0
        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) { }
        }
Esempio n. 2
0
    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) { }
    }
Esempio n. 3
0
        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());
        }