""" 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 `_ .. 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'' 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 `_ .. 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, )