Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletion discord/app_commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
List,
MutableMapping,
Optional,
Sequence,
Set,
TYPE_CHECKING,
Tuple,
Expand All @@ -48,7 +49,7 @@
import re
from copy import copy as shallow_copy

from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale, FileType
from .installs import AppCommandContext, AppInstallationType
from .models import Choice
from .transformers import annotation_to_parameter, CommandParameter, NoneType
Expand Down Expand Up @@ -107,6 +108,7 @@
'user_install',
'allowed_installs',
'default_permissions',
'set_file_types',
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -373,6 +375,25 @@ def _populate_autocomplete(params: Dict[str, CommandParameter], autocomplete: Di
raise TypeError(f'unknown parameter given: {first}')


def _populate_file_types(params: Dict[str, CommandParameter], file_types: Dict[str, Sequence[Union[str, FileType]]]) -> None:
for name, param in params.items():
Comment thread
Soheab marked this conversation as resolved.
if param.type is not AppCommandOptionType.attachment:
raise TypeError('file_types is only supported for attachment option types')

types = file_types.pop(name, MISSING)
if types is MISSING:
continue

if not isinstance(types, (list, tuple)) or not all(isinstance(ft, (str, FileType)) for ft in types):
raise TypeError('file_types must be a list of strings or FileType enums')

param.file_types = [ft.value if isinstance(ft, FileType) else ft for ft in types]

if file_types:
first = next(iter(file_types))
raise TypeError(f'unknown parameter given: {first}')


def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[str, Any]) -> Dict[str, CommandParameter]:
params = inspect.signature(func).parameters
cache = {}
Expand Down Expand Up @@ -428,6 +449,13 @@ def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[s
else:
_populate_autocomplete(result, autocomplete.copy())

try:
file_types = func.__discord_app_commands_param_file_types__
except AttributeError:
pass
else:
_populate_file_types(result, file_types.copy())

return result


Expand Down Expand Up @@ -497,6 +525,10 @@ class Parameter:
The minimum supported value for this parameter.
max_value: Optional[Union[:class:`int`, :class:`float`]]
The maximum supported value for this parameter.
file_types: Optional[Sequence[Union[:class:`str`, :class:`.FileType`]]]
A list of file types that are allowed to be uploaded for this parameter.

.. versionadded:: 2.8
default: Any
The default value of the parameter, if given.
If not given then this is :data:`~discord.utils.MISSING`.
Expand Down Expand Up @@ -574,6 +606,10 @@ def min_value(self) -> Optional[Union[int, float]]:
def max_value(self) -> Optional[Union[int, float]]:
return self.__parent.max_value

@property
def file_types(self) -> Optional[Sequence[Union[str, FileType]]]:
return self.__parent.file_types


class Command(Generic[GroupT, P, T]):
"""A class that implements an application command.
Expand Down Expand Up @@ -2905,3 +2941,59 @@ def decorator(func: T) -> T:
return func

return decorator


def set_file_types(**parameters: Sequence[Union[str, FileType]]) -> Callable[[T], T]:
r"""Sets the file types for the given parameters by their name using the key of the keyword argument
as the name.

.. versionadded:: 2.8

.. warning::

The actual file is not guaranteed to be of the specified type. The client only
checks the file extension, so users can easily bypass this check by renaming the file.

Example:

.. code-block:: python3

@app_commands.command(description='Uploads a file')
@app_commands.set_file_types(file=['.png', discord.FileType.video])
async def upload(interaction: discord.Interaction, file: discord.Attachment):
await interaction.response.send_message(f'Uploaded {file.filename}')

Parameters
-----------
\*\*parameters: Sequence[Union[:class:`str`, :class:`.FileType`]]
A list of up to 10 file types that are allowed to be uploaded for each attachment parameter.
The type of the parameter must be :class:`discord.Attachment`.

You can mix and match strings and :class:`.FileType` enums in the list.

If a string is provided, make sure to prefix it with a period (``.``) (e.g. ``.png``).
This is required.
You may provide any string you want, but (if you are specifying only extensions) you must
include ``.jpg`` for image uploads, and both ``.mp4`` and ``.mov`` for video uploads.

Must be between 0 and 10. Defaults to allowing all file types.
Comment thread
Soheab marked this conversation as resolved.

Raises
--------
TypeError
The parameter name is not found or the parameter type was incorrect.
"""

def decorator(inner: T) -> T:
unwrapped = getattr(inner, '__discord_app_commands_unwrap__', inner) or inner
if isinstance(unwrapped, Command):
_populate_file_types(unwrapped._params, parameters)
else:
try:
inner.__discord_app_commands_param_file_types__.update(parameters) # type: ignore # Runtime attribute access
except AttributeError:
inner.__discord_app_commands_param_file_types__ = parameters # type: ignore # Runtime attribute assignment

return inner

return decorator
7 changes: 7 additions & 0 deletions discord/app_commands/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,10 @@ class Argument:
The maximum allowed length for this parameter.
autocomplete: :class:`bool`
Whether the argument has autocomplete.
file_types: Sequence[Union[:class:`str`, :class:`.FileType`]]
A list of file types that are allowed to be uploaded for this argument.

.. versionadded:: 2.8
"""

__slots__ = (
Expand All @@ -1036,6 +1040,7 @@ class Argument:
'autocomplete',
'parent',
'_state',
'file_types',
)

def __init__(
Expand All @@ -1062,6 +1067,7 @@ def _from_data(self, data: ApplicationCommandOption) -> None:
self.choices: List[Choice[Union[int, float, str]]] = [Choice.from_dict(d) for d in data.get('choices', [])]
self.name_localizations: Dict[Locale, str] = _to_locale_dict(data.get('name_localizations') or {})
self.description_localizations: Dict[Locale, str] = _to_locale_dict(data.get('description_localizations') or {})
self.file_types: List[str] = data.get('file_types', [])

def to_dict(self) -> ApplicationCommandOption:
return {
Expand All @@ -1079,6 +1085,7 @@ def to_dict(self) -> ApplicationCommandOption:
'options': [],
'name_localizations': {str(k): v for k, v in self.name_localizations.items()},
'description_localizations': {str(k): v for k, v in self.description_localizations.items()},
'file_types': self.file_types,
} # type: ignore # Type checker does not understand this literal.


Expand Down
6 changes: 5 additions & 1 deletion discord/app_commands/transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
List,
Literal,
Optional,
Sequence,
Set,
Tuple,
Type,
Expand All @@ -52,7 +53,7 @@
from ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel, ForumChannel
from ..abc import GuildChannel
from ..threads import Thread
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale, FileType
from ..utils import MISSING, maybe_coroutine, _human_join, _iscoroutinefunction, TIMESTAMP_PATTERN
from ..user import User
from ..role import Role
Expand Down Expand Up @@ -91,6 +92,7 @@ class CommandParameter:
min_value: Optional[Union[int, float]] = None
max_value: Optional[Union[int, float]] = None
autocomplete: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None
file_types: Optional[Sequence[Union[str, FileType]]] = MISSING
_rename: Union[str, locale_str] = MISSING
_annotation: Any = MISSING

Expand Down Expand Up @@ -143,6 +145,8 @@ def to_dict(self) -> Dict[str, Any]:
base['channel_types'] = [t.value for t in self.channel_types]
if self.autocomplete:
base['autocomplete'] = True
if self.file_types:
base['file_types'] = [ft.value if isinstance(ft, FileType) else ft for ft in self.file_types]

min_key, max_key = (
('min_value', 'max_value') if self.type is not AppCommandOptionType.string else ('min_length', 'max_length')
Expand Down
9 changes: 9 additions & 0 deletions discord/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,11 @@ class FileUploadComponent(Component):
required: :class:`bool`
Whether the component is required.
Defaults to ``True``.
file_types: List[:class:`str`]
A list of file types that are allowed to be uploaded for this component.
Defaults to allowing all file types.

.. versionadded:: 2.8
"""

__slots__: Tuple[str, ...] = (
Expand All @@ -1475,6 +1480,7 @@ class FileUploadComponent(Component):
'max_values',
'required',
'id',
'file_types',
)

__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
Expand All @@ -1485,6 +1491,7 @@ def __init__(self, data: FileUploadComponentPayload, /) -> None:
self.max_values: int = data.get('max_values', 1)
self.required: bool = data.get('required', True)
self.id: Optional[int] = data.get('id')
self.file_types: List[str] = data.get('file_types', [])

@property
def type(self) -> Literal[ComponentType.file_upload]:
Expand All @@ -1501,6 +1508,8 @@ def to_dict(self) -> FileUploadComponentPayload:
}
if self.id is not None:
payload['id'] = self.id
if self.file_types:
payload['file_types'] = self.file_types

return payload

Expand Down
24 changes: 24 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
'MediaItemLoadingState',
'CollectibleType',
'NameplatePalette',
'FileType',
)


Expand Down Expand Up @@ -1006,6 +1007,29 @@ class NameplatePalette(Enum):
white = 'white'


class FileType(Enum):
audio = 'audio'
video = 'video'
image = 'image'

@property
def file_extensions(self) -> Tuple[str, ...]:

@vmphase vmphase Aug 8, 2026

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 is not documented in docs/api.rst

""":class:`tuple[str]`: Returns a tuple of file extensions that belong to this file type.

.. warning::

These are subject to change at anytime and should not be relied upon for validation.
"""
# fmt: off
lookup: Dict[FileType, Tuple[str, ...]] = {
FileType.image: ('png', 'gif', 'jpg', 'jpeg', 'jfif', 'webp', 'avif'),
FileType.video: ('mp4', 'mov', 'qt', 'webm'),
FileType.audio: ('mp3', 'm4a', 'wav', 'ogg', 'opus', 'flac'),
}
# fmt: on
return lookup.get(self, ())


def create_unknown_value(cls: Type[E], val: Any) -> E:
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'unknown_{val}'
Expand Down
6 changes: 6 additions & 0 deletions discord/types/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,18 @@ class _NumberApplicationCommandOption(_BaseValueApplicationCommandOption, total=
autocomplete: bool


class _AttachmentApplicationCommandOption(_BaseValueApplicationCommandOption):
type: Literal[11]
file_types: NotRequired[List[str]]


_ValueApplicationCommandOption = Union[
_StringApplicationCommandOption,
_IntegerApplicationCommandOption,
_BooleanApplicationCommandOption,
_SnowflakeApplicationCommandOptionChoice,
_NumberApplicationCommandOption,
_AttachmentApplicationCommandOption,
]

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


class RadioGroupComponent(ComponentBase):
Expand Down
Loading
Loading