Browse Source

feat: Components V2

pull/10166/head
DA-344 3 months ago
parent
commit
ae2410fa3a
  1. 417
      discord/attachment.py
  2. 176
      discord/components.py
  3. 21
      discord/enums.py
  4. 8
      discord/flags.py
  5. 298
      discord/message.py
  6. 58
      discord/types/attachment.py
  7. 69
      discord/types/components.py
  8. 26
      discord/types/message.py
  9. 50
      discord/ui/section.py

417
discord/attachment.py

@ -0,0 +1,417 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the 'Software'),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import io
from os import PathLike
from typing import TYPE_CHECKING, Any, Optional, Union
from .mixins import Hashable
from .file import File
from .state import ConnectionState
from .flags import AttachmentFlags
from . import utils
if TYPE_CHECKING:
from .types.attachment import Attachment as AttachmentPayload
MISSING = utils.MISSING
__all__ = (
'Attachment',
'UnfurledAttachment',
)
class AttachmentBase:
url: str
async def save(
self,
fp: Union[io.BufferedIOBase, PathLike[Any]],
*,
seek_begin: bool = True,
use_cached: bool = False,
) -> int:
"""|coro|
Saves this attachment into a file-like object.
Parameters
----------
fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`]
The file-like object to save this attachment to or the filename
to use. If a filename is passed then a file is created with that
filename and used instead.
seek_begin: :class:`bool`
Whether to seek to the beginning of the file after saving is
successfully done.
use_cached: :class:`bool`
Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading
the attachment. This will allow attachments to be saved after deletion
more often, compared to the regular URL which is generally deleted right
after the message is deleted. Note that this can still fail to download
deleted attachments if too much time has passed and it does not work
on some types of attachments.
Raises
--------
HTTPException
Saving the attachment failed.
NotFound
The attachment was deleted.
Returns
--------
:class:`int`
The number of bytes written.
"""
data = await self.read(use_cached=use_cached)
if isinstance(fp, io.BufferedIOBase):
written = fp.write(data)
if seek_begin:
fp.seek(0)
return written
else:
with open(fp, 'wb') as f:
return f.write(data)
async def read(self, *, use_cached: bool = False) -> bytes:
"""|coro|
Retrieves the content of this attachment as a :class:`bytes` object.
.. versionadded:: 1.1
Parameters
-----------
use_cached: :class:`bool`
Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading
the attachment. This will allow attachments to be saved after deletion
more often, compared to the regular URL which is generally deleted right
after the message is deleted. Note that this can still fail to download
deleted attachments if too much time has passed and it does not work
on some types of attachments.
Raises
------
HTTPException
Downloading the attachment failed.
Forbidden
You do not have permissions to access this attachment
NotFound
The attachment was deleted.
Returns
-------
:class:`bytes`
The contents of the attachment.
"""
url = self.proxy_url if use_cached else self.url
data = await self._http.get_from_cdn(url)
return data
async def to_file(
self,
*,
filename: Optional[str] = MISSING,
description: Optional[str] = MISSING,
use_cached: bool = False,
spoiler: bool = False,
) -> File:
"""|coro|
Converts the attachment into a :class:`File` suitable for sending via
:meth:`abc.Messageable.send`.
.. versionadded:: 1.3
Parameters
-----------
filename: Optional[:class:`str`]
The filename to use for the file. If not specified then the filename
of the attachment is used instead.
.. versionadded:: 2.0
description: Optional[:class:`str`]
The description to use for the file. If not specified then the
description of the attachment is used instead.
.. versionadded:: 2.0
use_cached: :class:`bool`
Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading
the attachment. This will allow attachments to be saved after deletion
more often, compared to the regular URL which is generally deleted right
after the message is deleted. Note that this can still fail to download
deleted attachments if too much time has passed and it does not work
on some types of attachments.
.. versionadded:: 1.4
spoiler: :class:`bool`
Whether the file is a spoiler.
.. versionadded:: 1.4
Raises
------
HTTPException
Downloading the attachment failed.
Forbidden
You do not have permissions to access this attachment
NotFound
The attachment was deleted.
Returns
-------
:class:`File`
The attachment as a file suitable for sending.
"""
data = await self.read(use_cached=use_cached)
file_filename = filename if filename is not MISSING else self.filename
file_description = (
description if description is not MISSING else self.description
)
return File(
io.BytesIO(data),
filename=file_filename,
description=file_description,
spoiler=spoiler,
)
class Attachment(Hashable, AttachmentBase):
"""Represents an attachment from Discord.
.. container:: operations
.. describe:: str(x)
Returns the URL of the attachment.
.. describe:: x == y
Checks if the attachment is equal to another attachment.
.. describe:: x != y
Checks if the attachment is not equal to another attachment.
.. describe:: hash(x)
Returns the hash of the attachment.
.. versionchanged:: 1.7
Attachment can now be casted to :class:`str` and is hashable.
Attributes
------------
id: :class:`int`
The attachment ID.
size: :class:`int`
The attachment size in bytes.
height: Optional[:class:`int`]
The attachment's height, in pixels. Only applicable to images and videos.
width: Optional[:class:`int`]
The attachment's width, in pixels. Only applicable to images and videos.
filename: :class:`str`
The attachment's filename.
url: :class:`str`
The attachment URL. If the message this attachment was attached
to is deleted, then this will 404.
proxy_url: :class:`str`
The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the
case of images. When the message is deleted, this URL might be valid for a few
minutes or not valid at all.
content_type: Optional[:class:`str`]
The attachment's `media type <https://en.wikipedia.org/wiki/Media_type>`_
.. versionadded:: 1.7
description: Optional[:class:`str`]
The attachment's description. Only applicable to images.
.. versionadded:: 2.0
ephemeral: :class:`bool`
Whether the attachment is ephemeral.
.. versionadded:: 2.0
duration: Optional[:class:`float`]
The duration of the audio file in seconds. Returns ``None`` if it's not a voice message.
.. versionadded:: 2.3
waveform: Optional[:class:`bytes`]
The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message.
.. versionadded:: 2.3
title: Optional[:class:`str`]
The normalised version of the attachment's filename.
.. versionadded:: 2.5
"""
__slots__ = (
'id',
'size',
'height',
'width',
'filename',
'url',
'proxy_url',
'_http',
'content_type',
'description',
'ephemeral',
'duration',
'waveform',
'_flags',
'title',
)
def __init__(self, *, data: AttachmentPayload, state: ConnectionState):
self.id: int = int(data['id'])
self.size: int = data['size']
self.height: Optional[int] = data.get('height')
self.width: Optional[int] = data.get('width')
self.filename: str = data['filename']
self.url: str = data['url']
self.proxy_url: str = data['proxy_url']
self._http = state.http
self.content_type: Optional[str] = data.get('content_type')
self.description: Optional[str] = data.get('description')
self.ephemeral: bool = data.get('ephemeral', False)
self.duration: Optional[float] = data.get('duration_secs')
self.title: Optional[str] = data.get('title')
waveform = data.get('waveform')
self.waveform: Optional[bytes] = (
utils._base64_to_bytes(waveform) if waveform is not None else None
)
self._flags: int = data.get('flags', 0)
@property
def flags(self) -> AttachmentFlags:
""":class:`AttachmentFlags`: The attachment's flags."""
return AttachmentFlags._from_value(self._flags)
def is_spoiler(self) -> bool:
""":class:`bool`: Whether this attachment contains a spoiler."""
return self.filename.startswith('SPOILER_')
def is_voice_message(self) -> bool:
""":class:`bool`: Whether this attachment is a voice message."""
return self.duration is not None and 'voice-message' in self.url
def __repr__(self) -> str:
return f'<Attachment id={self.id} filename={self.filename!r} url={self.url!r}>'
def __str__(self) -> str:
return self.url or ''
def to_dict(self) -> AttachmentPayload:
result: AttachmentPayload = {
'filename': self.filename,
'id': self.id,
'proxy_url': self.proxy_url,
'size': self.size,
'url': self.url,
'spoiler': self.is_spoiler(),
}
if self.height:
result['height'] = self.height
if self.width:
result['width'] = self.width
if self.content_type:
result['content_type'] = self.content_type
if self.description is not None:
result['description'] = self.description
return result
class UnfurledAttachment(AttachmentBase):
"""Represents an unfurled attachment item from a :class:`Component`.
.. versionadded:: tbd
.. container:: operations
.. describe:: str(x)
Returns the URL of the attachment.
.. describe:: x == y
Checks if the unfurled attachment is equal to another unfurled attachment.
.. describe:: x != y
Checks if the unfurled attachment is not equal to another unfurled attachment.
Attributes
----------
url: :class:`str`
The unfurled attachment URL.
proxy_url: Optional[:class:`str`]
The proxy URL. This is cached version of the :attr:`~UnfurledAttachment.url` in the
case of images. When the message is deleted, this URL might be valid for a few
minutes or not valid at all.
.. note::
This will be ``None`` if :meth:`.is_resolved` is ``False``.
height: Optional[:class:`int`]
The unfurled attachment's height, in pixels.
.. note::
This will be ``None`` if :meth:`.is_resolved` is ``False``.
width: Optional[:class:`int`]
The unfurled attachment's width, in pixels.
.. note::
This will be ``None`` if :meth:`.is_resolved` is ``False``.
content_type: Optional[:class:`str`]
The attachment's `media type <https://en.wikipedia.org/wiki/Media_type>`_
.. note::
This will be ``None`` if :meth:`.is_resolved` is ``False``.
loading_state: :class:`MediaLoadingState`
The load state of this attachment on Discord side.
description
"""
__slots__ = (
'url',
'proxy_url',
'height',
'width',
'content_type',
'loading_state',
'_resolved',
'_state',
)
def __init__(self, )

176
discord/components.py

@ -4,7 +4,7 @@ The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
copy of this software and associated documentation files (the 'Software'),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
@ -13,7 +13,7 @@ Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
@ -24,8 +24,24 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload
from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, ChannelType, SelectDefaultValueType
from typing import (
ClassVar,
List,
Literal,
Optional,
TYPE_CHECKING,
Tuple,
Union,
)
from .enums import (
try_enum,
ComponentType,
ButtonStyle,
TextStyle,
ChannelType,
SelectDefaultValueType,
DividerSize,
)
from .utils import get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag
@ -33,14 +49,21 @@ if TYPE_CHECKING:
from typing_extensions import Self
from .types.components import (
ComponentBase as ComponentBasePayload,
Component as ComponentPayload,
ButtonComponent as ButtonComponentPayload,
SelectMenu as SelectMenuPayload,
SelectOption as SelectOptionPayload,
ActionRow as ActionRowPayload,
TextInput as TextInputPayload,
ActionRowChildComponent as ActionRowChildComponentPayload,
SelectDefaultValues as SelectDefaultValuesPayload,
SectionComponent as SectionComponentPayload,
TextComponent as TextComponentPayload,
ThumbnailComponent as ThumbnailComponentPayload,
MediaGalleryComponent as MediaGalleryComponentPayload,
FileComponent as FileComponentPayload,
DividerComponent as DividerComponentPayload,
ComponentContainer as ComponentContainerPayload,
)
from .emoji import Emoji
from .abc import Snowflake
@ -56,6 +79,13 @@ __all__ = (
'SelectOption',
'TextInput',
'SelectDefaultValue',
'SectionComponent',
'TextComponent',
'ThumbnailComponent',
'MediaGalleryComponent',
'FileComponent',
'DividerComponent',
'ComponentContainer',
)
@ -99,7 +129,7 @@ class Component:
setattr(self, slot, value)
return self
def to_dict(self) -> ComponentPayload:
def to_dict(self) -> ComponentBasePayload:
raise NotImplementedError
@ -290,9 +320,13 @@ class SelectMenu(Component):
self.placeholder: Optional[str] = data.get('placeholder')
self.min_values: int = data.get('min_values', 1)
self.max_values: int = data.get('max_values', 1)
self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])]
self.options: List[SelectOption] = [
SelectOption.from_dict(option) for option in data.get('options', [])
]
self.disabled: bool = data.get('disabled', False)
self.channel_types: List[ChannelType] = [try_enum(ChannelType, t) for t in data.get('channel_types', [])]
self.channel_types: List[ChannelType] = [
try_enum(ChannelType, t) for t in data.get('channel_types', [])
]
self.default_values: List[SelectDefaultValue] = [
SelectDefaultValue.from_dict(d) for d in data.get('default_values', [])
]
@ -312,7 +346,7 @@ class SelectMenu(Component):
if self.channel_types:
payload['channel_types'] = [t.value for t in self.channel_types]
if self.default_values:
payload["default_values"] = [v.to_dict() for v in self.default_values]
payload['default_values'] = [v.to_dict() for v in self.default_values]
return payload
@ -408,7 +442,9 @@ class SelectOption:
elif isinstance(value, _EmojiTag):
self._emoji = value._to_partial()
else:
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__.__name__} instead')
raise TypeError(
f'expected str, Emoji, or PartialEmoji, received {value.__class__.__name__} instead'
)
else:
self._emoji = None
@ -564,7 +600,9 @@ class SelectDefaultValue:
@type.setter
def type(self, value: SelectDefaultValueType) -> None:
if not isinstance(value, SelectDefaultValueType):
raise TypeError(f'expected SelectDefaultValueType, received {value.__class__.__name__} instead')
raise TypeError(
f'expected SelectDefaultValueType, received {value.__class__.__name__} instead'
)
self._type = value
@ -642,17 +680,105 @@ class SelectDefaultValue:
)
@overload
def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]:
...
class SectionComponent(Component):
"""Represents a section from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type to create a section is :class:`discord.ui.Section`
not this one.
.. versionadded:: tbd
Attributes
----------
components: List[Union[:class:`TextDisplay`, :class:`Button`]]
The components on this section.
accessory: Optional[:class:`Component`]
The section accessory.
"""
def __init__(self, data: SectionComponentPayload) -> None:
self.components: List[Union[TextDisplay, Button]] = []
for component_data in data['components']:
component = _component_factory(component_data)
if component is not None:
self.components.append(component)
try:
self.accessory: Optional[Component] = _component_factory(data['accessory'])
except KeyError:
self.accessory = None
@property
def type(self) -> Literal[ComponentType.section]:
return ComponentType.section
def to_dict(self) -> SectionComponentPayload:
payload: SectionComponentPayload = {
'type': self.type.value,
'components': [c.to_dict() for c in self.components],
}
if self.accessory:
payload['accessory'] = self.accessory.to_dict()
return payload
class TextDisplay(Component):
"""Represents a text display from the Discord Bot UI Kit.
@overload
def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]:
...
This inherits from :class:`Component`.
.. versionadded:: tbd
Parameters
----------
content: :class:`str`
The content that this display shows.
"""
def __init__(self, content: str) -> None:
self.content: str = content
@property
def type(self) -> Literal[ComponentType.text_display]:
return ComponentType.text_display
@classmethod
def _from_data(cls, data: TextComponentPayload) -> TextDisplay:
return cls(
content=data['content'],
)
def to_dict(self) -> TextComponentPayload:
return {
'type': self.type.value,
'content': self.content,
}
class ThumbnailComponent(Component):
"""Represents a thumbnail display from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructuble and usable type to create a thumbnail
component is :class:`discord.ui.Thumbnail` not this one.
.. versionadded:: tbd
Attributes
----------
media: :class:`ComponentMedia`
"""
def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]:
def _component_factory(data: ComponentPayload) -> Optional[Component]:
if data['type'] == 1:
return ActionRow(data)
elif data['type'] == 2:
@ -661,3 +787,17 @@ def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, Acti
return TextInput(data)
elif data['type'] in (3, 5, 6, 7, 8):
return SelectMenu(data)
elif data['type'] == 9:
return SectionComponent(data)
elif data['type'] == 10:
return TextDisplay._from_data(data)
elif data['type'] == 11:
return ThumbnailComponent(data)
elif data['type'] == 12:
return MediaGalleryComponent(data)
elif data['type'] == 13:
return FileComponent(data)
elif data['type'] == 14:
return DividerComponent(data)
elif data['type'] == 17:
return ComponentContainer(data)

21
discord/enums.py

@ -77,6 +77,8 @@ __all__ = (
'VoiceChannelEffectAnimationType',
'SubscriptionStatus',
'MessageReferenceType',
'DividerSize',
'MediaLoadingState',
)
@ -641,6 +643,13 @@ class ComponentType(Enum):
role_select = 6
mentionable_select = 7
channel_select = 8
section = 9
text_display = 10
thumbnail = 11
media_gallery = 12
file = 13
separator = 14
container = 17
def __int__(self) -> int:
return self.value
@ -863,6 +872,18 @@ class SubscriptionStatus(Enum):
inactive = 2
class DividerSize(Enum):
small = 1
large = 2
class MediaLoadingState(Enum):
unknown = 0
loading = 1
loaded = 2
not_found = 3
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}'

8
discord/flags.py

@ -498,6 +498,14 @@ class MessageFlags(BaseFlags):
"""
return 16384
@flag_value
def components_v2(self):
""":class:`bool`: Returns ``True`` if the message has Discord's v2 components.
Does not allow sending any ``content``, ``embed``, or ``embeds``.
"""
return 32768
@fill_with_flags()
class PublicUserFlags(BaseFlags):

298
discord/message.py

@ -27,8 +27,6 @@ from __future__ import annotations
import asyncio
import datetime
import re
import io
from os import PathLike
from typing import (
Dict,
TYPE_CHECKING,
@ -55,7 +53,7 @@ from .errors import HTTPException
from .components import _component_factory
from .embeds import Embed
from .member import Member
from .flags import MessageFlags, AttachmentFlags
from .flags import MessageFlags
from .file import File
from .utils import escape_mentions, MISSING, deprecated
from .http import handle_message_parameters
@ -65,6 +63,7 @@ from .sticker import StickerItem, GuildSticker
from .threads import Thread
from .channel import PartialMessageable
from .poll import Poll
from .attachment import Attachment
if TYPE_CHECKING:
from typing_extensions import Self
@ -108,7 +107,6 @@ if TYPE_CHECKING:
__all__ = (
'Attachment',
'Message',
'PartialMessage',
'MessageInteraction',
@ -140,298 +138,6 @@ def convert_emoji_reaction(emoji: Union[EmojiInputType, Reaction]) -> str:
raise TypeError(f'emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}.')
class Attachment(Hashable):
"""Represents an attachment from Discord.
.. container:: operations
.. describe:: str(x)
Returns the URL of the attachment.
.. describe:: x == y
Checks if the attachment is equal to another attachment.
.. describe:: x != y
Checks if the attachment is not equal to another attachment.
.. describe:: hash(x)
Returns the hash of the attachment.
.. versionchanged:: 1.7
Attachment can now be casted to :class:`str` and is hashable.
Attributes
------------
id: :class:`int`
The attachment ID.
size: :class:`int`
The attachment size in bytes.
height: Optional[:class:`int`]
The attachment's height, in pixels. Only applicable to images and videos.
width: Optional[:class:`int`]
The attachment's width, in pixels. Only applicable to images and videos.
filename: :class:`str`
The attachment's filename.
url: :class:`str`
The attachment URL. If the message this attachment was attached
to is deleted, then this will 404.
proxy_url: :class:`str`
The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the
case of images. When the message is deleted, this URL might be valid for a few
minutes or not valid at all.
content_type: Optional[:class:`str`]
The attachment's `media type <https://en.wikipedia.org/wiki/Media_type>`_
.. versionadded:: 1.7
description: Optional[:class:`str`]
The attachment's description. Only applicable to images.
.. versionadded:: 2.0
ephemeral: :class:`bool`
Whether the attachment is ephemeral.
.. versionadded:: 2.0
duration: Optional[:class:`float`]
The duration of the audio file in seconds. Returns ``None`` if it's not a voice message.
.. versionadded:: 2.3
waveform: Optional[:class:`bytes`]
The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message.
.. versionadded:: 2.3
title: Optional[:class:`str`]
The normalised version of the attachment's filename.
.. versionadded:: 2.5
"""
__slots__ = (
'id',
'size',
'height',
'width',
'filename',
'url',
'proxy_url',
'_http',
'content_type',
'description',
'ephemeral',
'duration',
'waveform',
'_flags',
'title',
)
def __init__(self, *, data: AttachmentPayload, state: ConnectionState):
self.id: int = int(data['id'])
self.size: int = data['size']
self.height: Optional[int] = data.get('height')
self.width: Optional[int] = data.get('width')
self.filename: str = data['filename']
self.url: str = data['url']
self.proxy_url: str = data['proxy_url']
self._http = state.http
self.content_type: Optional[str] = data.get('content_type')
self.description: Optional[str] = data.get('description')
self.ephemeral: bool = data.get('ephemeral', False)
self.duration: Optional[float] = data.get('duration_secs')
self.title: Optional[str] = data.get('title')
waveform = data.get('waveform')
self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None
self._flags: int = data.get('flags', 0)
@property
def flags(self) -> AttachmentFlags:
""":class:`AttachmentFlags`: The attachment's flags."""
return AttachmentFlags._from_value(self._flags)
def is_spoiler(self) -> bool:
""":class:`bool`: Whether this attachment contains a spoiler."""
return self.filename.startswith('SPOILER_')
def is_voice_message(self) -> bool:
""":class:`bool`: Whether this attachment is a voice message."""
return self.duration is not None and 'voice-message' in self.url
def __repr__(self) -> str:
return f'<Attachment id={self.id} filename={self.filename!r} url={self.url!r}>'
def __str__(self) -> str:
return self.url or ''
async def save(
self,
fp: Union[io.BufferedIOBase, PathLike[Any]],
*,
seek_begin: bool = True,
use_cached: bool = False,
) -> int:
"""|coro|
Saves this attachment into a file-like object.
Parameters
-----------
fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`]
The file-like object to save this attachment to or the filename
to use. If a filename is passed then a file is created with that
filename and used instead.
seek_begin: :class:`bool`
Whether to seek to the beginning of the file after saving is
successfully done.
use_cached: :class:`bool`
Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading
the attachment. This will allow attachments to be saved after deletion
more often, compared to the regular URL which is generally deleted right
after the message is deleted. Note that this can still fail to download
deleted attachments if too much time has passed and it does not work
on some types of attachments.
Raises
--------
HTTPException
Saving the attachment failed.
NotFound
The attachment was deleted.
Returns
--------
:class:`int`
The number of bytes written.
"""
data = await self.read(use_cached=use_cached)
if isinstance(fp, io.BufferedIOBase):
written = fp.write(data)
if seek_begin:
fp.seek(0)
return written
else:
with open(fp, 'wb') as f:
return f.write(data)
async def read(self, *, use_cached: bool = False) -> bytes:
"""|coro|
Retrieves the content of this attachment as a :class:`bytes` object.
.. versionadded:: 1.1
Parameters
-----------
use_cached: :class:`bool`
Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading
the attachment. This will allow attachments to be saved after deletion
more often, compared to the regular URL which is generally deleted right
after the message is deleted. Note that this can still fail to download
deleted attachments if too much time has passed and it does not work
on some types of attachments.
Raises
------
HTTPException
Downloading the attachment failed.
Forbidden
You do not have permissions to access this attachment
NotFound
The attachment was deleted.
Returns
-------
:class:`bytes`
The contents of the attachment.
"""
url = self.proxy_url if use_cached else self.url
data = await self._http.get_from_cdn(url)
return data
async def to_file(
self,
*,
filename: Optional[str] = MISSING,
description: Optional[str] = MISSING,
use_cached: bool = False,
spoiler: bool = False,
) -> File:
"""|coro|
Converts the attachment into a :class:`File` suitable for sending via
:meth:`abc.Messageable.send`.
.. versionadded:: 1.3
Parameters
-----------
filename: Optional[:class:`str`]
The filename to use for the file. If not specified then the filename
of the attachment is used instead.
.. versionadded:: 2.0
description: Optional[:class:`str`]
The description to use for the file. If not specified then the
description of the attachment is used instead.
.. versionadded:: 2.0
use_cached: :class:`bool`
Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading
the attachment. This will allow attachments to be saved after deletion
more often, compared to the regular URL which is generally deleted right
after the message is deleted. Note that this can still fail to download
deleted attachments if too much time has passed and it does not work
on some types of attachments.
.. versionadded:: 1.4
spoiler: :class:`bool`
Whether the file is a spoiler.
.. versionadded:: 1.4
Raises
------
HTTPException
Downloading the attachment failed.
Forbidden
You do not have permissions to access this attachment
NotFound
The attachment was deleted.
Returns
-------
:class:`File`
The attachment as a file suitable for sending.
"""
data = await self.read(use_cached=use_cached)
file_filename = filename if filename is not MISSING else self.filename
file_description = description if description is not MISSING else self.description
return File(io.BytesIO(data), filename=file_filename, description=file_description, spoiler=spoiler)
def to_dict(self) -> AttachmentPayload:
result: AttachmentPayload = {
'filename': self.filename,
'id': self.id,
'proxy_url': self.proxy_url,
'size': self.size,
'url': self.url,
'spoiler': self.is_spoiler(),
}
if self.height:
result['height'] = self.height
if self.width:
result['width'] = self.width
if self.content_type:
result['content_type'] = self.content_type
if self.description is not None:
result['description'] = self.description
return result
class DeletedReferencedMessage:
"""A special sentinel type given when the resolved message reference
points to a deleted message.

58
discord/types/attachment.py

@ -0,0 +1,58 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Literal, Optional, TypedDict
from typing_extensions import NotRequired
from .snowflake import Snowflake
LoadingState = Literal[0, 1, 2, 3]
class AttachmentBase(TypedDict):
url: str
proxy_url: str
description: NotRequired[str]
spoiler: NotRequired[bool]
height: NotRequired[Optional[int]]
width: NotRequired[Optional[int]]
content_type: NotRequired[str]
flags: NotRequired[int]
class Attachment(AttachmentBase):
id: Snowflake
filename: str
size: int
ephemeral: NotRequired[bool]
duration_secs: NotRequired[float]
waveform: NotRequired[str]
class UnfurledAttachment(AttachmentBase):
loading_state: LoadingState
src_is_animated: NotRequired[bool]
placeholder: str
placeholder_version: int

69
discord/types/components.py

@ -29,19 +29,27 @@ from typing_extensions import NotRequired
from .emoji import PartialEmoji
from .channel import ChannelType
from .attachment import UnfurledAttachment
ComponentType = Literal[1, 2, 3, 4]
ButtonStyle = Literal[1, 2, 3, 4, 5, 6]
TextStyle = Literal[1, 2]
DefaultValueType = Literal['user', 'role', 'channel']
DefaultValueType = Literal["user", "role", "channel"]
DividerSize = Literal[1, 2]
MediaItemLoadingState = Literal[0, 1, 2, 3]
class ActionRow(TypedDict):
class ComponentBase(TypedDict):
id: NotRequired[int]
type: int
class ActionRow(ComponentBase):
type: Literal[1]
components: List[ActionRowChildComponent]
class ButtonComponent(TypedDict):
class ButtonComponent(ComponentBase):
type: Literal[2]
style: ButtonStyle
custom_id: NotRequired[str]
@ -52,7 +60,7 @@ class ButtonComponent(TypedDict):
sku_id: NotRequired[str]
class SelectOption(TypedDict):
class SelectOption(ComponentBase):
label: str
value: str
default: bool
@ -60,7 +68,7 @@ class SelectOption(TypedDict):
emoji: NotRequired[PartialEmoji]
class SelectComponent(TypedDict):
class SelectComponent(ComponentBase):
custom_id: str
placeholder: NotRequired[str]
min_values: NotRequired[int]
@ -99,7 +107,7 @@ class ChannelSelectComponent(SelectComponent):
default_values: NotRequired[List[SelectDefaultValues]]
class TextInput(TypedDict):
class TextInput(ComponentBase):
type: Literal[4]
custom_id: str
style: TextStyle
@ -118,5 +126,52 @@ class SelectMenu(SelectComponent):
default_values: NotRequired[List[SelectDefaultValues]]
class SectionComponent(ComponentBase):
type: Literal[9]
components: List[Union[TextComponent, ButtonComponent]]
accessory: NotRequired[ComponentBase]
class TextComponent(ComponentBase):
type: Literal[10]
content: str
class ThumbnailComponent(ComponentBase, UnfurledAttachment):
type: Literal[11]
class MediaGalleryComponent(ComponentBase):
type: Literal[12]
items: List[MediaItem]
class FileComponent(ComponentBase):
type: Literal[13]
file: MediaItem
spoiler: NotRequired[bool]
class DividerComponent(ComponentBase):
type: Literal[14]
divider: NotRequired[bool]
spacing: NotRequired[DividerSize]
class ComponentContainer(ComponentBase):
type: Literal[17]
accent_color: NotRequired[int]
spoiler: NotRequired[bool]
components: List[ContainerComponent]
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput]
Component = Union[ActionRow, ActionRowChildComponent]
ContainerComponent = Union[
ActionRow,
TextComponent,
MediaGalleryComponent,
FileComponent,
SectionComponent,
SectionComponent,
]
Component = Union[ActionRowChildComponent, ContainerComponent]

26
discord/types/message.py

@ -33,11 +33,12 @@ from .user import User
from .emoji import PartialEmoji
from .embed import Embed
from .channel import ChannelType
from .components import Component
from .components import ComponentBase
from .interactions import MessageInteraction, MessageInteractionMetadata
from .sticker import StickerItem
from .threads import Thread
from .poll import Poll
from .attachment import Attachment
class PartialMessage(TypedDict):
@ -69,23 +70,6 @@ class Reaction(TypedDict):
burst_colors: List[str]
class Attachment(TypedDict):
id: Snowflake
filename: str
size: int
url: str
proxy_url: str
height: NotRequired[Optional[int]]
width: NotRequired[Optional[int]]
description: NotRequired[str]
content_type: NotRequired[str]
spoiler: NotRequired[bool]
ephemeral: NotRequired[bool]
duration_secs: NotRequired[float]
waveform: NotRequired[str]
flags: NotRequired[int]
MessageActivityType = Literal[1, 2, 3, 5]
@ -189,7 +173,7 @@ class MessageSnapshot(TypedDict):
mentions: List[UserWithMember]
mention_roles: SnowflakeList
sticker_items: NotRequired[List[StickerItem]]
components: NotRequired[List[Component]]
components: NotRequired[List[ComponentBase]]
class Message(PartialMessage):
@ -221,7 +205,7 @@ class Message(PartialMessage):
referenced_message: NotRequired[Optional[Message]]
interaction: NotRequired[MessageInteraction] # deprecated, use interaction_metadata
interaction_metadata: NotRequired[MessageInteractionMetadata]
components: NotRequired[List[Component]]
components: NotRequired[List[ComponentBase]]
position: NotRequired[int]
role_subscription_data: NotRequired[RoleSubscriptionData]
thread: NotRequired[Thread]
@ -229,7 +213,7 @@ class Message(PartialMessage):
purchase_notification: NotRequired[PurchaseNotificationResponse]
AllowedMentionType = Literal['roles', 'users', 'everyone']
AllowedMentionType = Literal["roles", "users", "everyone"]
class AllowedMentions(TypedDict):

50
discord/ui/section.py

@ -0,0 +1,50 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Optional
from .item import Item
class Section(Item):
"""Represents a UI section.
.. versionadded:: tbd
Parameters
----------
accessory: Optional[:class:`Item`]
The accessory to show within this section, displayed on the top right of this section.
"""
__slots__ = (
'accessory',
'_children',
)
def __init__(self, *, accessory: Optional[Item]) -> None:
self.accessory: Optional[Item] = accessory
self._children: List[Item] = []
self._underlying = SectionComponent
Loading…
Cancel
Save