I've wanted to improve on my bot to not only assign students based on the invite they use, but also ask them for their name (I haven't got around to OAuth2's, which I know would be more seamless and more secure).

And I was happy that it was possible with Discord Views and Modals (I still don't understand why they're separate).

For everyone that needs the same thing I needed, and stumbles upon my post here, this is the solution I found while using discord.py as the library (I also have my code in Cogs; if you use a single file, your mileage may vary)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class NameFormModal(discord.ui.Modal, title = "<NAME DISPLAYED>"):
	def __init__(self):
		super().__init__(title = "<NAME DISPLAYED>")
		self._name = None
		self._surname = None

    name = discord.ui.TextInput(
        label = "<DISPLAYED OVER TEXT FIELD>",
        placeholder = "<DISPLAYED GREYED OUT INSIDE TEXT FIELD>",
        style = discord.TextStyle.short,
        required = True
    )
    surname = [...]

    async def on_submit(self, interaction: discord.Interaction):
        self._name, self._surname = self.name.value, self.surname.value
        await interaction.response.send_message("<REPLY WHEN SUBMITTING SUCCESSFUL>")

    async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
        await interaction.response.send_message("<REPLY WHEN ERROR OCCURED>")

class NameFormView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout = None)
        self._name = None
        self._surname = None

    @discord.ui.button(label = "<NAME ON THE BUTTON>", style = discord.ButtonStyle.primary)
    async def name_form(self, interaction: discord.Interaction, button: discord.ui.Button):
        modal = NameFormModal()
        await interaction.response.send_modal(modal)
        await modal.wait()
        self._name, self._surname = modal._name, modal._surname
        self.stop()

@app_commands.guild_only()
class <COG NAME>(commands.GroupCog, group_name = "<COMMAND GROUP NAME>"):
    def __init__(self, client):
        self.client = client

    @commands.Cog.listener()
    async def on_member_join(self, member):
        recipient = self.client.get_user(member.id)
        view = NameFormView()
        message = await recipient.send("<MESSAGE TO SEND WITH THE BUTTON>", view = view)
        await view.wait()
        view._children[0].disabled = True
        await message.edit(view = view)
        name = view._name.lower().capitalize()
        surname = " ".join(w.capitalize() for w in view._surname.split())
        await member.edit(nick = f"{name} {surname}")

async def setup(client):
    await client.add_cog(<COG NAME>(client))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class NameFormModal(discord.ui.Modal, title = "<NAME DISPLAYED>"):
	def __init__(self):
		super().__init__(title = "<NAME DISPLAYED>")
		self._name = None
		self._surname = None

    name = discord.ui.TextInput(
        label = "<DISPLAYED OVER TEXT FIELD>",
        placeholder = "<DISPLAYED GREYED OUT INSIDE TEXT FIELD>",
        style = discord.TextStyle.short,
        required = True
    )
    surname = [...]

    async def on_submit(self, interaction: discord.Interaction):
        self._name, self._surname = self.name.value, self.surname.value
        await interaction.response.send_message("<REPLY WHEN SUBMITTING SUCCESSFUL>")

    async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
        await interaction.response.send_message("<REPLY WHEN ERROR OCCURED>")

This is the Modal, so the "window" Discord will show your user, and it is the only way to get Text Input not via a text message.

But, Modals can't be "forced" - the user has to somehow "request" them. The easiest solution is a command, but as your users may not be accustomed to using commands, so we use a button, in a View, as seen below.

22
23
24
25
26
27
28
29
30
31
32
33
34
class NameFormView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout = None)
        self._name = None
        self._surname = None

    @discord.ui.button(label = "<NAME ON THE BUTTON>", style = discord.ButtonStyle.primary)
    async def name_form(self, interaction: discord.Interaction, button: discord.ui.Button):
        modal = NameFormModal()
        await interaction.response.send_modal(modal)
        await modal.wait()
        self._name, self._surname = modal._name, modal._surname
        self.stop()

I also opted for an infinite timeout (line #24), so a user may input their name after x time, but they can do so only once, as I've also implemented stops and waits for modal and view that disable the button after successfully submitting the Modal, with the code in Modal on lines #32 and #34 and the code in the event handler on lines #46-48 below.

41
42
43
44
45
46
47
48
49
50
51
@commands.Cog.listener()
async def on_member_join(self, member):
	recipient = self.client.get_user(member.id)
	view = NameFormView()
	message = await recipient.send("<MESSAGE TO SEND WITH THE BUTTON>", view = view)
	await view.wait()
	view._children[0].disabled = True
	await message.edit(view = view)
	name = view._name.lower().capitalize()
	surname = " ".join(w.capitalize() for w in view._surname.split())
	await member.edit(nick = f"{name} {surname}")

We also need to get the form values all the way back to the event listener, so we define _name and _surname in both the View and the Modal, and fill them prior to stopping (#16 - Modal, #33 - View).


If you'd be crazy enough to use code I wrote, you should replace all <>'s for some more reasonable strings.

I've also tried defining everything in the Cog, instead of Modal and View classes prior, but then Buttons aren't sent; my guess - some init issues.