Skip to content

Instantly share code, notes, and snippets.

@InterStella0
Last active December 26, 2024 15:10
Show Gist options
  • Save InterStella0/454cc51e05e60e63b81ea2e8490ef140 to your computer and use it in GitHub Desktop.
Save InterStella0/454cc51e05e60e63b81ea2e8490ef140 to your computer and use it in GitHub Desktop.
A walkthrough on action based pagination in discord.py

Pagination Walkthrough in discord.py

In this tutorial I will go through the entire length of pagination in discord.py

I kindly ask you to go through the entire thing, as it is not recommended to skip the subtopics because they are interconnected in some way.

Table Content

Pagination is an extremely common thing to do in discord.py that I decided to create this gist. One of the most common uses of pagination is when you want to show contents that are more than the limit of what discord allows you.

In this tutorial, you would need to install discord-ext-menus library that was written by Danny. The creator of discord.py. This library mainly uses reactions as interfaces. Now don't worry, we will go into discord button once we fully understand this library.

Before diving any further, make sure to install discord-ext-menus.

Getting Started

While the library contains more than just pagination. We will mainly go into pagination because that's the focus.

The library contains a few classes, mainly these ones.

  1. menus.Menu
  2. menus.MenuPages
  3. menus.ListPageSource
  4. menus.GroupByPageSource
  5. menus.AsyncIteratorPageSource

However, we will only look into 3 of them, which is menus.Menu , menus.MenuPages and menus.ListPageSource. The rest are derived from menus.MenuPages which you can learn on your own after this tutorial.

Menu

This class is responsible for handling the reactions given and the behaviour of what a reaction would do.

Let's use the example given by the library.

from discord.ext import menus

class MyMenu(menus.Menu):
    async def send_initial_message(self, ctx, channel):
        return await channel.send(f'Hello {ctx.author}')

    @menus.button('\N{THUMBS UP SIGN}')
    async def on_thumbs_up(self, payload):
        await self.message.edit(content=f'Thanks {self.ctx.author}!')

    @menus.button('\N{THUMBS DOWN SIGN}')
    async def on_thumbs_down(self, payload):
        await self.message.edit(content=f"That's not nice {self.ctx.author}...")

    @menus.button('\N{BLACK SQUARE FOR STOP}\ufe0f')
    async def on_stop(self, payload):
        self.stop()

@bot.command()
async def your_command(ctx):
    menu = MyMenu()
    await menu.start(ctx)

Explanation

  1. send_initial_message method is called when you call menu.start. You're required to return a message object for MyMenu to handle them.
  2. menus.button refers to your reaction. They are automatically reacted by your bot as soon as menu.start is called. The first argument would be the emoji that you would want to listen to and added to the Message.
  3. The callbacks of each decorator of menus.button are called when you reacted to the corresponding reaction in the decorator.
  4. self.stop method stops the MyMenu class from listening to the reactions. This fully stops the instance from operating.

To simply, this sums up on what menus.Menu class does. Of course, it's a lot more complicated than this but this is the simplification i can give you. menu_works

Output

menu_output

Discussion

As you can see, you can do plenty of things with this class. There's lots of application that can be used from this class alone.

For example, you can do a confirmation button using pure reactions. Have a greater control in controlling reactions without any headache. You can also use this for controls in a game where it is purely based on reactions. While yes, discord button exist. This were made before the existence of discord buttons.

It's great to have a fundamental understanding of how this will help us on creating a pagination based on reactions AND discord buttons.

Pagination

MenuPages and ListPageSource

[Reaction based pagination]

MenuPages

This class is responsible for controlling the reactions for pagination. It is responsible for handling the reactions properly on what to do whether to show the first page, next page and so on.

This class inherits menus.Menu. Meaning, all functionality coming from menus.Menu is also available in this class. This class fully has all the methods to control a pagination.

However, this class alone is entirely useless. To make it useful, we will need ListPageSource.

ListPageSource

This class is responsible for formatting each page in our paginator. It is also responsible for handling each of our data. Given an iterable into the class would allow it to fully handle the data into each pages. We will see this in action in Combine MenuPages & ListPageSource

Combine MenuPages & ListPageSource

With the help of MenuPages as our reaction manager and ListPageSource as our data and format manager. We can fully make an operational paginator with them.

Example

from discord.ext import menus

class MySource(menus.ListPageSource):
    async def format_page(self, menu, entries):
        return f"This is number {entries}."

@bot.command()
async def your_command(ctx):
    data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    formatter = MySource(data, per_page=1)
    menu = menus.MenuPages(formatter)
    await menu.start(ctx)

Explanation

  1. MySource acts as a formatter, given data, elements will be separated given by per_page kwargs. In this case, each element is separated as a single page.
  2. MenuPages accepts any class that subclasses menus.PageSource. This includes ListPageSource(our MySource).
  3. MenuPages adds all the necessary reactions for navigation.
  4. everytime MenuPages receives a reaction, it processes them on which way to go. After that, it calls format_page after it processed on which page to show.
  5. When format_page is called. menu would be MenuPages instance and entries will be the values that were separated given by per_page from data.
  6. Anything that is returned in format_page are displayed onto your Message. The value that can be returned are Embed/dict/str.

Output

menupages

Custom MenuPages

If you want to customize the reaction. You would need to override MenuPages class. As we know, MenuPages is the class that handles the reaction. We can fully change this to fit our needs.

Example

import discord
from discord.ext import menus
from discord.ext.menus import button, First, Last
class MyMenuPages(menus.MenuPages, inherit_buttons=False):
    @button('<:before_fast_check:754948796139569224>', position=First(0))
    async def go_to_first_page(self, payload):
        await self.show_page(0)

    @button('<:before_check:754948796487565332>', position=First(1))
    async def go_to_previous_page(self, payload):
        await self.show_checked_page(self.current_page - 1)

    @button('<:next_check:754948796361736213>', position=Last(1))
    async def go_to_next_page(self, payload):
        await self.show_checked_page(self.current_page + 1)

    @button('<:next_fast_check:754948796391227442>', position=Last(2))
    async def go_to_last_page(self, payload):
        max_pages = self._source.get_max_pages()
        last_page = max(max_pages - 1, 0)
        await self.show_page(last_page)

    @button('<:stop_check:754948796365930517>', position=Last(0))
    async def stop_pages(self, payload):
        self.stop()


class MySource(menus.ListPageSource):
    async def format_page(self, menu, entries):
        embed = discord.Embed(
            description=f"This is number {entries}.", 
            color=discord.Colour.random()
        )
        embed.set_footer(text=f"Requested by {menu.ctx.author}")
        return embed

    
@bot.command()
async def your_command(ctx):
    data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    formatter = MySource(data, per_page=1)
    menu = MyMenuPages(formatter)
    await menu.start(ctx)

Explanation

  1. Setting inherit_buttons kwargs to False removes buttons that came from MenuPages.
  2. in each button decorator, the first argument would be your emoji. Followed by position kwargs. position kwargs accepts Position, First, Last class.
  3. First refers to your position of button. This class inherits Position. First acts as an anchor, where it will always be added before Last class gets added. In this case, the position of buttons would be;
    • First(0), First(1), Last(0), Last(1), Last(2)
  4. show_page method sets the current_page to your page. In this case, it is used in go_to_first_page where we only wanna show the first page, and go_to_last_page where we only want to show the last page.
  5. show_checked_page method uses show_page, but checks if it is within the range of your page. This ensure that there isn't an out of bound exception.
  6. format_page this time returns an embed, with a randomized color to make it pretty.
  7. We would use MyMenuPages instead of MenuPages because we want to try the custom emoji.
  8. The rest of the explanation are available at Menu

As a reference, here are the emojis I'm using currently. emojis

Output

custom_menupages

Discussion

As you can see, it is relatively easy to create your own pagination with the default MenuPages. The complexity increases as you want to start doing customization, however this can be easy once you get the hang of it.

It is recommended for you to explore other classes for other use cases. However, I'm not gonna be focusing them here.

Now that we have the fundamentals of menus. We can also construct them using View on our own ways.

View

[Button based pagination]

This tutorial is only compatible with discord.py 2.0. Any lower such as discord.py 1.7 will not have this class because it was not built with the newer feature of discord such as buttons and dropdowns.

You would need to install discord.py 2.0 via git instead of pypi. 2.0 were never released thus not available in pypi.

Brief explanation of View

This class is responsible for handling classes that inherits from discord.ui.Item. This includes discord.ui.Button which we will be using in this tutorial as navigation for the pagination.

Classes relating to View

Here are a list of classes that is related to View.

  1. discord.ui.View
  2. discord.ui.Item
  3. discord.ui.Button
  4. discord.ui.Select

However, we will only talk about 2 here which is discord.ui.View and discord.ui.Button. Those are the only thing we need for the pagination.

To start, discord.ui.View works similarly like menus.Menu. To demonstrate, here's an example.

import discord
from discord import ui
class MyView(ui.View):
    @ui.button(label="Hello", emoji="\U0001f590", style=discord.ButtonStyle.blurple)
    async def on_click_hello(self, button, interaction):
        await interaction.response.send_message("Hi")

@bot.command()
async def your_command(ctx):
    view = MyView()
    await ctx.send("Click button", view=view)

Explanation

  1. ui.button is a decorator that will create a Button. The parameter are almost the same as discord.ui.Button.
  2. The said decorator will call the callback when clicked which in this case refers to on_click_hello method. It will pass buttonwhich is discord.ui.Button and interaction which is discord.Interaction as the parameter.
  3. interaction.response returns InteractionResponse instance, which you can use to respond to the user. We will use send_message for now to send a message.
  4. MyView is instantiated
  5. abc.Messageable.send contains view kwargs which you can pass your View instance into. In our case, view is the instance of MyView.

Output

view_button

Pagination with View

With this brief knowledge, we can now make pagination with View. As we know, MenuPages acts as the navigation of the pagination while ListPageSource acts as the formatter which format the data and the page. While menus.Menu handles reactions and menus.MenuPages contains everything about pagination handling that we really needed. We only need the handling part.

Based on this knowledge alone, we can combine MenuPages with discord.ui.View to make a fully functioning paginator with discord.ui.Button as the navigation. This code will be similar to Custom MenuPages code.

Example

import discord
from discord import ui
from discord.ext import menus

class MyMenuPages(ui.View, menus.MenuPages):
    def __init__(self, source):
        super().__init__(timeout=60)
        self._source = source
        self.current_page = 0
        self.ctx = None
        self.message = None

    async def start(self, ctx, *, channel=None, wait=False):
        # We wont be using wait/channel, you can implement them yourself. This is to match the MenuPages signature.
        await self._source._prepare_once()
        self.ctx = ctx
        self.message = await self.send_initial_message(ctx, ctx.channel)

    async def _get_kwargs_from_page(self, page):
        """This method calls ListPageSource.format_page class"""
        value = await super()._get_kwargs_from_page(page)
        if 'view' not in value:
            value.update({'view': self})
        return value

    async def interaction_check(self, interaction):
        """Only allow the author that invoke the command to be able to use the interaction"""
        return interaction.user == self.ctx.author

    # This is extremely similar to Custom MenuPages(I will not explain these)
    @ui.button(emoji='<:before_fast_check:754948796139569224>', style=discord.ButtonStyle.blurple)
    async def first_page(self, button, interaction):
        await self.show_page(0)

    @ui.button(emoji='<:before_check:754948796487565332>', style=discord.ButtonStyle.blurple)
    async def before_page(self, button, interaction):
        await self.show_checked_page(self.current_page - 1)

    @ui.button(emoji='<:stop_check:754948796365930517>', style=discord.ButtonStyle.blurple)
    async def stop_page(self, button, interaction):
        self.stop()

    @ui.button(emoji='<:next_check:754948796361736213>', style=discord.ButtonStyle.blurple)
    async def next_page(self, button, interaction):
        await self.show_checked_page(self.current_page + 1)

    @ui.button(emoji='<:next_fast_check:754948796391227442>', style=discord.ButtonStyle.blurple)
    async def last_page(self, button, interaction):
        await self.show_page(self._source.get_max_pages() - 1)


@bot.command()
async def your_command(ctx):
    data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    formatter = MySource(data, per_page=1) # MySource came from Custom MenuPages subtopic. [Please refer to that]
    menu = MyMenuPages(formatter)
    await menu.start(ctx)

Jesus christ that's a long code

Explanation

  1. We inherit both ui.View and menus.MenuPages. Where we will borrow methods from menus.MenuPages while using ui.View as our main navigation for the pagination.
  2. In __init__, the super().__init__ will refer to ui.View instead. To understand why this is, learn Method Resolution Order(MRO). source argument must be a PageSource instance. The rest of the attribute are required to be assign because MenuPages will use them.
  3. In start method, it will follow the MenuPages method signature. self.send_initial_message is a method from MenuPages, it acts as sending a message to the user and returns a Message object. We will store them in self.message
  4. self._source._prepare_once is a method to declare that the PageSource object has started.
  5. _get_kwargs_from_page method is also from MenuPages, it is responsible for calling format_page and returns a dictionary which will directly be used in discord.Message.edit kwargs. We take advantage of this and put view as the to put our View object into the message for navigation.
  6. interaction_check is a method of ui.View. It gets called when a button is clicked. Return True to allow for callback to be called. We will use interaction.user where it's the person who clicked the button to check if they are our author. If it is, it's True else return False. Optionally, you would send an ephemaral message to the user if it is not the author.
  7. The rest of the explanation must refer to Custom MenuPages subtopic. It is exactly the same explanation.

Output

view_pagination

Discussion

While yes it may seem long. But keep in mind, you would only do MyMenuPages once, after that, you can create infinite amount of ListPageSource that fit your need for all of the pagination you will ever need. Feel free to derive this code into a much more advance handler. I've only talked briefly on how to use ui.View. There's plenty more uses you can do with it and I would recommend exploring more on ui.View. Danny has created plenty of View examples here: Click Here.

The end

Congrats, you've reached the end of this tutorial. I don't recommend skipping any of the topic here because you will be left confused on what you've just read and learnt nothing. Each subtopic is linked to each other hence why. Anyways, any question regarding ui.View and discord-ext-menus can be asked in discord.py. I don't check this gist that much. So your question wont be answered that quickly.

Tbh i wrote this because i need to remind myself about this in about a year. that's all, thanks.

@bsod2528
Copy link

Thank you for the detailed explanation :D

@Nevvyboi
Copy link

Thank you very much!

@staciax
Copy link

staciax commented May 3, 2022

<3

@JuanQ23
Copy link

JuanQ23 commented Jun 16, 2022

async def on_click_hello(self, button, interaction):  #don't forget to flip button and interaction next year !

Awesome tutorial :)

@InterStella0
Copy link
Author

InterStella0 commented Jul 5, 2022

To point out minor outdated code, for View items, they no longer will automatically defer if you do not respond in the item callback. Therefore you would need to defer them yourself. The position of interaction and button has also changed as pointed out above.
There were an update a few months ago regarding that change.
You can fix it by an example shown below,

    @ui.button(emoji='<:before_fast_check:754948796139569224>', style=discord.ButtonStyle.blurple)
    async def first_page(self, interaction, button):
        await self.show_page(0)
        await interaction.response.defer()

"defer" is a way to acknowledge an interaction without responding with anything. Though this only applies to message components rather than slash commands. For more information, read here

You can also use interaction to edit it for you instead of a self.message.edit that was done internally to save an API call, however, I will leave it as a homework for you to solve it.

I would like to update the gist further for the new stuff but really don't have time to update this gist as I'm busy with my university and part-time work.

@delrius-euphoria
Copy link

Can you check if the images work?

@InterStella0
Copy link
Author

This is no longer maintained cos ion like this one

@anselal
Copy link

anselal commented Dec 16, 2022

I use the last example and I get "Interaction failed." How do I resolve this ?

@InterStella0
Copy link
Author

This is no longer maintained cos ion like this one

Hi, read several comments above regarding it.

@anselal
Copy link

anselal commented Dec 16, 2022

This is no longer maintained cos ion like this one

Hi, read several comments above regarding it.

Thnx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment