Skip to content

Instantly share code, notes, and snippets.

@1oonie
Last active March 30, 2024 18:22
Show Gist options
  • Save 1oonie/500eafdad0aaf278b94c612764688976 to your computer and use it in GitHub Desktop.
Save 1oonie/500eafdad0aaf278b94c612764688976 to your computer and use it in GitHub Desktop.
Message components

Message components

This gist shows you how to use message components in discord.py 2.0

This assumes that you know how Object Orientated Programming in Python works, if you don't know what this is then I recommend that you read this guide.

Installing

You'll need to install discord.py from git to use this (if you don't have git then google how to install it for your OS, I can't be bothered to put it in here...), if you already have this then you can skip to the next section. However if you don't please read the below

# on windows
py -3 -m pip install git+https://github.com/rapptz/discord.py.git

# on linux/osx
python3 -m pip install git+https://github.com/rapptz/discord.py.git

look at the below table for what you want the values to be

Buttons

If you want have buttons in messages that your bot sends you have to pass the view kwarg to any .send function. This view kwarg has to be of type discord.ui.View or inherit from it. This way you can subclass discord.ui.View and add buttons from that e.g.

class MyView(discord.ui.View):
    @discord.ui.button(label='A button', style=discord.ButtonStyle.primary)
    async def button_callback(self, interaction, button):
        await interaction.response.send_message('Button was pressed', ephemeral=True)

# somewhere else...
view = MyView()
await something.send('Press the button!', view=view)

Note: if you want to override MyView's __init__ method you will need to add

super().__init__()

to the code in the __init__ method otherwise the View will not work!!

Button styles can be one of the following:

image

Buttons can also have emojis at the start, you can use the emoji keyword argument in the discord.ui.button decorator to set this. As well as this buttons can be disabled with the enabled kwarg and you can also disable a button when pressed like this:

class MyView(discord.ui.View):
    @discord.ui.button(label='A button', style=discord.ButtonStyle.primary)
    async def button_callback(self, interaction, button):
        button.disabled = True
        button.label = 'No more pressing!'
        await interaction.response.edit_message(view=self)

# somewhere else...
view = MyView()
await something.send('Press the button!', view=view)

This looks like this:

button_disable

URL Buttons

If you want a URL button then you can simply do this (note that you MUST have the style set to discord.ButtonStyle.url and URL buttons cannot have a callback since they are handled at the client - this also means that URL buttons don't have a custom id as well) e.g.

view = discord.ui.View()
view.add_item(discord.ui.Button(label='Go to website', url='https://example.com/', style=discord.ButtonStyle.url))
await something.send('Press the button!', view=view)

URL buttons look like this:

image

Subclassing discord.ui.Button

You can also just create an instance of discord.ui.View and call add_item passing a discord.ui.Button object to it (this can also be a subclass of discord.ui.Button )e.g.

class MyButton(discord.ui.Button):
    def __init__(self):
        super().__init__(label='A button', style=discord.ButtonStyle.primary)

    async def callback(self, interaction):
        await interaction.response.send_message('Button was pressed', ephemeral=True)

# somewhere else...
view = discord.ui.View()
view.add_item(MyButton())
await something.send('Press the button!', view=view)

Select menus

You can have select menus in views as well, for this you can use the discord.ui.select decorator (but the recommended way is subclassing it because it looks easier on the eye). You access the selected values through select.values e.g.

class MyView(discord.ui.View):
    @discord.ui.select(placeholder='Pick your colour', min_values=1, max_values=1, options=[
        discord.SelectOption(label='Red', description='Your favourite colour is red', emoji='🟥'),
        discord.SelectOption(label='Green', description='Your favourite colour is green', emoji='🟩'),
        discord.SelectOption(label='Blue', description='Your favourite colour is blue', emoji='🟦')
    ])
    async def select_callback(self, intersction, select):
        await interaction.response.send_message(f'Your favourite colour is {select.values[0]}', ephemeral=True)
    
# Somewhere else...
view = MyView()
await something.send('What is your favourite colour?', view=view)

Select menus look like this:

selects

Subclassing discord.ui.Select

Like discord.ui.Button you can also subclass discord.ui.Select e.g.

class MySelect(discord.ui.Select):
    def __init__(self):
        options = [
            discord.SelectOption(label='Red', description='Your favourite colour is red', emoji='🟥'),
            discord.SelectOption(label='Green', description='Your favourite colour is green', emoji='🟩'),
            discord.SelectOption(label='Blue', description='Your favourite colour is blue', emoji='🟦')
        ]
        super().__init__(placeholder='Pick your colour', min_values=1, max_values=1, options=options)
     
     async def callback(self, interaction):
        await interaction.response.send_message(f'Your favourite colour is {self.values[0]}', ephemeral=True)
        
# somewhere else...
view = discord.ui.View()
view.add_item(MySelect())
await something.send('What is your favourite colour?', view=view)

Context in views

Another common thing to do is to pass a Context object to the view e.g.

class MyView(discord.ui.View):
    def __init__(self, ctx):
        self.context = ctx
        super().__init__()
        
    ...

# somewhere else...
view = MyView(ctx)

then you can override the interaction_check method of MyView like so

    async def interaction_check(self, interaction):
        if interaction.user != self.context.author:
            return False
        return True

to only allow the invoker of the command to use the view.

Timeouts

You can add timeouts to views meaning that they will expire and you will not be able to interact with them after x seconds. When the timeout expires the on_timeout method is called e.g.

class MyView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=60)
        
    async def on_timeout(self):
        for child in self.children:
            child.disabled = True
        await self.message.edit(view=self)
    
    ...
    
# somewhere else...
view = MyView()
view.message = await something.send(..., view=view)

Error handling

If an error happens in a button/select callback then by default, it will not be handled and an "interaction failed" message will appear to the user. If you want to customize this then you can use the on_error method that gets called whenever something fails within the view e.g.

import traceback

class MyView(discord.ui.View):
    @discord.ui.button(label='A button', style=discord.ButtonStyle.primary)
    async def button_callback(self, interaction, button):
        raise RuntimeError('Exception was raised')
    
    async def on_error(self, error, item, interaction):
        await interaction.response.defer()
        exception = '\n'.join(traceback.format_exception(type(error), error, error.__traceback__))
        exception = f'```py\n{exception}```'
        await interaction.channel.send(f'An error occured in the button `{item.custom_id}`:\n{exception}')

# somewhere else...
view = MyView()
await ctx.send('Press the button!', view=view)

This looks like this which I personally think looks better than the normal "interaction has failed" message since it is more verbose and tells the user what exactly has gone wrong (not that they really need to know but it is still nice to know how to do this):

error_handling

Limits

A message can have up to five "action rows" and each of these "action" rows have five slots where you can put message components. A button takes up one of these slots but a select menus takes up all five slots of a "action row". Keep this in mine when creating your views since you don't want to run out of space!

A note

You must respond to an interaction within 3 seconds or the interaction token will be invalidated (this can be simply deferring it). You then have 15 minutes to send any follow-up messages (using the interaction.followup webhook) since that is how long the interaction token lasts for once you have responded the first time. Also, pycord automatically defers the interaction for you if you have not responded yourself in the component callback so make sure not to spend too long sleeping/doing stuff in it!

image

@InsightOS
Copy link

Excellent article!

@1oonie
Copy link
Author

1oonie commented Nov 22, 2021

Excellent article!

thanks!

@PyMohsen
Copy link

PyMohsen commented Dec 3, 2021

Thanks 😄

@SoulFire2879
Copy link

pog

@nicholasyoannou
Copy link

Thanks for this. An article that actually explains what stuff does.
Been using the reactionmenu module for a little while, I need to add some functionality that goes outside the module (using Pycord directly) and this article helped me find out how, so thanks!

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