Search code examples
discorddiscord.pydiscord-buttons

How do I make dynamically persistant buttons?


I'm trying to make dynamically persistent buttons and am not sure how to go about it. I'm making a polls bot. What I want to happen is if the bot resets, I want the buttons on the polls to still work. I tried looking at some tutorials and the discord.py documentation but couldn't find anything helpful or relevant.

Below is my code:

import discord
from discord.ext import commands
from discord import app_commands

#My button class is a call for when someone presses a button
class MyButton(discord.ui.Button):
    async def callback(self, interaction: discord.Interaction):
        #Setting vars for the class.
        button_id = self.custom_id
        user_id = interaction.user.id
        timestamp = interaction.created_at
        message_id = interaction.message.id  # Access the message ID #getting message id
        print(f"Received button click: {button_id}") #Print for the msg ID
        #The response message to clicking a button
        await interaction.response.send_message(f"You clicked {self.label}")

#the My View class for 
class MyView(discord.ui.View):
    def __init__(self, options_and_ids): #The options_and_ids variable is a list thats ziped from the polls class
        super().__init__(timeout=None)
        
        #dynamically creating the Buttons for the message depending in the list.
        for option, button_id in options_and_ids:
            button = MyButton(label=option, custom_id=button_id)
            self.add_item(button)

class polls(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot

    #setup hook for non-dynamic buttons that I tried to use.
    async def setup_hook(self) -> None:
        self.add_view(MyView())

    @app_commands.command(name = "poll", description="Make a poll.")
    @app_commands.describe(question = "The question you want to ask for your poll.")
    @app_commands.describe(choices = 'Add choices with commas. ex. "One, Two, Three". (MAX IS 20)
                                      Put commas within a choice using a double coma ",,".')
    async def create_poll(self, interaction: discord.Interaction, question: str, choices: str)
        #function for putting string into a list separated by single commas. Returns a list of str.
        text_options = split_text(choices) 

        #Function that creates a 5 char id using Uppercase, lowercase, and digits.
        #    Returns a list of IDs the same size as the choices.
        id_list = make_id(text_options)

        #pairs text_options and id_list into a new list
        names_and_ids = list(zip(text_options, id_list))
        view = MyView(names_and_ids) #calls the MyView class sending over the list of buttons and their IDs.
        await channel.send(embed=embed, view=view)

async def setup(bot:commands.Bot) -> None:
    await bot.add_cog(polls(bot))

[How to make persistant buttons in discord.py]

I tried to use this to help me; though, I couldn't figure out how to use their example to make dynamic persistent buttons. I also thought about saving the button IDs in a database and then loading them in an on_ready event but I couldn't find the method to do this so I was out of luck with that. Thank you so much for your time. Hopefully you can help me!


Solution

  • How to store persistent buttons in discord.py

    Hi all, after some tinkering and looking at how Discord stores persistent buttons I figured it out.

    Storing dynamic persistent button in discord.py is fairly simple; though, it requires you to use a database to store the button_name and button_id.

    I also did this in a cog so this will vary depending on how you make your bot... Here's how I did it...

    Saving buttons to the DB:

    def save_buttons(names_and_id): #touple list [(name1,id1),(name2,id2),...] will always be the same amount of each
        conn = sqlite3.connect('your_SQLite.db')
        cursor = conn.cursor()
        for name, id in names_and_id:
            cursor.execute('INSERT INTO buttons (button_name, button_id) VALUES (?, ?)', (name,id))
            conn.commit()
    

    Fetching buttons from the DB:

    def get_buttons():
        conn = sqlite3.connect('criticalbot.db')
        cursor = conn.cursor()
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS buttons (
            button_name TEXT,
            button_id TEXT PRIMARY KEY)
        ''')
        conn.commit()
    
        #getting all buttons from the db. Will conveniently be the same format as the touple that I use to make the View when making buttons.
        cursor.execute('SELECT * FROM buttons')
        names_and_ids = cursor.fetchall()
        if names_and_ids == None:
            return None
    
        return names_and_ids
    

    Now I didn't have to do much to my original code to make it work...

    The MyButton class stays the same except getting rid of unneeded bloat:

    #My button class is a call for when someone presses a button
    class MyButton(discord.ui.Button):
        async def callback(self, interaction: discord.Interaction):
            #Setting vars for the class.
            button_id = self.custom_id
            print(f"Received button click: {button_id}") #Print for the msg ID
            #The response message to clicking a button
            await interaction.response.send_message(f"You clicked {self.label}")
    

    Here is the MyView class where the dynamic buttons view will be made as well as what we call to make the buttons to be persistent.

    #the My View class for creating the view
    class MyView(discord.ui.View):
        def __init__(self, options_and_ids): #The options_and_ids variable is a list thats ziped from the polls class
            super().__init__(timeout=None)
            
            #dynamically creating the Buttons for the message depending in the list.
            for option, button_id in options_and_ids:
                button = MyButton(label=option, custom_id=button_id)
                self.add_item(button)
    

    This is where we can make the call when the bot goes online to make the buttons persistent. What's nice about this is because its a cog class, the vars will be set before the bot goes completely online so it will auto-run our add_view() method before the bot starts.

    
    class polls(commands.Cog):
        def __init__(self, bot: commands.Bot):
            self.bot = bot
            
            try: #We use this in case there's nothing in the DB
                self.bot.add_view(MyView(get_buttons())) #THIS IS WHAT YOU NEED TO MAKE THE BUTTONS PERSISTANT
            except: #                         ^This is where we call the get buttons function to get the touple we need to make the dynamic persistent view.
                pass 
    
        #REPLACEING THIS WITH WHATS ABOVE
        #setup hook for non-dynamic buttons that I tried to use.
        #async def setup_hook(self) -> None:
        #    self.add_view(MyView())
    
    

    Here is my main app_command that i use

        @app_commands.command(name = "poll", description="Make a poll.")
        @app_commands.describe(question = "The question you want to ask for your poll.")
        @app_commands.describe(choices = 'Add choices with commas. ex. "One, Two, Three". (MAX IS 20)
                                          Put commas within a choice using a double coma ",,".')
        async def create_poll(self, interaction: discord.Interaction, question: str, choices: str)
            #function for putting string into a list separated by single commas. Returns a list of str.
            text_options = split_text(choices) 
    
            #Function that creates a 5 char id using Uppercase, lowercase, and digits.
            #    Returns a list of IDs the same size as the choices.
            id_list = make_id(text_options)
    
            #pairs text_options and id_list into a new list
            names_and_ids = list(zip(text_options, id_list))
            save_buttons(names_and_ids) #<--- this is where we save the commands when we create new dynamic buttons when a pole is made.
    
            view = MyView(names_and_ids) #calls the MyView class sending over the list of buttons and their IDs.
            await channel.send("", view=view)
    
    async def setup(bot:commands.Bot) -> None:
        await bot.add_cog(polls(bot))