robertwayne / dpymenus

Simplified menus for discord.py developers.
https://dpymenus.com/
MIT License
26 stars 4 forks source link

Allow implementing on_next() and handling data ourselves with the ButtonMenu #24

Closed RaymondLWong closed 4 years ago

RaymondLWong commented 4 years ago

It would be useful if I could setup the Page data myself when the next() function is called. If my data comes from a database or other paginated source (such as a REST API), I want to be able to update the current Embed with new data. One way to do this is to implement our own next() event handler.

robertwayne commented 4 years ago

Assuming I'm understanding correctly, you can do this already. On your Page object, just do something like:

page1 = Page(title='Example', description='example')
page1.on_next(update_data)
page1.buttons([btn1, btn2])

async def update_data(menu: ButtonMenu):
    if menu.button_pressed(btn1):
        e = discord.Embed()
        await menu.output.edit(embed=e)

Each menu has access to an output attr which is the current pages discord Message object. You can call any normal methods on that like delete, edit, etc.

I'm writing this on my phone so I haven't tested it, but that should work. Obviously the buttons would remain the same as the page isn't changing, just the embed data, but it sounds like you just needed a button to update that page anyway.

EDIT: Just to add more info -- the next event on a button menu is always called after it receives a valid reaction (ie. passes all predicate checks). So any callable you pass into a specific Pages' on_next method will run on each button press.

You can perform all your logic inside that method and dictate how buttons and pages will react based on anything you want then; technically you don't even need to check if a specific button was pressed to perform an action.

RaymondLWong commented 4 years ago

Unfortunately, I get an error when pressing the button. It looks like you might be trying to serialise something?

Traceback (most recent call last):
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\ext\commands\core.py", line 85, in wrapped
    ret = await coro(*args, **kwargs)
  File "C:/Users/USERNAME/PycharmProjects/PROJECT_NAME/src/main.py", line 151, in test_me
    await menu.open()
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\site-packages\dpymenus\button_menu.py", line 66, in open
    await self.page.on_next_event(self)
  File "C:/Users/USERNAME/PycharmProjects/PROJECT_NAME/src/main.py", line 146, in update_data
    await menu.output.edit(embed=get_page(2))
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\message.py", line 886, in edit
    data = await self._state.http.edit_message(self.channel.id, self.id, **fields)
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\http.py", line 156, in request
    kwargs['data'] = utils.to_json(kwargs.pop('json'))
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\utils.py", line 318, in to_json
    return json.dumps(obj, separators=(',', ':'), ensure_ascii=True)
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\json\__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\json\encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\json\encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "C:\Users\USERNAME\AppData\Local\Programs\Python\Python37\lib\json\encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type function is not JSON serializable

My code:

@bot.command()
async def test_me(context: Context):
    forward = '⏩'
    reverse = '⏪'

    def get_empty_page() -> Page:
        page1 = Page(title=f'Page {page}', description='example')
        page1.on_next(update_data)
        page1.buttons([reverse, forward])
        return page1

    def get_page(page: int) -> discord.Embed:
        data = req_to_db(page)

        page1 = Page(title='Example', description='example')
        page1.on_next(update_data)
        page1.buttons([reverse, forward])

        page1.add_field(name="Name", value=data)

        return page1

    async def update_data(menu: ButtonMenu):
        if menu.button_pressed(forward):
            await menu.output.edit(embed=get_page(2))
        elif menu.button_pressed(reverse):
            await menu.output.edit(embed=get_page(1))

    menu = ButtonMenu(context).add_pages([get_empty_page(), get_empty_page()])
    await menu.open()
robertwayne commented 4 years ago

EDIT: If you want to pass in a Page as an Embed you need to do .as_safe_embed() on it. I updated the below example to showcase that. This will remove the serialization error, as it's trying to serialize the Callable events attached to a Page object. That method will strip it of any non-standard discord Embed data.

I whipped up a basic example based on your initial inquiry that I just tested and works as expected:

@bot.command()
async def test(self, ctx):
    reload = '🔄'
    close = '❌'

    async def make_request():
        """We will fake a web API request and return JSON data."""
        return {'mock': 'json', 'web': 'request', 'random_data': randint(1, 100)}

    async def update_data(menu: ButtonMenu):
        if menu.button_pressed(reload):
            response = await make_request()

            p = Page(title='Awesome Data', description='We can reload this data.')
            p.add_field(name='Random Updating Integer', value=response.get('random_data'))
            await menu.output.edit(embed=p.as_safe_embed())

        elif menu.button_pressed(close):
            await menu.close()

    page1 = Page(title='Example', description='example')
    page1.on_next(update_data)
    page1.buttons([reload, close])

    menu = ButtonMenu(ctx).add_pages([page1, Page()])
    await menu.open()

It's a little janky because a menu doesn't expect to only have a single page, so it will require a 'dummy' page (can be empty, as seen above) to pad it. This is something I will change in the next release, though. In addition, I will add the ability for buttons to remain static (ie. prevent them from refreshing if the page did not change, but the embed data did).

RaymondLWong commented 4 years ago

Thanks for your help @robertwayne ! I look forward to your next release and will try using your library for my bot.