NAFTeam / NAFF

A Python API wrapper for Discord
https://naff.info
MIT License
117 stars 24 forks source link

[FEAT] Modal builder class decorator #704

Open i0bs opened 1 year ago

i0bs commented 1 year ago

Is your feature request related to a problem? Please describe.

I'm currently using NAFF for a company contract that involves needing to survey user data at a large scale and give us administrative control over what text inputs are used. We do this by checking a database for given text inputs and select menus, and sending it on an interaction create event.

Doing this, however, brings pros and cons:

The good

The bad

Describe the solution you'd like

The modal class in NAFF strictly follows the API object schema for its resource. Instead of directly modifying the contents of the Python object, I'd like if there was a way we could pre-emptively build a modal without being too specific. This is where I pitch my idea: a modal builder.

Take the following example of what you currently do in NAFF:

modal = Modal(
  title="Modal title",
  components=[
    ShortText(label="Short text input", custom_id="short_text_input"),
    ParagraphText(label="Paragraph text input", custom_id="long_text", required=False),
    Select(options=[SelectOption(label="foo", value="bar", default=True)], custom_id="selection")
  ]
)

This works good if you're not needing to change anything, and/or you're keeping the modal's design hardcoded inside the source code. However, doing modal.components[0] and etc. is an inefficient; and non-ergonomic manner to changing the modal's contents. We now have to remember which indices of the modal's components we want to change, or iterate through it. Yucky and not nice!

Instead, if we used something like dataclasses, we could establish a key-value pair of the attribute to the object. Take the following proposal where we use typing.TypedDict to our advantage for required and non-required values.

from naff import * # forgive me polls
import typing

# We can either adopt the name of the modal through the class or as a positional argument of the decorator.
@naff.modal_builder(name="Modal title")
class MyNewModal(typing.TypedDict):
  # In the class, modal text inputs and their respective object are type annotated instead.
  # We still adopt the kwargs approach to writing contents.
  short_text_input: typing.Required[ShortText(label="Short input text")]

  # Instead of declaring a text input component's optionality in the object, we can refer from
  # the optional value in the class.
  long_text: typing.NotRequired[ParagraphText(label="Paragraph text input")] = None # equals expression is unnecessary

  # The same may apply for select menus within where we want to automatically represent an
  # default choice.
  # We technically make it "required" since it's not an undefined value, but it'll still default to that select option.
  selection: typing.Required[Select] = SelectOption(label="foo", value="bar")

With a key-value pair set with the attribute name and the type, we could also establish an object return from the decorator's application onto the class. In this case we'll just call it ModalBuild. This object essentially acts as a manager of the newly built modal and will now allow us to modify it in whatever situation. This theoretically lets us do this:

global new_modal
new_modal = typing.Mapping[MyNewModal, ModalBuild]

# We can now directly affect the contents of it easily without needing to iterate through
# components and checking the custom ID, and offers more versatility.
new_modal.short_text_input.required = False

# Subsequently we could do this. Not nice, but if we were working with a database, it'd be simpler.
new_modal.long_text = ParagraphText(label="Paragraph text input")

Describe alternatives you've considered

We've originally considered just writing our own modal builder class that used special magic methods for changing attribute data based off of the database and apply them during runtime, but we found this didn't work nicely for what we wanted.

Additional Information

With this solution, it should still be possible to send a ModalBuild the same as a regular Modal object from NAFF in an interaction response.

@slash_command(name="modal_sender", description="Sends a modal.")
async def my_new_modal_sender(ctx: ModalContext) -> None:
  await ctx.send_modal(new_modal)
silasary commented 1 year ago

Sounds useful