9 changed files with 781 additions and 342 deletions
@ -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, ) |
@ -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 |
@ -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…
Reference in new issue