Skip to content

Instantly share code, notes, and snippets.

@Soheab
Last active January 13, 2024 11:57
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Soheab/f46fee27498aad4a8962d59b6f0415c6 to your computer and use it in GitHub Desktop.
Save Soheab/f46fee27498aad4a8962d59b6f0415c6 to your computer and use it in GitHub Desktop.
See how to wait for user input using a modal!

Wait for with a Modal

See here an overcomplicated way to wait for input from a user using a modal. Every step is explained using comments.

This is meant to replace Client.wait_for("message") for application commands.

Features

  • Easy way to construct a Modal with one text input field.
  • Easily pass a check from the constructor

Files

Feel free to mention me in the discord.py server at Soheab_#6240 (150665783268212746) for any questions.

# Importing everything we need...
# https://gist.github.com/Soheab/f46fee27498aad4a8962d59b6f0415c6
from __future__ import annotations
from typing import Any, Callable, Coroutine, Optional, Union, TYPE_CHECKING
from discord import Interaction, TextStyle
from discord.ui import Modal, TextInput
from discord.utils import maybe_coroutine
if TYPE_CHECKING:
from typing_extensions import Self
# Defining a class named SimpleModalWaitFor and subclassing Modal
class SimpleModalWaitFor(Modal):
# Overriding the __init__ method.
# This way we can set the title of the modal more easily
# to a custom value or "Waiting For Input" by default.
# Same goes for the timeout, it defaults to no timeout
# but we are setting it to 30 seconds.
# We are also adding an Optional check kwarg to the constructor
# which will be used to easily define a check for the modal
# that will be called in Modal.interaction_check.
# It's called before on_submit by the library.
# The check will take a function with two parameters,
# the class and interaction and optional async.
# We also added 6 keyword arguments to make it easier to set the text input field's values.
# - input_label: The label of the text input field. Defaults to "Input text".
# This is the label of the text input field, appears above the text input.
# - input_placeholder: The placeholder of the text input field. Defaults to nothing.
# This is the text that appears in the text input field when it is empty.
# - input_default: The default value of the text input field. Defaults to nothing.
# This is the value that will be set to the text input field when the modal is opened.
# the user can change it ofc.
# - input_max_lengte: The max length of the text input field. Defaults to 100.
# This limits how many characters the user can enter, discord will error if they try to enter more than this.
# - input_min_length: The min length of the text input field. Defaults to 5.
# This is how many characters the user must enter before they can submit the modal.
# - input_style: The style of the text input field, defaulting to TextStyle.short
# TextStyle.short = small text bar
# TextStyle.long = big text bar
def __init__(
self,
title: str = "Waiting For Input",
*,
# Adding the check attribute to the modal
# and typing as a optional async function that takes two parameters, the class and interaction
# and returns a boolean.
check: Optional[Callable[[Self, Interaction], Union[Coroutine[Any, Any, bool], bool]]] = None,
# timeout is an original kwarg that we are adding because we want to set a default value.
# and want to able set it from the constructor.
timeout: float = 30.0,
# Adding the text input modifiers.
input_label: str = "Input text",
input_max_length: int = 100,
input_min_length: int = 5,
input_style: TextStyle = TextStyle.short,
input_placeholder: Optional[str] = None,
input_default: Optional[str] = None,
):
# Passing our custom title or "Waiting For Input" if not provided and the timeout.
# custom_id is a unique identifier for discord, nothing important (it's not required).
super().__init__(title=title, timeout=timeout, custom_id="wait_for_modal")
# Set an attribute for the check function.
# So we can access it in our interaction_check method.
self._check: Optional[Callable[[Self, Interaction], Union[Coroutine[Any, Any, bool], bool]]] = check
# Set an attribute on the class to store the value of the text input field,
# we will set this to None by default
# because we don't know what the user will enter yet.
# We will set this to a string value when the user submits the modal in on_submit.
self.value: Optional[str] = None
# Set an attribute on the class to store the interaction from on_submit.
# This is so we can send a message to the user submitted the modal.
self.interaction: Optional[Interaction] = None
# Define our input field.
self.answer = TextInput(
label=input_label,
placeholder=input_placeholder,
max_length=input_max_length,
min_length=input_min_length,
style=input_style,
default=input_default,
# custom_id is a unique identifier for discord, nothing important (it's not required).
# we are using the modal's custom_id as a prefix and adding "_input_field" to it.
custom_id=self.custom_id + "_input_field",
)
# Add the input field to the modal.
self.add_item(self.answer)
# Define our interaction_check method.
# This method will be called when the modal is submitted.
async def interaction_check(self, interaction: Interaction) -> bool:
# If we have a check function, call it.
if self._check:
# Since the check can be optional async
# we need to use maybe_coroutine to make await it if it's a coroutine
# else just call it as a normal function.
allow = await maybe_coroutine(self._check, self, interaction)
return allow
# If we don't have a check function, return True (default).
return True
# Define our on_submit method.
# This is where we will get the value of the text input field.
async def on_submit(self, interaction: Interaction) -> None:
# Set our value attrbute to the value of the text input field.
self.value = self.answer.value
# Set our interaction attribute to the interaction.
self.interaction = interaction
# Stop the modal.
# This will allow us to get the value of the text input field
# in our command.
self.stop()
# Define our bot, not much to it.
# Importing everything we need...
from discord import Intents
from discord.ext import commands
bot = commands.Bot("$", intents=Intents(messages=True, guilds=True))
# Define our slash command.
# The command will wait for the user to enter a value in the modal.
# Since modals can only be send in response to an interaction,
# we need a slash command or button to trigger the modal.
@bot.tree.command()
async def wait_for_input(interaction: Interaction):
"""Test command to wait for input. This will trigger a modal."""
# Defining our modal check
# we can use this to check the input or user or whatever..
def check(modal: SimpleModalWaitFor, modal_interaction: Interaction) -> bool:
# Check if the modal user is the same as the command's author
return modal_interaction.user.id == interaction.user.id
# Check the input of the modal
# since on_submit is not called yet.. we need to check the input
# from the TextInput we assigned to an attribute on the modal called "answer"
# Here we are checking if the input is not "test123"
# if it's not "test123" we can send a message in the channel and return False.
# Don't forget to make the check async.
# if modal.answer.value != "test123":
# await modal_interaction.response.send_message(f"{interaction.user} expected input to be 'test123', not '{modal.answer.value}'.")
# return False
# If the input is "test123" we can return True.
# return True
# Initiate our modal.
wait_modal = SimpleModalWaitFor(
# Changing the title of the modal
# to "Waiting For {the user's name}" instead of "Waiting For Input"
title=f"Waiting for {interaction.user.name}",
# Changing the label of the text input field
# to "What is your name?" instead of "Input text"
input_label="What is your name?",
# Changing the input_max_length to 20 instead of 100
# to prevent spam being entered. Names generally are not that long.
input_max_length=20,
# Assign our check
check=check,
)
# Send the modal.
await interaction.response.send_modal(wait_modal)
# Wait for the modal to be submitted or timeout.
# This will block the command until the modal is submitted, stopped or timed out.
wait_value = await wait_modal.wait()
# Get the value of the text input field and send it in the channel as a response.
# This is what we use the interaction of the modal for.
# Check if the value is not None.
# If it is, the modal was timed out or explicitly stopped using the stop method.
# You can check if the modal was timed out or stopped using wait_value.
# wait_value will be True if the modal was timed out
# and False if it was stopped.
if wait_modal.value is None:
# Send a followup message to the channel.
await interaction.followup.send(f"{interaction.user} did not enter a value in time.")
return
# If the value is not None, the modal was submitted.
# Send a response to the channel.
await wait_modal.interaction.response.send_message(f"Your name is {wait_modal.value}")
# https://gist.github.com/Soheab/f46fee27498aad4a8962d59b6f0415c6
from __future__ import annotations
from typing import Any, Callable, Coroutine, Optional, Union, TYPE_CHECKING
from discord import Interaction, TextStyle
from discord.ui import Modal, TextInput
from discord.utils import maybe_coroutine
if TYPE_CHECKING:
from typing_extensions import Self
class SimpleModalWaitFor(Modal):
def __init__(
self,
title: str = "Waiting For Input",
*,
check: Optional[Callable[[Self, Interaction], Union[Coroutine[Any, Any, bool], bool]]] = None,
timeout: float = 30.0,
input_label: str = "Input text",
input_max_length: int = 100,
input_min_length: int = 5,
input_style: TextStyle = TextStyle.short,
input_placeholder: Optional[str] = None,
input_default: Optional[str] = None,
):
super().__init__(title=title, timeout=timeout, custom_id="wait_for_modal")
self._check: Optional[Callable[[Self, Interaction], Union[Coroutine[Any, Any, bool], bool]]] = check
self.value: Optional[str] = None
self.interaction: Optional[Interaction] = None
self.answer = TextInput(
label=input_label,
placeholder=input_placeholder,
max_length=input_max_length,
min_length=input_min_length,
style=input_style,
default=input_default,
custom_id=self.custom_id + "_input_field",
)
self.add_item(self.answer)
async def interaction_check(self, interaction: Interaction) -> bool:
if self._check:
allow = await maybe_coroutine(self._check, self, interaction)
return allow
return True
async def on_submit(self, interaction: Interaction) -> None:
self.value = self.answer.value
self.interaction = interaction
self.stop()
@fw-real
Copy link

fw-real commented Sep 16, 2022

man fuck this shit, imma make nothing instead

@aurkaxi
Copy link

aurkaxi commented Sep 26, 2022

imma make nothing instead
me too. my life is easier than this

@NapoII
Copy link

NapoII commented Sep 29, 2022

This is a big joke from Discord that this workaround is necessary for dealing with a modal. If you add an input field, each coder has to code their own response to make the input field wait for the USER. That's kind of stupid.

@Motzumoto
Copy link

Makes me gasm.

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