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