Closed RaymondLWong closed 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.
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()
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).
Thanks for your help @robertwayne ! I look forward to your next release and will try using your library for my bot.
It would be useful if I could setup the
Page
data myself when thenext()
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 currentEmbed
with new data. One way to do this is to implement our ownnext()
event handler.