Browse Source

chore: message attachments things

pull/10166/head
DA-344 3 months ago
parent
commit
2b14f0b014
  1. 302
      discord/message.py

302
discord/message.py

@ -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

Loading…
Cancel
Save