Skip to content
30 changes: 29 additions & 1 deletion discord/commands/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
from ..commands import ApplicationContext, AutocompleteContext
from ..enums import ChannelType
from ..enums import Enum as DiscordEnum
from ..enums import SlashCommandOptionType
from ..enums import FileType, SlashCommandOptionType
from ..utils import MISSING, basic_autocomplete

if TYPE_CHECKING:
Expand Down Expand Up @@ -195,6 +195,15 @@ class Option:
description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this option. The values of this should be ``"locale": "description"``.
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
file_types: List[:class:`str`, :class:`FileType`]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List[X, Y] is not valid, because it only takes one type parameter.

Suggested change
file_types: List[:class:`str`, :class:`FileType`]
file_types: List[Union[:class:`str`, :class:`FileType`]]

The file types allowed for this option. Supports a mix of :class:`FileType` and dot-prefixed extensions (e.g. ``".pdf"``).
A maximum of 10 file types can be provided, up to 16 characters each.
Only applies to Options with an :attr:`input_type` of :class:`Attachment`.

.. warning::
This only checks the file extension - you will still need to validate the file contents yourself.

.. versionadded:: 2.9

Examples
--------
Expand Down Expand Up @@ -393,6 +402,23 @@ def __init__(
self.description_localizations = kwargs.pop(
"description_localizations", MISSING
)
self.file_types: list[FileType | str] = kwargs.pop("file_types", []) or []
if self.file_types:
if not isinstance(self.file_types, list):
raise TypeError(
f"file_types must be a list, not {self.file_types.__class__.__name__}"
)
if len(self.file_types) > 10:
raise ValueError("file_types must be between 0 and 10 in length")
for f in self.file_types:
if not isinstance(f, (str, FileType)):
raise TypeError(
"items in file_types must be of type str or FileType"
)
if len(str(f)) > 16:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also checks the length of our enum values, which is a bit useless?

raise ValueError(
"items in file_types must be a maximum of 16 characters in length"
)

if input_type is None:
raise TypeError("input_type cannot be NoneType.")
Expand Down Expand Up @@ -453,6 +479,8 @@ def to_dict(self) -> dict:
as_dict["min_length"] = self.min_length
if self.max_length is not None:
as_dict["max_length"] = self.max_length
if self.file_types:
as_dict["file_types"] = [str(f) for f in self.file_types]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

str(FileType) won't serialize correctly, since it subclasses Enum. Either make it subclass StrEnum, or explicitly serialize with .value when the item is a FileType.


return as_dict

Expand Down
20 changes: 20 additions & 0 deletions discord/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
ButtonStyle,
ChannelType,
ComponentType,
FileType,
InputTextStyle,
SelectDefaultValueType,
SeparatorSpacingSize,
Expand Down Expand Up @@ -1386,6 +1387,14 @@ class FileUpload(Component):
The maximum number of files that can be uploaded.
required: Optional[:class:`bool`]
Whether the file upload field is required or not. Defaults to `True`.
file_types: List[:class:`str`, :class:`FileType`]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List[X, Y] is not valid, because it only takes one type parameter.

Suggested change
file_types: List[:class:`str`, :class:`FileType`]
file_types: List[Union[:class:`str`, :class:`FileType`]]

The file types allowed in this upload. Supports a mix of :class:`FileType` and dot-prefixed extensions (e.g. ``".pdf"``).
A maximum of 10 file types can be provided, up to 16 characters each.

.. warning::
This only checks the file extension - you will still need to validate the file contents yourself.

.. versionadded:: 2.9
id: Optional[:class:`int`]
The file upload's ID.
"""
Expand All @@ -1396,6 +1405,7 @@ class FileUpload(Component):
"min_values",
"max_values",
"required",
"file_types",
"id",
)

Expand All @@ -1409,6 +1419,13 @@ def __init__(self, data: FileUploadComponentPayload):
self.min_values: int | None = data.get("min_values", None)
self.max_values: int | None = data.get("max_values", None)
self.required: bool = data.get("required", True)
self.file_types: list[str | FileType] = []
for f in data.get("file_types", []):
(
self.file_types.append(f)
if f not in FileType.__members__
else try_enum(FileType, f)
)
Comment on lines +1424 to +1429

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure the logic here matches what you were planning, unless I missed something. Currently, when f is a known FileType member, it goes into the else branch and calls try_enum(FileType, f), but the result is never appended anywhere, so it's just discarded. Only unknown values are in self.file_types.


def to_dict(self) -> FileUploadComponentPayload:
payload = {
Expand All @@ -1427,6 +1444,9 @@ def to_dict(self) -> FileUploadComponentPayload:
if not self.required:
payload["required"] = self.required

if self.file_types:
payload["file_types"] = [str(f) for f in self.file_types]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

str(FileType) won't serialize correctly, since it subclasses Enum. Either make it subclass StrEnum, or explicitly serialize with .value when the item is a FileType.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it a string enum


return payload # type: ignore


Expand Down
21 changes: 21 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"SelectDefaultValueType",
"ApplicationEventWebhookStatus",
"InviteTargetUsersJobStatusCode",
"FileType",
)


Expand Down Expand Up @@ -1212,6 +1213,26 @@ class InviteTargetUsersJobStatusCode(Enum):
failed = 3


class FileType(Enum):
"""Represents the allowed file types in an attachment option or component. The types are subject to change based on what is supported on Discord clients.

.. versionadded:: 2.9

Attributes
----------
image: :class:`str`
Allow image files. This includes ``.png``, ``.gif``, ``.jpg``, ``.jpeg``, ``.jfif``, ``.webp``, and ``.avif``.
video: :class:`str`
Allow video files. This includes ``.mp4``, ``.mov``, ``.qt``, and ``.webm``.
audio: :class:`str`
Allow audio files. This includes ``.mp3``, ``.m4a``, ``.wav``, ``.ogg``, ``.opus``, and ``.flac``.
"""

image = "image"
video = "video"
audio = "audio"


T = TypeVar("T")


Expand Down
1 change: 1 addition & 0 deletions discord/types/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class FileUploadComponent(BaseComponent):
max_values: NotRequired[int]
min_values: NotRequired[int]
required: NotRequired[bool]
file_types: NotRequired[list[str]]


class RadioGroupOption(TypedDict):
Expand Down
64 changes: 63 additions & 1 deletion discord/ui/file_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from typing import TYPE_CHECKING

from ..components import FileUpload as FileUploadComponent
from ..enums import ComponentType
from ..enums import ComponentType, FileType
from ..message import Attachment
from .item import ModalItem

Expand Down Expand Up @@ -58,13 +58,22 @@ class FileUpload(ModalItem):
Whether the file upload field is required or not. Defaults to ``True``.
id: Optional[:class:`int`]
The file upload field's ID.
file_types: List[:class:`str`, :class:`FileType`]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List[X, Y] is not valid, because it only takes one type parameter.

Suggested change
file_types: List[:class:`str`, :class:`FileType`]
file_types: List[Union[:class:`str`, :class:`FileType`]]

The file types allowed in this file upload. Supports a mix of :class:`FileType` and dot-prefixed extensions (e.g. ``".pdf"``).
A maximum of 10 file types can be provided, up to 16 characters each.

.. warning::
This only checks the file extension - you will still need to validate the file contents yourself.

.. versionadded:: 2.9
"""

__item_repr_attributes__: tuple[str, ...] = (
"required",
"min_values",
"max_values",
"custom_id",
"file_types",
"id",
)

Expand All @@ -75,6 +84,7 @@ def __init__(
min_values: int | None = None,
max_values: int | None = None,
required: bool = True,
file_types: list[str | FileType] | None = None,
id: int | None = None,
):
super().__init__()
Expand All @@ -88,6 +98,22 @@ def __init__(
)
if not isinstance(required, bool):
raise TypeError(f"required must be bool not {required.__class__.__name__}") # type: ignore
if file_types is not None:
if not isinstance(file_types, list):
raise TypeError(
f"file_types must be a list, not {file_types.__class__.__name__}"
)
if len(file_types) > 10:
raise ValueError("file_types must be between 0 and 10 in length")
for f in file_types:
if not isinstance(f, (str, FileType)):
raise TypeError(
"items in file_types must be of type str or FileType"
)
if len(str(f)) > 16:
raise ValueError(
"items in file_types must be a maximum of 16 characters in length"
)
custom_id = os.urandom(16).hex() if custom_id is None else custom_id
self._attachments: list[Attachment] | None = None

Expand All @@ -96,6 +122,7 @@ def __init__(
min_values=min_values,
max_values=max_values,
required=required,
file_types=file_types,
id=id,
)

Expand All @@ -105,6 +132,7 @@ def _generate_underlying(
min_values: int | None = None,
max_values: int | None = None,
required: bool | None = None,
file_types: list[str | FileType] | None = None,
id: int | None = None,
) -> FileUploadComponent:
super()._generate_underlying(FileUploadComponent)
Expand All @@ -114,6 +142,7 @@ def _generate_underlying(
min_values=min_values if min_values is not None else self.min_values,
max_values=max_values if max_values is not None else self.max_values,
required=required if required is not None else self.required,
file_types=file_types if file_types else [],
id=id or self.id,
)

Expand Down Expand Up @@ -167,6 +196,38 @@ def required(self, value: bool):
raise TypeError(f"required must be bool not {value.__class__.__name__}") # type: ignore
self.underlying.required = bool(value)

@property
def file_types(self) -> list[str | FileType]:
"""The file types allowed in this file upload. Supports a mix of :class:`FileType` and dot-prefixed extensions (e.g. ``".pdf"``).
A maximum of 10 file types can be provided, up to 16 characters each.

.. warning::
This only checks the file extension - you will still need to validate the file contents yourself.

.. versionadded:: 2.9
"""
return self.underlying.file_types

@file_types.setter
def file_types(self, value: list[str | FileType] | None):
if value:
if not isinstance(value, list):
raise TypeError(
f"file_types must be a list, not {value.__class__.__name__}"
)
if len(value) > 10:
raise ValueError("file_types must be between 0 and 10 in length")
for f in value:
if not isinstance(f, (str, FileType)):
raise TypeError(
"items in file_types must be of type str or FileType"
)
if len(str(f)) > 16:
raise ValueError(
"items in file_types must be a maximum of 16 characters in length"
)
self.underlying.file_types = value or []

@property
def values(self) -> list[Attachment] | None:
"""The files that were uploaded to the field. This will be ``None`` if the file upload has not been submitted via a modal yet."""
Expand Down Expand Up @@ -195,5 +256,6 @@ def from_component(
min_values=component.min_values,
max_values=component.max_values,
required=component.required,
file_types=component.file_types,
id=component.id,
)
17 changes: 17 additions & 0 deletions docs/api/enums.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2702,3 +2702,20 @@ of :class:`enum.Enum`.
.. attribute:: read_only

Represents the team read only role.

.. class:: FileType
Represents the allowed file types in an attachment option or component. The types are subject to change based on what is supported on Discord clients.

.. versionadded:: 2.9

.. attribute:: image

Allow image files. This includes ``.png``, ``.gif``, ``.jpg``, ``.jpeg``, ``.jfif``, ``.webp``, and ``.avif``.

.. attribute:: video

Allow video files. This includes ``.mp4``, ``.mov``, ``.qt``, and ``.webm``.

.. attribute:: audio

Allow audio files. This includes ``.mp3``, ``.m4a``, ``.wav``, ``.ogg``, ``.opus``, and ``.flac``.
Comment on lines +2711 to +2721

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented twice? Docstring in the class too. Not sure if that's even considered.

Loading