Browse Source

Implement GCP cloud uploads

pull/10109/head
dolfies 2 years ago
parent
commit
aa2529c027
  1. 58
      discord/abc.py
  2. 10
      discord/ext/commands/context.py
  3. 167
      discord/file.py
  4. 31
      discord/http.py
  5. 29
      discord/message.py
  6. 16
      discord/types/message.py
  7. 7
      docs/api.rst

58
discord/abc.py

@ -54,7 +54,7 @@ from .mentions import AllowedMentions
from .permissions import PermissionOverwrite, Permissions from .permissions import PermissionOverwrite, Permissions
from .role import Role from .role import Role
from .invite import Invite from .invite import Invite
from .file import File from .file import File, CloudFile
from .http import handle_message_parameters from .http import handle_message_parameters
from .voice_client import VoiceClient, VoiceProtocol from .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem from .sticker import GuildSticker, StickerItem
@ -79,6 +79,7 @@ if TYPE_CHECKING:
from .client import Client from .client import Client
from .user import ClientUser, User from .user import ClientUser, User
from .asset import Asset from .asset import Asset
from .file import _FileBase
from .state import ConnectionState from .state import ConnectionState
from .guild import Guild from .guild import Guild
from .member import Member from .member import Member
@ -1570,13 +1571,52 @@ class Messageable:
async def _get_channel(self) -> MessageableChannel: async def _get_channel(self) -> MessageableChannel:
raise NotImplementedError raise NotImplementedError
async def upload_files(self, *files: File) -> List[CloudFile]:
r"""|coro|
Pre-uploads files to Discord's GCP bucket for use with :meth:`send`.
This method is useful if you have local files that you want to upload and
reuse multiple times.
Parameters
------------
\*files: :class:`~discord.File`
A list of files to upload. Must be a maximum of 10.
Raises
-------
~discord.HTTPException
Uploading the files failed.
~discord.Forbidden
You do not have the proper permissions to upload files.
Returns
--------
List[:class:`~discord.CloudFile`]
The files that were uploaded. These can be used in lieu
of normal :class:`~discord.File`\s in :meth:`send`.
"""
if not files:
return []
state = self._state
channel = await self._get_channel()
mapped_files = {i: f for i, f in enumerate(files)}
data = await self._state.http.get_attachment_urls(channel.id, [f.to_upload_dict(i) for i, f in mapped_files.items()])
return [
await CloudFile.from_file(state=state, data=uploaded, file=mapped_files[int(uploaded.get('id', 11))])
for uploaded in data['attachments']
]
@overload @overload
async def send( async def send(
self, self,
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
file: File = ..., file: _FileBase = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -1594,7 +1634,7 @@ class Messageable:
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
files: Sequence[File] = ..., files: Sequence[_FileBase] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -1612,7 +1652,7 @@ class Messageable:
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
file: File = ..., file: _FileBase = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -1630,7 +1670,7 @@ class Messageable:
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
files: Sequence[File] = ..., files: Sequence[_FileBase] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -1647,8 +1687,8 @@ class Messageable:
content: Optional[str] = None, content: Optional[str] = None,
*, *,
tts: bool = False, tts: bool = False,
file: Optional[File] = None, file: Optional[_FileBase] = None,
files: Optional[Sequence[File]] = None, files: Optional[Sequence[_FileBase]] = None,
stickers: Optional[Sequence[Union[GuildSticker, StickerItem]]] = None, stickers: Optional[Sequence[Union[GuildSticker, StickerItem]]] = None,
delete_after: Optional[float] = None, delete_after: Optional[float] = None,
nonce: Optional[Union[str, int]] = MISSING, nonce: Optional[Union[str, int]] = MISSING,
@ -1680,9 +1720,9 @@ class Messageable:
The content of the message to send. The content of the message to send.
tts: :class:`bool` tts: :class:`bool`
Indicates if the message should be sent using text-to-speech. Indicates if the message should be sent using text-to-speech.
file: :class:`~discord.File` file: Union[:class:`~discord.File`, :class:`~discord.CloudFile`]
The file to upload. The file to upload.
files: List[:class:`~discord.File`] files: List[Union[:class:`~discord.File`, :class:`~discord.CloudFile`]]
A list of files to upload. Must be a maximum of 10. A list of files to upload. Must be a maximum of 10.
nonce: :class:`int` nonce: :class:`int`
The nonce to use for sending this message. If the message was successfully sent, The nonce to use for sending this message. If the message was successfully sent,

10
discord/ext/commands/context.py

@ -51,7 +51,7 @@ if TYPE_CHECKING:
from discord.abc import MessageableChannel from discord.abc import MessageableChannel
from discord.commands import MessageCommand from discord.commands import MessageCommand
from discord.file import File from discord.file import _FileBase
from discord.guild import Guild from discord.guild import Guild
from discord.member import Member from discord.member import Member
from discord.mentions import AllowedMentions from discord.mentions import AllowedMentions
@ -433,7 +433,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
file: File = ..., file: _FileBase = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -451,7 +451,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
files: Sequence[File] = ..., files: Sequence[_FileBase] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -469,7 +469,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
file: File = ..., file: _FileBase = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -487,7 +487,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
files: Sequence[File] = ..., files: Sequence[_FileBase] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,

167
discord/file.py

@ -28,15 +28,21 @@ from base64 import b64encode
from hashlib import md5 from hashlib import md5
import io import io
import os import os
from typing import Any, Dict, Optional, Tuple, Union import yarl
from typing import Any, Dict, Optional, Tuple, Union, TYPE_CHECKING
from .utils import MISSING, cached_slot_property from .utils import MISSING, cached_slot_property
# fmt: off if TYPE_CHECKING:
from typing_extensions import Self
from .state import ConnectionState
from .types.message import CloudAttachment as CloudAttachmentPayload, UploadedAttachment as UploadedAttachmentPayload
__all__ = ( __all__ = (
'File', 'File',
'CloudFile',
) )
# fmt: on
def _strip_spoiler(filename: str) -> Tuple[str, bool]: def _strip_spoiler(filename: str) -> Tuple[str, bool]:
@ -47,7 +53,48 @@ def _strip_spoiler(filename: str) -> Tuple[str, bool]:
return stripped, spoiler return stripped, spoiler
class File: class _FileBase:
__slots__ = ('_filename', 'spoiler', 'description')
def __init__(self, filename: str, *, spoiler: bool = False, description: Optional[str] = None):
self._filename, filename_spoiler = _strip_spoiler(filename)
if spoiler is MISSING:
spoiler = filename_spoiler
self.spoiler: bool = spoiler
self.description: Optional[str] = description
@property
def filename(self) -> str:
""":class:`str`: The filename to display when uploading to Discord.
If this is not given then it defaults to ``fp.name`` or if ``fp`` is
a string then the ``filename`` will default to the string given.
"""
return 'SPOILER_' + self._filename if self.spoiler else self._filename
@filename.setter
def filename(self, value: str) -> None:
self._filename, self.spoiler = _strip_spoiler(value)
def to_dict(self, index: int) -> Dict[str, Any]:
payload = {
'id': str(index),
'filename': self.filename,
}
if self.description is not None:
payload['description'] = self.description
return payload
def reset(self, *, seek: Union[int, bool] = True) -> None:
return
def close(self) -> None:
return
class File(_FileBase):
r"""A parameter object used for :meth:`abc.Messageable.send` r"""A parameter object used for :meth:`abc.Messageable.send`
for sending file objects. for sending file objects.
@ -79,7 +126,7 @@ class File:
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
__slots__ = ('fp', '_filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer', '_cs_md5') __slots__ = ('fp', '_original_pos', '_owner', '_closer', '_cs_md5', '_cs_size')
def __init__( def __init__(
self, self,
@ -112,24 +159,7 @@ class File:
else: else:
filename = getattr(fp, 'name', 'untitled') filename = getattr(fp, 'name', 'untitled')
self._filename, filename_spoiler = _strip_spoiler(filename) super().__init__(filename, spoiler=spoiler, description=description)
if spoiler is MISSING:
spoiler = filename_spoiler
self.spoiler: bool = spoiler
self.description: Optional[str] = description
@property
def filename(self) -> str:
""":class:`str`: The filename to display when uploading to Discord.
If this is not given then it defaults to ``fp.name`` or if ``fp`` is
a string then the ``filename`` will default to the string given.
"""
return 'SPOILER_' + self._filename if self.spoiler else self._filename
@filename.setter
def filename(self, value: str) -> None:
self._filename, self.spoiler = _strip_spoiler(value)
@cached_slot_property('_cs_md5') @cached_slot_property('_cs_md5')
def md5(self) -> str: def md5(self) -> str:
@ -138,6 +168,17 @@ class File:
finally: finally:
self.reset() self.reset()
@cached_slot_property('_cs_size')
def size(self) -> int:
return os.fstat(self.fp.fileno()).st_size
def to_upload_dict(self, index: int) -> UploadedAttachmentPayload:
return {
'id': str(index),
'filename': self.filename,
'file_size': self.size,
}
def reset(self, *, seek: Union[int, bool] = True) -> None: def reset(self, *, seek: Union[int, bool] = True) -> None:
# The `seek` parameter is needed because # The `seek` parameter is needed because
# the retry-loop is iterated over multiple times # the retry-loop is iterated over multiple times
@ -155,13 +196,79 @@ class File:
if self._owner: if self._owner:
self._closer() self._closer()
def to_dict(self, index: int) -> Dict[str, Any]:
payload = {
'id': index,
'filename': self.filename,
}
if self.description is not None: class CloudFile(_FileBase):
payload['description'] = self.description """A parameter object used for :meth:`abc.Messageable.send`
for sending file objects that have been pre-uploaded to Discord's GCP bucket.
.. note::
Unlike :class:`File`, this class is not directly user-constructable, however
it can be reused multiple times in :meth:`abc.Messageable.send`.
To construct it, see :meth:`abc.Messageable.upload_files`.
.. versionadded:: 2.1
Attributes
-----------
url: :class:`str`
The upload URL of the file.
.. note::
This URL cannot be used to download the file,
it is merely used to send the file to Discord.
upload_filename: :class:`str`
The filename that Discord has assigned to the file.
spoiler: :class:`bool`
Whether the attachment is a spoiler. If left unspecified, the :attr:`~CloudFile.filename` is used
to determine if the file is a spoiler.
description: Optional[:class:`str`]
The file description to display, currently only supported for images.
"""
__slots__ = ('url', 'upload_filename', '_state')
def __init__(
self,
url: str,
filename: str,
upload_filename: str,
*,
spoiler: bool = MISSING,
description: Optional[str] = None,
state: ConnectionState,
):
super().__init__(filename, spoiler=spoiler, description=description)
self.url = url
self.upload_filename = upload_filename
self._state = state
@classmethod
async def from_file(cls, *, file: File, state: ConnectionState, data: CloudAttachmentPayload) -> Self:
await state.http.upload_to_cloud(data['upload_url'], file)
return cls(data['upload_url'], file._filename, data['upload_filename'], description=file.description, state=state)
@property
def upload_id(self) -> str:
""":class:`str`: The upload ID of the file."""
url = yarl.URL(self.url)
return url.query['upload_id']
def to_dict(self, index: int) -> Dict[str, Any]:
payload = super().to_dict(index)
payload['uploaded_filename'] = self.upload_filename
return payload return payload
async def delete(self) -> None:
"""|coro|
Deletes the uploaded file from Discord's GCP bucket.
Raises
-------
HTTPException
Deleting the file failed.
"""
await self._state.http.delete_attachment(self.upload_filename)

31
discord/http.py

@ -63,7 +63,7 @@ from .errors import (
GatewayNotFound, GatewayNotFound,
CaptchaRequired, CaptchaRequired,
) )
from .file import File from .file import _FileBase, File
from .tracking import ContextProperties from .tracking import ContextProperties
from . import utils from . import utils
from .mentions import AllowedMentions from .mentions import AllowedMentions
@ -75,7 +75,6 @@ if TYPE_CHECKING:
from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable, VoiceChannel, ForumChannel from .channel import TextChannel, DMChannel, GroupChannel, PartialMessageable, VoiceChannel, ForumChannel
from .handlers import CaptchaHandler from .handlers import CaptchaHandler
from .threads import Thread from .threads import Thread
from .file import File
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .message import Attachment, Message from .message import Attachment, Message
from .flags import MessageFlags from .flags import MessageFlags
@ -166,11 +165,11 @@ def handle_message_parameters(
tts: bool = False, tts: bool = False,
nonce: Optional[Union[int, str]] = MISSING, nonce: Optional[Union[int, str]] = MISSING,
flags: MessageFlags = MISSING, flags: MessageFlags = MISSING,
file: File = MISSING, file: _FileBase = MISSING,
files: Sequence[File] = MISSING, files: Sequence[_FileBase] = MISSING,
embed: Optional[Embed] = MISSING, embed: Optional[Embed] = MISSING,
embeds: Sequence[Embed] = MISSING, embeds: Sequence[Embed] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING, attachments: Sequence[Union[Attachment, _FileBase]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING,
message_reference: Optional[message.MessageReference] = MISSING, message_reference: Optional[message.MessageReference] = MISSING,
stickers: Optional[SnowflakeList] = MISSING, stickers: Optional[SnowflakeList] = MISSING,
@ -249,13 +248,13 @@ def handle_message_parameters(
if attachments is MISSING: if attachments is MISSING:
attachments = files attachments = files
else: else:
files = [a for a in attachments if isinstance(a, File)] files = [a for a in attachments if isinstance(a, _FileBase)]
if attachments is not MISSING: if attachments is not MISSING:
file_index = 0 file_index = 0
attachments_payload = [] attachments_payload = []
for attachment in attachments: for attachment in attachments:
if isinstance(attachment, File): if isinstance(attachment, _FileBase):
attachments_payload.append(attachment.to_dict(file_index)) attachments_payload.append(attachment.to_dict(file_index))
file_index += 1 file_index += 1
else: else:
@ -269,11 +268,13 @@ def handle_message_parameters(
} }
payload.update(channel_payload) payload.update(channel_payload)
# Legacy uploading
multipart = [] multipart = []
if files: to_upload = [file for file in files if isinstance(file, File)]
if to_upload:
multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)}) multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)})
payload = None payload = None
for index, file in enumerate(files): for index, file in enumerate(to_upload):
multipart.append( multipart.append(
{ {
'name': f'files[{index}]', 'name': f'files[{index}]',
@ -283,7 +284,7 @@ def handle_message_parameters(
} }
) )
return MultipartParameters(payload=payload, multipart=multipart, files=files) return MultipartParameters(payload=payload, multipart=multipart, files=to_upload)
def _gen_accept_encoding_header(): def _gen_accept_encoding_header():
@ -1326,6 +1327,16 @@ class HTTPClient:
def ack_pins(self, channel_id: Snowflake) -> Response[None]: def ack_pins(self, channel_id: Snowflake) -> Response[None]:
return self.request(Route('POST', '/channels/{channel_id}/pins/ack', channel_id=channel_id)) return self.request(Route('POST', '/channels/{channel_id}/pins/ack', channel_id=channel_id))
def get_attachment_urls(
self, channel_id: Snowflake, attachments: List[message.UploadedAttachment]
) -> Response[message.CloudAttachments]:
payload = {'files': attachments}
return self.request(Route('POST', '/channels/{channel_id}/attachments', channel_id=channel_id), json=payload)
def delete_attachment(self, uploaded_filename: str) -> Response[None]:
return self.request(Route('DELETE', '/attachments/{uploaded_filename}', uploaded_filename=uploaded_filename))
# Member management # Member management
def kick(self, user_id: Snowflake, guild_id: Snowflake, reason: Optional[str] = None) -> Response[None]: def kick(self, user_id: Snowflake, guild_id: Snowflake, reason: Optional[str] = None) -> Response[None]:

29
discord/message.py

@ -99,6 +99,7 @@ if TYPE_CHECKING:
from .abc import Snowflake from .abc import Snowflake
from .abc import GuildChannel, MessageableChannel from .abc import GuildChannel, MessageableChannel
from .components import ActionRow, ActionRowChildComponentType from .components import ActionRow, ActionRowChildComponentType
from .file import _FileBase
from .state import ConnectionState from .state import ConnectionState
from .mentions import AllowedMentions from .mentions import AllowedMentions
from .sticker import GuildSticker from .sticker import GuildSticker
@ -745,7 +746,7 @@ class PartialMessage(Hashable):
self, self,
*, *,
content: Optional[str] = ..., content: Optional[str] = ...,
attachments: Sequence[Union[Attachment, File]] = ..., attachments: Sequence[Union[Attachment, _FileBase]] = ...,
delete_after: Optional[float] = ..., delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ..., allowed_mentions: Optional[AllowedMentions] = ...,
) -> Message: ) -> Message:
@ -756,7 +757,7 @@ class PartialMessage(Hashable):
self, self,
*, *,
content: Optional[str] = ..., content: Optional[str] = ...,
attachments: Sequence[Union[Attachment, File]] = ..., attachments: Sequence[Union[Attachment, _FileBase]] = ...,
delete_after: Optional[float] = ..., delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ..., allowed_mentions: Optional[AllowedMentions] = ...,
) -> Message: ) -> Message:
@ -765,7 +766,7 @@ class PartialMessage(Hashable):
async def edit( async def edit(
self, self,
content: Optional[str] = MISSING, content: Optional[str] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING, attachments: Sequence[Union[Attachment, _FileBase]] = MISSING,
delete_after: Optional[float] = None, delete_after: Optional[float] = None,
allowed_mentions: Optional[AllowedMentions] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING,
) -> Message: ) -> Message:
@ -787,7 +788,7 @@ class PartialMessage(Hashable):
content: Optional[:class:`str`] content: Optional[:class:`str`]
The new content to replace the message with. The new content to replace the message with.
Could be ``None`` to remove the content. Could be ``None`` to remove the content.
attachments: List[Union[:class:`Attachment`, :class:`File`]] attachments: List[Union[:class:`Attachment`, :class:`File`, :class:`CloudFile`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed. then all attachments are removed.
@ -1171,7 +1172,7 @@ class PartialMessage(Hashable):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
file: File = ..., file: _FileBase = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -1188,7 +1189,7 @@ class PartialMessage(Hashable):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
files: Sequence[File] = ..., files: Sequence[_FileBase] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -1205,7 +1206,7 @@ class PartialMessage(Hashable):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
file: File = ..., file: _FileBase = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -1222,7 +1223,7 @@ class PartialMessage(Hashable):
content: Optional[str] = ..., content: Optional[str] = ...,
*, *,
tts: bool = ..., tts: bool = ...,
files: Sequence[File] = ..., files: Sequence[_FileBase] = ...,
stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ..., delete_after: float = ...,
nonce: Union[str, int] = ..., nonce: Union[str, int] = ...,
@ -2122,7 +2123,7 @@ class Message(PartialMessage, Hashable):
self, self,
*, *,
content: Optional[str] = ..., content: Optional[str] = ...,
attachments: Sequence[Union[Attachment, File]] = ..., attachments: Sequence[Union[Attachment, _FileBase]] = ...,
suppress: bool = ..., suppress: bool = ...,
delete_after: Optional[float] = ..., delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ..., allowed_mentions: Optional[AllowedMentions] = ...,
@ -2134,7 +2135,7 @@ class Message(PartialMessage, Hashable):
self, self,
*, *,
content: Optional[str] = ..., content: Optional[str] = ...,
attachments: Sequence[Union[Attachment, File]] = ..., attachments: Sequence[Union[Attachment, _FileBase]] = ...,
suppress: bool = ..., suppress: bool = ...,
delete_after: Optional[float] = ..., delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ..., allowed_mentions: Optional[AllowedMentions] = ...,
@ -2144,7 +2145,7 @@ class Message(PartialMessage, Hashable):
async def edit( async def edit(
self, self,
content: Optional[str] = MISSING, content: Optional[str] = MISSING,
attachments: Sequence[Union[Attachment, File]] = MISSING, attachments: Sequence[Union[Attachment, _FileBase]] = MISSING,
suppress: bool = False, suppress: bool = False,
delete_after: Optional[float] = None, delete_after: Optional[float] = None,
allowed_mentions: Optional[AllowedMentions] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING,
@ -2170,7 +2171,7 @@ class Message(PartialMessage, Hashable):
content: Optional[:class:`str`] content: Optional[:class:`str`]
The new content to replace the message with. The new content to replace the message with.
Could be ``None`` to remove the content. Could be ``None`` to remove the content.
attachments: List[Union[:class:`Attachment`, :class:`File`]] attachments: List[Union[:class:`Attachment`, :class:`File`, :class:`CloudFile`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed. then all attachments are removed.
@ -2238,7 +2239,7 @@ class Message(PartialMessage, Hashable):
return message return message
async def add_files(self, *files: File) -> Message: async def add_files(self, *files: _FileBase) -> Message:
r"""|coro| r"""|coro|
Adds new files to the end of the message attachments. Adds new files to the end of the message attachments.
@ -2247,7 +2248,7 @@ class Message(PartialMessage, Hashable):
Parameters Parameters
----------- -----------
\*files: :class:`File` \*files: Union[:class:`File`, :class:`CloudFile`]
New files to add to the message. New files to add to the message.
Raises Raises

16
discord/types/message.py

@ -185,3 +185,19 @@ MessageSearchHasType = Literal[
] ]
MessageSearchSortType = Literal['timestamp', 'relevance'] MessageSearchSortType = Literal['timestamp', 'relevance']
MessageSearchSortOrder = Literal['desc', 'asc'] MessageSearchSortOrder = Literal['desc', 'asc']
class UploadedAttachment(TypedDict):
id: NotRequired[Snowflake]
filename: str
file_size: int
class CloudAttachment(TypedDict):
id: NotRequired[Snowflake]
upload_url: str
upload_filename: str
class CloudAttachments(TypedDict):
attachments: List[CloudAttachment]

7
docs/api.rst

@ -7637,6 +7637,13 @@ File
.. autoclass:: File() .. autoclass:: File()
:members: :members:
:inherited-members:
.. attributetable:: CloudFile
.. autoclass:: CloudFile()
:members:
:inherited-members:
Colour Colour
~~~~~~ ~~~~~~

Loading…
Cancel
Save