You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
13 KiB
417 lines
13 KiB
"""
|
|
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, )
|
|
|