diff --git a/discord/commands/options.py b/discord/commands/options.py index 6567d0c4e7..8359a2dde5 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -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: @@ -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 `_ for a list of valid locales. + file_types: List[: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 -------- @@ -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: + 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.") @@ -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] return as_dict diff --git a/discord/components.py b/discord/components.py index 5749e4ad74..09b77b2a02 100644 --- a/discord/components.py +++ b/discord/components.py @@ -33,6 +33,7 @@ ButtonStyle, ChannelType, ComponentType, + FileType, InputTextStyle, SelectDefaultValueType, SeparatorSpacingSize, @@ -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`] + 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. """ @@ -1396,6 +1405,7 @@ class FileUpload(Component): "min_values", "max_values", "required", + "file_types", "id", ) @@ -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) + ) def to_dict(self) -> FileUploadComponentPayload: payload = { @@ -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] + return payload # type: ignore diff --git a/discord/enums.py b/discord/enums.py index 5011a338c2..c45c7690cd 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -88,6 +88,7 @@ "SelectDefaultValueType", "ApplicationEventWebhookStatus", "InviteTargetUsersJobStatusCode", + "FileType", ) @@ -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") diff --git a/discord/types/components.py b/discord/types/components.py index af567b6566..c8dd29779d 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -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): diff --git a/discord/ui/file_upload.py b/discord/ui/file_upload.py index 784b8f6733..04b586028c 100644 --- a/discord/ui/file_upload.py +++ b/discord/ui/file_upload.py @@ -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 @@ -58,6 +58,14 @@ 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`] + 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, ...] = ( @@ -65,6 +73,7 @@ class FileUpload(ModalItem): "min_values", "max_values", "custom_id", + "file_types", "id", ) @@ -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__() @@ -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 @@ -96,6 +122,7 @@ def __init__( min_values=min_values, max_values=max_values, required=required, + file_types=file_types, id=id, ) @@ -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) @@ -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, ) @@ -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.""" @@ -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, ) diff --git a/docs/api/enums.rst b/docs/api/enums.rst index efa16a4a5e..ee051ca989 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -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``.