Browse Source

chore: undo attachment file move

pull/10166/head
DA-344 4 months ago
parent
commit
4aef97e249
  1. 421
      discord/attachment.py
  2. 116
      discord/components.py
  3. 4
      discord/enums.py
  4. 19
      discord/ui/thumbnail.py
  5. 3
      discord/ui/view.py

421
discord/attachment.py

@ -1,421 +0,0 @@
"""
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 .errors import ClientException
from .mixins import Hashable
from .file import File
from .flags import AttachmentFlags
from .enums import MediaLoadingState, try_enum
from . import utils
if TYPE_CHECKING:
from .types.attachment import (
AttachmentBase as AttachmentBasePayload,
Attachment as AttachmentPayload,
UnfurledAttachment as UnfurledAttachmentPayload,
)
from .http import HTTPClient
from .state import ConnectionState
MISSING = utils.MISSING
__all__ = (
'Attachment',
'UnfurledAttachment',
)
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
spoiler: :class:`bool`
Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned
data.
.. versionadded:: 2.6
"""
__slots__ = (
'id',
'size',
'ephemeral',
'duration',
'waveform',
'title',
'url',
'proxy_url',
'description',
'filename',
'spoiler',
'height',
'width',
'content_type',
'_flags',
'_http',
'_state',
)
def __init__(self, *, data: AttachmentPayload, state: Optional[ConnectionState]):
self.id: int = int(data['id'])
self.filename: str = data['filename']
self.size: int = data['size']
self.ephemeral: bool = data.get('ephemeral', False)
self.duration: Optional[float] = data.get('duration_secs')
self.title: Optional[str] = data.get('title')
self._state: Optional[ConnectionState] = state
self._http: Optional[HTTPClient] = state.http if state else None
self.url: str = data['url']
self.proxy_url: str = data['proxy_url']
self.description: Optional[str] = data.get('description')
self.spoiler: bool = data.get('spoiler', False)
self.height: Optional[int] = data.get('height')
self.width: Optional[int] = data.get('width')
self.content_type: Optional[str] = data.get('content_type')
self._flags: int = data.get('flags', 0)
@property
def flags(self) -> AttachmentFlags:
""":class:`AttachmentFlags`: The attachment's flag value."""
return AttachmentFlags._from_value(self._flags)
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.
ClientException
Cannot read a stateless attachment.
Returns
-------
:class:`bytes`
The contents of the attachment.
"""
if not self._http:
raise ClientException(
'Cannot read a stateless 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 is_spoiler(self) -> bool:
""":class:`bool`: Whether this attachment contains a spoiler."""
return self.spoiler or 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 to_dict(self) -> AttachmentPayload:
base: AttachmentPayload = {
'url': self.url,
'proxy_url': self.proxy_url,
'spoiler': self.spoiler,
'id': self.id,
'filename': self.filename,
'size': self.size,
}
if self.width:
base['width'] = self.width
if self.height:
base['height'] = self.height
if self.description:
base['description'] = self.description
return base
class UnfurledAttachment(Attachment):
"""Represents an unfurled attachment item from a :class:`Component`.
.. versionadded:: 2.6
.. 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
----------
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.
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>`_
description: Optional[:class:`str`]
The attachment's description. Only applicable to images.
spoiler: :class:`bool`
Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned
data.
loading_state: :class:`MediaLoadingState`
The cache state of this unfurled attachment.
"""
__slots__ = (
'loading_state',
)
def __init__(self, data: UnfurledAttachmentPayload, state: Optional[ConnectionState]) -> None:
self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data.get('loading_state', 0))
super().__init__(data={'id': 0, 'filename': '', 'size': 0, **data}, state=state) # type: ignore
def __repr__(self) -> str:
return f'<UnfurledAttachment url={self.url!r}>'
def to_object_dict(self):
return {'url': self.url}

116
discord/components.py

@ -34,7 +34,7 @@ from typing import (
Union, Union,
) )
from .attachment import UnfurledAttachment from .asset import AssetMixin
from .enums import ( from .enums import (
try_enum, try_enum,
ComponentType, ComponentType,
@ -43,7 +43,9 @@ from .enums import (
ChannelType, ChannelType,
SelectDefaultValueType, SelectDefaultValueType,
SeparatorSize, SeparatorSize,
MediaItemLoadingState,
) )
from .flags import AttachmentFlags
from .colour import Colour from .colour import Colour
from .utils import get_slots, MISSING from .utils import get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag from .partial_emoji import PartialEmoji, _EmojiTag
@ -68,6 +70,7 @@ if TYPE_CHECKING:
MediaGalleryItem as MediaGalleryItemPayload, MediaGalleryItem as MediaGalleryItemPayload,
ThumbnailComponent as ThumbnailComponentPayload, ThumbnailComponent as ThumbnailComponentPayload,
ContainerComponent as ContainerComponentPayload, ContainerComponent as ContainerComponentPayload,
UnfurledMediaItem as UnfurledMediaItemPayload,
) )
from .emoji import Emoji from .emoji import Emoji
@ -773,7 +776,7 @@ class ThumbnailComponent(Component):
data: ThumbnailComponentPayload, data: ThumbnailComponentPayload,
state: Optional[ConnectionState], state: Optional[ConnectionState],
) -> None: ) -> None:
self.media: UnfurledAttachment = UnfurledAttachment(data['media'], state) self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['media'], state)
self.description: Optional[str] = data.get('description') self.description: Optional[str] = data.get('description')
self.spoiler: bool = data.get('spoiler', False) self.spoiler: bool = data.get('spoiler', False)
@ -817,15 +820,96 @@ class TextDisplay(Component):
} }
class UnfurledMediaItem(AssetMixin):
"""Represents an unfurled media item that can be used on
:class:`MediaGalleryItem`s.
Unlike :class:`UnfurledAttachment` this represents a media item
not yet stored on Discord and thus it does not have any data.
Parameters
----------
url: :class:`str`
The URL of this media item.
Attributes
----------
proxy_url: Optional[:class:`str`]
The proxy URL. This is a cached version of the :attr:`~UnfurledMediaItem.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.
height: Optional[:class:`int`]
The media item's height, in pixels. Only applicable to images and videos.
width: Optional[:class:`int`]
The media item's width, in pixels. Only applicable to images and videos.
content_type: Optional[:class:`str`]
The media item's `media type <https://en.wikipedia.org/wiki/Media_type>`_
placeholder: Optional[:class:`str`]
The media item's placeholder.
loading_state: Optional[:class:`MediaItemLoadingState`]
The loading state of this media item.
"""
__slots__ = (
'url',
'proxy_url',
'height',
'width',
'content_type',
'_flags',
'placeholder',
'loading_state',
'_state',
)
def __init__(self, url: str) -> None:
self.url: str = url
self.proxy_url: Optional[str] = None
self.height: Optional[int] = None
self.width: Optional[int] = None
self.content_type: Optional[str] = None
self._flags: int = 0
self.placeholder: Optional[str] = None
self.loading_state: Optional[MediaItemLoadingState] = None
self._state: Optional[ConnectionState] = None
@property
def flags(self) -> AttachmentFlags:
""":class:`AttachmentFlags`: This media item's flags."""
return AttachmentFlags._from_value(self._flags)
@classmethod
def _from_data(cls, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]):
self = cls(data['url'])
self._update(data, state)
return self
def _update(self, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]) -> None:
self.proxy_url = data['proxy_url']
self.height = data.get('height')
self.width = data.get('width')
self.content_type = data.get('content_type')
self._flags = data.get('flags', 0)
self.placeholder = data.get('placeholder')
self.loading_state = try_enum(MediaItemLoadingState, data['loading_state'])
self._state = state
def to_dict(self):
return {
'url': self.url,
}
class MediaGalleryItem: class MediaGalleryItem:
"""Represents a :class:`MediaGalleryComponent` media item. """Represents a :class:`MediaGalleryComponent` media item.
Parameters Parameters
---------- ----------
url: :class:`str` media: Union[:class:`str`, :class:`UnfurledMediaItem`]
The url of the media item. This can be a local file uploaded The media item data. This can be a string representing a local
as an attachment in the message, that can be accessed using file uploaded as an attachment in the message, that can be accessed
the ``attachment://file-name.extension`` format. using the ``attachment://file-name.extension`` format.
description: Optional[:class:`str`] description: Optional[:class:`str`]
The description to show within this item. The description to show within this item.
spoiler: :class:`bool` spoiler: :class:`bool`
@ -833,7 +917,7 @@ class MediaGalleryItem:
""" """
__slots__ = ( __slots__ = (
'url', 'media',
'description', 'description',
'spoiler', 'spoiler',
'_state', '_state',
@ -841,12 +925,12 @@ class MediaGalleryItem:
def __init__( def __init__(
self, self,
url: str, media: Union[str, UnfurledMediaItem],
*, *,
description: Optional[str] = None, description: Optional[str] = None,
spoiler: bool = False, spoiler: bool = False,
) -> None: ) -> None:
self.url: str = url self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media
self.description: Optional[str] = description self.description: Optional[str] = description
self.spoiler: bool = spoiler self.spoiler: bool = spoiler
self._state: Optional[ConnectionState] = None self._state: Optional[ConnectionState] = None
@ -857,7 +941,7 @@ class MediaGalleryItem:
) -> MediaGalleryItem: ) -> MediaGalleryItem:
media = data['media'] media = data['media']
self = cls( self = cls(
url=media['url'], media=media['url'],
description=data.get('description'), description=data.get('description'),
spoiler=data.get('spoiler', False), spoiler=data.get('spoiler', False),
) )
@ -873,8 +957,8 @@ class MediaGalleryItem:
return [cls._from_data(item, state) for item in items] return [cls._from_data(item, state) for item in items]
def to_dict(self) -> MediaGalleryItemPayload: def to_dict(self) -> MediaGalleryItemPayload:
return { # type: ignore return {
'media': {'url': self.url}, 'media': self.media.to_dict(), # type: ignore
'description': self.description, 'description': self.description,
'spoiler': self.spoiler, 'spoiler': self.spoiler,
} }
@ -927,9 +1011,7 @@ class FileComponent(Component):
) )
def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None: def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None:
self.media: UnfurledAttachment = UnfurledAttachment( self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state)
data['file'], state,
)
self.spoiler: bool = data.get('spoiler', False) self.spoiler: bool = data.get('spoiler', False)
@property @property
@ -937,8 +1019,8 @@ class FileComponent(Component):
return ComponentType.file return ComponentType.file
def to_dict(self) -> FileComponentPayload: def to_dict(self) -> FileComponentPayload:
return { # type: ignore return {
'file': {'url': self.url}, 'file': self.media.to_dict(), # type: ignore
'spoiler': self.spoiler, 'spoiler': self.spoiler,
'type': self.type.value, 'type': self.type.value,
} }

4
discord/enums.py

@ -78,7 +78,7 @@ __all__ = (
'SubscriptionStatus', 'SubscriptionStatus',
'MessageReferenceType', 'MessageReferenceType',
'SeparatorSize', 'SeparatorSize',
'MediaLoadingState', 'MediaItemLoadingState',
) )
@ -877,7 +877,7 @@ class SeparatorSize(Enum):
large = 2 large = 2
class MediaLoadingState(Enum): class MediaItemLoadingState(Enum):
unknown = 0 unknown = 0
loading = 1 loading = 1
loaded = 2 loaded = 2

19
discord/ui/thumbnail.py

@ -23,10 +23,11 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar, Union
from .item import Item from .item import Item
from ..enums import ComponentType from ..enums import ComponentType
from ..components import UnfurledMediaItem
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
@ -47,9 +48,9 @@ class Thumbnail(Item[V]):
Parameters Parameters
---------- ----------
url: :class:`str` media: Union[:class:`str`, :class:`UnfurledMediaItem`]
The URL of the thumbnail. This can only point to a local attachment uploaded The media of the thumbnail. This can be a string that points to a local
within this item. URLs must match the ``attachment://file-name.extension`` attachment uploaded within this item. URLs must match the ``attachment://file-name.extension``
structure. structure.
description: Optional[:class:`str`] description: Optional[:class:`str`]
The description of this thumbnail. Defaults to ``None``. The description of this thumbnail. Defaults to ``None``.
@ -57,11 +58,13 @@ class Thumbnail(Item[V]):
Whether to flag this thumbnail as a spoiler. Defaults to ``False``. Whether to flag this thumbnail as a spoiler. Defaults to ``False``.
""" """
def __init__(self, url: str, *, description: Optional[str] = None, spoiler: bool = False) -> None: def __init__(self, media: Union[str, UnfurledMediaItem], *, description: Optional[str] = None, spoiler: bool = False) -> None:
self.url: str = url self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media
self.description: Optional[str] = description self.description: Optional[str] = description
self.spoiler: bool = spoiler self.spoiler: bool = spoiler
self._underlying = ThumbnailComponent._raw_construct()
@property @property
def type(self) -> Literal[ComponentType.thumbnail]: def type(self) -> Literal[ComponentType.thumbnail]:
return ComponentType.thumbnail return ComponentType.thumbnail
@ -73,14 +76,14 @@ class Thumbnail(Item[V]):
return { return {
'type': self.type.value, 'type': self.type.value,
'spoiler': self.spoiler, 'spoiler': self.spoiler,
'media': {'url': self.url}, 'media': self.media.to_dict(),
'description': self.description, 'description': self.description,
} }
@classmethod @classmethod
def from_component(cls, component: ThumbnailComponent) -> Self: def from_component(cls, component: ThumbnailComponent) -> Self:
return cls( return cls(
url=component.media.url, media=component.media.url,
description=component.description, description=component.description,
spoiler=component.spoiler, spoiler=component.spoiler,
) )

3
discord/ui/view.py

@ -255,6 +255,9 @@ class View:
# instead of grouping by row we will sort it so it is added # instead of grouping by row we will sort it so it is added
# in order and should work as the original implementation # in order and should work as the original implementation
# this will append directly the v2 Components into the list
# and will add to an action row the loose items, such as
# buttons and selects
for child in sorted(self._children, key=key): for child in sorted(self._children, key=key):
if child._is_v2(): if child._is_v2():
components.append(child.to_component_dict()) components.append(child.to_component_dict())

Loading…
Cancel
Save