Creating a Discord.py bot function that returns True/False based on button clicks

I’m working on a Discord bot and need to create a verification system for certain commands. I want to build something like this:

@tree.command(name='verify')
async def verifyCommand(interaction: discord.Interaction):
    if await getUserConfirmation(interaction, 'proceed with this action'):
        # Execute the command

For this to work, my getUserConfirmation() function needs to give back either True or False based on what button the user clicks.

I tried making a button interface like this:

async def getUserConfirmation(interaction: discord.Interaction, message: str):

    class YesNoButtons(discord.ui.View):

        @discord.ui.button(label = 'Yes', style = discord.ButtonStyle.green)
        async def confirmYes(self, interaction: discord.Interaction, button: discord.ui.Button):
            return()
        
        @discord.ui.button(label = 'No', style = discord.ButtonStyle.red)
        async def confirmNo(self, interaction: discord.Interaction, button: discord.ui.Button):
            return()
        
        
    await interaction.response.send_message(
        embed = discord.Embed(title = 'Do you want to ' + message),
        view = YesNoButtons()
    )

The problem is I can’t figure out how to make the main function return a value when someone clicks confirmYes() or confirmNo(). How can I pass the boolean result back to the calling function? Is there a different approach that would work better for this kind of user interaction?

Button callbacks and async functions don’t naturally return values to the parent function. You need a way to capture the user’s choice and wait for it. I use asyncio.Event objects to handle the sync between button clicks and your main function.

Here’s how to modify your approach:

import asyncio

async def getUserConfirmation(interaction: discord.Interaction, message: str):
    result = None
    event = asyncio.Event()
    
    class YesNoButtons(discord.ui.View):
        def __init__(self):
            super().__init__(timeout=30)
        
        @discord.ui.button(label='Yes', style=discord.ButtonStyle.green)
        async def confirmYes(self, button_interaction: discord.Interaction, button: discord.ui.Button):
            nonlocal result
            result = True
            await button_interaction.response.send_message("Confirmed!", ephemeral=True)
            event.set()
        
        @discord.ui.button(label='No', style=discord.ButtonStyle.red)
        async def confirmNo(self, button_interaction: discord.Interaction, button: discord.ui.Button):
            nonlocal result
            result = False
            await button_interaction.response.send_message("Cancelled!", ephemeral=True)
            event.set()
    
    await interaction.response.send_message(
        embed=discord.Embed(title='Do you want to ' + message),
        view=YesNoButtons()
    )
    
    await event.wait()
    return result

Use nonlocal to modify the result variable from within the button callbacks, and asyncio.Event to make your function wait until a button gets clicked before returning the value.

Everyone’s missing user validation. Your setup lets anyone click those buttons - not just whoever ran the command. Hit this same issue building mod tools where random users kept clicking admin verification buttons.

Add this check in your button callbacks:

if button_interaction.user != interaction.user:
    await button_interaction.response.send_message("This confirmation isn't for you.", ephemeral=True)
    return

Also add a timeout handler to your View class. When users don’t respond within the timeout, return False instead of None. Override on_timeout and set your result value before calling stop(). Otherwise your verification might hang or throw errors when the view expires.

honestly, just use wait_for() instead of events. set a custom_id on your buttons and use bot.wait_for(‘interaction’) to catch responses. way less code than asyncio events and you don’t need nonlocal variables. i’ve been doing this for years - works every time.

The Problem: Your Flask app and Discord bot are failing to run concurrently on Heroku because you’re attempting to manage them within a single dyno using a combination of threading and asyncio. This approach creates race conditions during startup, leading to one service preventing the other from initializing correctly. Heroku’s dynos expect a single process; your multi-threaded approach violates this expectation.

TL;DR: The Quick Fix: The most reliable solution is to run your Flask app and Discord bot on separate Heroku dynos. This eliminates the threading conflicts and aligns with Heroku’s architecture. Update your Procfile and deploy your application to Heroku, scaling your dynos accordingly.

:thinking: Understanding the “Why” (The Root Cause):

Heroku’s web dynos are designed for single-threaded applications. When deploying, the Heroku router tries to connect to your Flask server on the port specified by the PORT environment variable. However, because your runner.py uses threading to start both Flask and the Discord bot, and the bot’s asyncio event loop runs indefinitely, it blocks the main thread. This prevents Flask from properly binding to the port before the dyno’s 60-second timeout expires, causing the deployment to fail. While this might work locally, Heroku’s process management makes it unsuitable.

:gear: Step-by-Step Guide:

Step 1: Separate Your Services into Different Dynos. This is the most reliable solution.

Step 2: Update Your Procfile. Modify your Procfile to define separate processes:

web: gunicorn server:web_app
worker: python runner.py

Step 3: Modify server.py to Use the PORT Environment Variable. Ensure your Flask app uses the port from Heroku’s PORT environment variable:

import threading
import os
from flask import Flask

web_app = Flask(__name__)

@web_app.route('/')
def home():
    return 'Server is running'

def start_web_server():
    port = int(os.environ.get('PORT', 5000)) # Use Heroku's PORT or default to 5000
    web_app.run(host='0.0.0.0', port=port)

def launch_async():
    web_thread = threading.Thread(target=start_web_server)
    web_thread.daemon = True
    web_thread.start()

Step 4: Deploy and Scale Dynos. Deploy the updated application. Then, scale your worker dyno using heroku ps:scale worker=1.

:mag: Common Pitfalls & What to Check Next:

  • Dyno Startup Timeouts: Heroku dynos have a limited startup time. If your Discord bot takes too long to initialize, it might still cause issues. Optimize your bot’s startup process.
  • Resource Limits: Monitor your dyno usage. High resource consumption might lead to dyno termination. Optimize your code and consider scaling your dynos.
  • Heroku Logging: Check the Heroku logs to debug problems during deployment and runtime.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

The Problem: Your Discord bot’s getUserConfirmation function isn’t correctly returning the boolean result from the button interactions. The button callbacks are asynchronous, and you’re not properly handling the asynchronous nature of the interactions to get the result back to the main function.

TL;DR: The Quick Fix: Use a custom discord.ui.View class to store the result of the button interaction and use the await view.wait() method to get the result. This simplifies the process and avoids the complexities of asyncio.Event or manual synchronization.

:thinking: Understanding the “Why” (The Root Cause):

Button callbacks in discord.py are asynchronous functions. They execute independently of the main getUserConfirmation function. The return() statement within the callback functions doesn’t return a value to the calling function; it only returns from the callback itself. To get the boolean value back, you need a mechanism to synchronize the button click event with the main function’s execution. Simply using a return statement inside the asynchronous button callback function will not work. You need a way to signal back to the main function when the button is pressed and what the value is.

:gear: Step-by-Step Guide:

Step 1: Create a Custom View Class:

Create a custom discord.ui.View class that stores the result of the button interaction. This will act as a container to hold the result before it’s returned.

import discord

async def getUserConfirmation(interaction: discord.Interaction, message: str):
    class ConfirmationView(discord.ui.View):
        def __init__(self):
            super().__init__(timeout=60) # Set a timeout to prevent indefinite waiting
            self.value = None

        @discord.ui.button(label='Yes', style=discord.ButtonStyle.green)
        async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
            await interaction.response.send_message('Confirmed', ephemeral=True)
            self.value = True
            self.stop()

        @discord.ui.button(label='No', style=discord.ButtonStyle.red)
        async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
            await interaction.response.send_message('Cancelled', ephemeral=True)
            self.value = False
            self.stop()

        async def on_timeout(self):
            self.value = False # Default to False if timeout occurs
            self.stop()

    view = ConfirmationView()
    await interaction.response.send_message(
        embed=discord.Embed(title='Do you want to ' + message),
        view=view
    )

    await view.wait()
    return view.value

Step 2: Use await view.wait():

The await view.wait() method suspends execution of the getUserConfirmation function until either the ‘Yes’ or ‘No’ button is clicked or the timeout is reached. This ensures the function waits for the user’s response before returning a value. The on_timeout method is overridden to handle the case where no button is pressed before the timeout.

Step 3: Return the Value:

The view.value attribute now holds the boolean result (True or False), which is returned by the getUserConfirmation function.

:mag: Common Pitfalls & What to Check Next:

  • User Validation: Ensure that only the user who initiated the interaction can respond to the buttons. You can add a check within the button callbacks to verify this: if button_interaction.user != interaction.user:
  • Timeout Handling: The timeout value (currently set to 60 seconds) might need adjusting depending on your application’s needs.
  • Error Handling: Consider adding error handling for potential exceptions that might occur during the interaction or database operations. Log any errors encountered.
  • Ephemeral Messages: The example uses ephemeral=True to send confirmation messages only to the user who clicked the button, improving the user experience.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.