|
|
@ -27,6 +27,8 @@ from __future__ import annotations |
|
|
|
import asyncio |
|
|
|
import datetime |
|
|
|
import re |
|
|
|
import io |
|
|
|
from os import PathLike |
|
|
|
from typing import ( |
|
|
|
Dict, |
|
|
|
TYPE_CHECKING, |
|
|
@ -53,7 +55,7 @@ from .errors import HTTPException |
|
|
|
from .components import _component_factory |
|
|
|
from .embeds import Embed |
|
|
|
from .member import Member |
|
|
|
from .flags import MessageFlags |
|
|
|
from .flags import MessageFlags, AttachmentFlags |
|
|
|
from .file import File |
|
|
|
from .utils import escape_mentions, MISSING, deprecated |
|
|
|
from .http import handle_message_parameters |
|
|
@ -63,7 +65,6 @@ from .sticker import StickerItem, GuildSticker |
|
|
|
from .threads import Thread |
|
|
|
from .channel import PartialMessageable |
|
|
|
from .poll import Poll |
|
|
|
from .attachment import Attachment |
|
|
|
|
|
|
|
if TYPE_CHECKING: |
|
|
|
from typing_extensions import Self |
|
|
@ -107,6 +108,7 @@ if TYPE_CHECKING: |
|
|
|
|
|
|
|
|
|
|
|
__all__ = ( |
|
|
|
'Attachment', |
|
|
|
'Message', |
|
|
|
'PartialMessage', |
|
|
|
'MessageInteraction', |
|
|
@ -138,6 +140,298 @@ def convert_emoji_reaction(emoji: Union[EmojiInputType, Reaction]) -> str: |
|
|
|
raise TypeError(f'emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}.') |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
""" |
|
|
|
|
|
|
|
__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 '' |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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 DeletedReferencedMessage: |
|
|
|
"""A special sentinel type given when the resolved message reference |
|
|
|
points to a deleted message. |
|
|
@ -238,9 +532,9 @@ class MessageSnapshot: |
|
|
|
|
|
|
|
self.components: List[MessageComponentType] = [] |
|
|
|
for component_data in data.get('components', []): |
|
|
|
component = _component_factory(component_data, state) |
|
|
|
component = _component_factory(component_data, state) # type: ignore |
|
|
|
if component is not None: |
|
|
|
self.components.append(component) |
|
|
|
self.components.append(component) # type: ignore |
|
|
|
|
|
|
|
self._state: ConnectionState = state |
|
|
|
|
|
|
|