From 08bee0eeb60234ff691c07669a4c8282a8bdf052 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 18 Feb 2022 20:23:02 +1000 Subject: [PATCH] Add support for file/attachment descriptions --- discord/file.py | 21 +++++++++++---------- discord/http.py | 34 +++++++++++++++++++--------------- discord/message.py | 9 ++++++++- discord/types/message.py | 1 + discord/webhook/async_.py | 32 ++++++++++++++++++-------------- 5 files changed, 57 insertions(+), 40 deletions(-) diff --git a/discord/file.py b/discord/file.py index 5303e3251..701418ab4 100644 --- a/discord/file.py +++ b/discord/file.py @@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -from typing import Optional, TYPE_CHECKING, Union +from typing import Optional, Union import os import io @@ -62,14 +62,13 @@ class File: a string then the ``filename`` will default to the string given. spoiler: :class:`bool` Whether the attachment is a spoiler. - """ + description: Optional[:class:`str`] + The file description to display, currently only supported for images. - __slots__ = ('fp', 'filename', 'spoiler', '_original_pos', '_owner', '_closer') + .. versionadded:: 2.0 + """ - if TYPE_CHECKING: - fp: io.BufferedIOBase - filename: Optional[str] - spoiler: bool + __slots__ = ('fp', 'filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer') def __init__( self, @@ -77,11 +76,12 @@ class File: filename: Optional[str] = None, *, spoiler: bool = False, + description: Optional[str] = None, ): if isinstance(fp, io.IOBase): if not (fp.seekable() and fp.readable()): raise ValueError(f'File buffer {fp!r} must be seekable and readable') - self.fp = fp + self.fp: io.BufferedIOBase = fp self._original_pos = fp.tell() self._owner = False else: @@ -102,12 +102,13 @@ class File: else: self.filename = getattr(fp, 'name', None) else: - self.filename = filename + self.filename: Optional[str] = filename if spoiler and self.filename is not None and not self.filename.startswith('SPOILER_'): self.filename = 'SPOILER_' + self.filename - self.spoiler = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_')) + self.spoiler: bool = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_')) + self.description: Optional[str] = description def reset(self, *, seek: Union[int, bool] = True) -> None: # The `seek` parameter is needed because diff --git a/discord/http.py b/discord/http.py index 84d9e7a9e..04c90c89f 100644 --- a/discord/http.py +++ b/discord/http.py @@ -267,7 +267,8 @@ class HTTPClient: f.reset(seek=tries) if form: - form_data = aiohttp.FormData() + # with quote_fields=True '[' and ']' in file field names are escaped, which discord does not support + form_data = aiohttp.FormData(quote_fields=False) for params in form: form_data.add_field(**params) kwargs['data'] = form_data @@ -496,28 +497,31 @@ class HTTPClient: payload['components'] = components if stickers: payload['sticker_ids'] = stickers + if files: + attachments = [] + for index, file in enumerate(files): + attachment = { + "id": index, + "filename": file.filename, + } + + if file.description is not None: + attachment["description"] = file.description + + attachments.append(attachment) + + payload['attachments'] = attachments form.append({'name': 'payload_json', 'value': utils._to_json(payload)}) - if len(files) == 1: - file = files[0] + for index, file in enumerate(files): form.append( { - 'name': 'file', + 'name': f'files[{index}]', 'value': file.fp, 'filename': file.filename, - 'content_type': 'application/octet-stream', + 'content_type': 'image/png', } ) - else: - for index, file in enumerate(files): - form.append( - { - 'name': f'file{index}', - 'value': file.fp, - 'filename': file.filename, - 'content_type': 'application/octet-stream', - } - ) return self.request(route, form=form, files=files) diff --git a/discord/message.py b/discord/message.py index 304c807da..c65d714b3 100644 --- a/discord/message.py +++ b/discord/message.py @@ -151,9 +151,13 @@ class Attachment(Hashable): The attachment's `media type `_ .. versionadded:: 1.7 + description: Optional[:class:`str`] + The attachment's description. Only applicable to images. + + .. versionadded:: 2.0 """ - __slots__ = ('id', 'size', 'height', 'width', 'filename', 'url', 'proxy_url', '_http', 'content_type') + __slots__ = ('id', 'size', 'height', 'width', 'filename', 'url', 'proxy_url', '_http', 'content_type', 'description') def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.id: int = int(data['id']) @@ -165,6 +169,7 @@ class Attachment(Hashable): self.proxy_url: str = data.get('proxy_url') self._http = state.http self.content_type: Optional[str] = data.get('content_type') + self.description: Optional[str] = data.get('description') def is_spoiler(self) -> bool: """:class:`bool`: Whether this attachment contains a spoiler.""" @@ -318,6 +323,8 @@ class Attachment(Hashable): 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 diff --git a/discord/types/message.py b/discord/types/message.py index 5448a56a4..0e1bff19d 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -52,6 +52,7 @@ class Reaction(TypedDict): class _AttachmentOptional(TypedDict, total=False): height: Optional[int] width: Optional[int] + description: str content_type: str spoiler: bool diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index fac601f32..be58a93ac 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -142,7 +142,7 @@ class AsyncWebhookAdapter: file.reset(seek=attempt) if multipart: - form_data = aiohttp.FormData() + form_data = aiohttp.FormData(quote_fields=False) for p in multipart: form_data.add_field(**p) to_send = form_data @@ -487,28 +487,32 @@ def handle_message_parameters( files = [file] if files: + for index, file in enumerate(files): + attachments = [] + for index, file in enumerate(files): + attachment = { + "id": index, + "filename": file.filename, + } + + if file.description is not None: + attachment["description"] = file.description + + attachments.append(attachment) + + payload['attachments'] = attachments + multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)}) payload = None - if len(files) == 1: - file = files[0] + for index, file in enumerate(files): multipart.append( { - 'name': 'file', + 'name': f'files[{index}]', 'value': file.fp, 'filename': file.filename, 'content_type': 'application/octet-stream', } ) - else: - for index, file in enumerate(files): - multipart.append( - { - 'name': f'file{index}', - 'value': file.fp, - 'filename': file.filename, - 'content_type': 'application/octet-stream', - } - ) return ExecuteWebhookParameters(payload=payload, multipart=multipart, files=files)