pythondiscorddiscord.pypersistence

Making view persistent in order to keep buttons working after bot restart


I'm new to coding and I'm trying to make a role-assignment bot using discord.py. The idea is to have the bot send a message that includes an embed and buttons which can be clicked to toggle a role off and on, respectively.

I'm aware that buttons need to be encased in a view, and as such, I've declared a view that's fully functional so far. The only issue is that after the bot is restarted, the code no longer works and pressing any of the buttons returns the typical "This interaction failed" error from Discord's end.

I've tried adding persistence to the view by following a few tutorials and answers I found, but I'm having trouble doing so, possibly due to my formatting. Here's the code I've got so far. I've removed some redundant code and ID information for clarity.

intents: Intents = Intents.default()
intents.message_content = True
client: Client = Client(intents=intents)
tree = discord.app_commands.CommandTree(client)

badcommand_embed = discord.Embed(color=0xfffb00, title='Invalid command',
                                    type='rich', description='You cannot use this command here.')

class GameRoleButtons(discord.ui.View):
    def __init__(self) -> None:
        super().__init__(timeout=None)

    @discord.ui.button(label='Elden Ring', style=discord.ButtonStyle.secondary, emoji=discord.PartialEmoji(name='EldenRing', id='[EMOJI_ID_HERE]'), custom_id='er_btn')
    async def eldenring_button(self, interaction: discord.Interaction, button: discord.ui.Button):
        if interaction.guild.get_role([ROLE_ID_HERE]) not in interaction.user.roles:
            await interaction.user.add_roles(interaction.guild.get_role([ROLE_ID_HERE]))
            await interaction.response.send_message('The <:EldenRing:[EMOJI_ID_HERE]> <@&[ROLE_ID_HERE]> role has been assigned to you.', ephemeral=True)
        else:
            await interaction.user.remove_roles(interaction.guild.get_role([ROLE_ID_HERE]))
            await interaction.response.send_message('The <:EldenRing:[EMOJI_ID_HERE]> <@&[ROLE_ID_HERE]> role has been removed from you.', ephemeral=True)
    @discord.ui.button(label='Remove game roles', style=discord.ButtonStyle.danger, emoji='✖', custom_id='removegameroles_btn')
    async def removegameroles_button(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.user.remove_roles(interaction.guild.get_role([ROLE_ID_HERE]))
        await interaction.response.send_message('Your game roles have been removed.', ephemeral=True)

async def send_message(message: Message, user_message: str) -> None:
    if not user_message:
        print('Message was empty as Intents were not enabled.')
        return

    is_private = user_message[0] == '$'

    if is_private:
        user_message = user_message[1:]

    try:
        response: str = get_response(user_message, message.author)
        await message.author.send(response) if is_private else await message.channel.send(response)

    except Exception as e:
        print(e)

@client.event
async def on_ready() -> None:
    await tree.sync(guild=discord.Object(id=[GUILD_ID_HERE]))
    print(f'{client.user} is now running.')

# Displays game roles.
@tree.command(name="gameroles", description="Displays the list of roles you can obtain.", guild=discord.Object(id=[GUILD_ID_HERE]))
async def gameroles_command(interaction: discord.Interaction):
    if interaction.channel_id == [CHANNEL_ID_HERE]:
        gameroles_embed = discord.Embed(color=0xfffb00, title='Select a game role',
                                        type='rich', description='Choose roles for the games that you play.')
        await interaction.channel.send(embed=gameroles_embed, view=GameRoleButtons())
    else:
        await interaction.channel.send(embed=badcommand_embed)

@client.event
async def on_message(message: Message) -> None:
    if message.author == client.user:
        return

    username: str = str(message.author)
    user_message: str = message.content
    channel: str = str(message.channel)
    guild: str = str(message.guild.name)

# Prints any messages from servers the bot is in in the console.
    print(f'[{guild} - #{channel}] {username}: {user_message}')

    await send_message(message, user_message)

def main() -> None:
    client.run(token=TOKEN)

if __name__ == '__main__':
    main()

I've been digging around with no luck. Could anyone explain what I'm missing or how I should go on about this? I apologize in advance if the code is too messy, I'm new to all of this.


Solution

  • One of the requirements for persistence of Views is to use the add_view command on the Views that you want to persist when the bot starts. You correctly set the timeout to None and added custom_id's for the views, but as per this documentation you also need to do client.add_view(GameRoleButtons()) right after the bot starts.