From 8459357fa18ade3a736d3799c678a222aeb31b80 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sun, 15 Sep 2024 15:15:20 -0400 Subject: [PATCH 1/8] Initial Testing For Voice Messages --- discord/abc.py | 1 + discord/file.py | 5 +++++ discord/http.py | 6 ++++++ discord/interactions.py | 2 ++ discord/webhook/async_.py | 5 +++++ 5 files changed, 19 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index d699f44702..72b16048cd 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1567,6 +1567,7 @@ async def send( flags = MessageFlags( suppress_embeds=bool(suppress), suppress_notifications=bool(silent), + is_voice_message=True, ).value if stickers is not None: diff --git a/discord/file.py b/discord/file.py index cb1a766bc9..16c102cc92 100644 --- a/discord/file.py +++ b/discord/file.py @@ -73,6 +73,8 @@ class File: "_owner", "_closer", "description", + "waveform", + "duration_secs", ) if TYPE_CHECKING: @@ -89,6 +91,9 @@ def __init__( description: str | None = None, spoiler: bool = False, ): + self.waveform = "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=" + self.duration_secs = 60.0 + if isinstance(fp, io.IOBase): if not (fp.seekable() and fp.readable()): raise ValueError(f"File buffer {fp!r} must be seekable and readable") diff --git a/discord/http.py b/discord/http.py index f42bcdc233..1b547e1a8a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -572,6 +572,8 @@ def send_multipart_helper( "id": index, "filename": file.filename, "description": file.description, + "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", + "duration_secs": 60.0, } ) form.append( @@ -638,6 +640,8 @@ def edit_multipart_helper( "id": index, "filename": file.filename, "description": file.description, + "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", + "duration_secs": 60.0 } ) form.append( @@ -1272,6 +1276,8 @@ def start_forum_thread( "id": index, "filename": file.filename, "description": file.description, + "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", + "duration_secs": 60.0 } ) form.append( diff --git a/discord/interactions.py b/discord/interactions.py index e7d7fade3e..d106b7af6f 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -918,6 +918,8 @@ async def send_message( if ephemeral: payload["flags"] = 64 + payload["flags"] = 8192 + if view is not None: payload["components"] = view.to_components() diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 090f7c96f2..acb2050c9b 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -156,6 +156,7 @@ async def request( for p in multipart: form_data.add_field(**p) to_send = form_data + print(to_send) try: async with session.request( method, @@ -512,6 +513,8 @@ def create_interaction_response( "id": index, "filename": file.filename, "description": file.description, + "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", + "duration_secs": 60.0, } ) form.append( @@ -687,6 +690,8 @@ def handle_message_parameters( "value": file.fp, "filename": file.filename, "content_type": "application/octet-stream", + "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", + "duration_secs": 60.0, } ) _attachments.append( From 7bad2f79ca2e26d3e6e7997c9cee5e29498ca718 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sun, 15 Sep 2024 19:52:19 -0400 Subject: [PATCH 2/8] Add Aiohttp Logging --- discord/http.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 1b547e1a8a..2297d54134 100644 --- a/discord/http.py +++ b/discord/http.py @@ -410,9 +410,24 @@ async def close(self) -> None: # login management async def static_login(self, token: str) -> user.User: + import logging + + async def on_request_start(session, context, params: aiohttp.TraceRequestStartParams): + # breakpoint() + logging.getLogger('aiohttp.client').debug(f'Starting request <{params}> <{session}> <{context}>') + + async def on_request_chunk_sent(session, context, params: aiohttp.TraceRequestChunkSentParams): + with open("output.txt", "a") as file: + file.write(str(params.chunk)) + # logging.getLogger('aiohttp.client').debug(f'Sent Chunk <{params}>') + + trace_config = aiohttp.TraceConfig() + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_chunk_sent.append(on_request_chunk_sent) + # Necessary to get aiohttp to stop complaining about session creation self.__session = aiohttp.ClientSession( - connector=self.connector, ws_response_class=DiscordClientWebSocketResponse + connector=self.connector, ws_response_class=DiscordClientWebSocketResponse, trace_configs=[trace_config] ) old_token = self.token self.token = token From 58aad56bac5e44069c8dcaad9f102d0ba909b9f0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 23:14:46 +0000 Subject: [PATCH 3/8] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/http.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/discord/http.py b/discord/http.py index 2297d54134..f79ed19bc7 100644 --- a/discord/http.py +++ b/discord/http.py @@ -412,11 +412,17 @@ async def close(self) -> None: async def static_login(self, token: str) -> user.User: import logging - async def on_request_start(session, context, params: aiohttp.TraceRequestStartParams): + async def on_request_start( + session, context, params: aiohttp.TraceRequestStartParams + ): # breakpoint() - logging.getLogger('aiohttp.client').debug(f'Starting request <{params}> <{session}> <{context}>') + logging.getLogger("aiohttp.client").debug( + f"Starting request <{params}> <{session}> <{context}>" + ) - async def on_request_chunk_sent(session, context, params: aiohttp.TraceRequestChunkSentParams): + async def on_request_chunk_sent( + session, context, params: aiohttp.TraceRequestChunkSentParams + ): with open("output.txt", "a") as file: file.write(str(params.chunk)) # logging.getLogger('aiohttp.client').debug(f'Sent Chunk <{params}>') @@ -427,7 +433,9 @@ async def on_request_chunk_sent(session, context, params: aiohttp.TraceRequestCh # Necessary to get aiohttp to stop complaining about session creation self.__session = aiohttp.ClientSession( - connector=self.connector, ws_response_class=DiscordClientWebSocketResponse, trace_configs=[trace_config] + connector=self.connector, + ws_response_class=DiscordClientWebSocketResponse, + trace_configs=[trace_config], ) old_token = self.token self.token = token @@ -656,7 +664,7 @@ def edit_multipart_helper( "filename": file.filename, "description": file.description, "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0 + "duration_secs": 60.0, } ) form.append( @@ -1292,7 +1300,7 @@ def start_forum_thread( "filename": file.filename, "description": file.description, "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0 + "duration_secs": 60.0, } ) form.append( From 83e3aad728a1aded69848e7b8af528107bc3ed01 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sun, 22 Sep 2024 15:06:35 -0400 Subject: [PATCH 4/8] Generalize Implementation A Bit More --- discord/abc.py | 3 ++- discord/file.py | 10 ++++++++-- discord/http.py | 18 ++++++++++-------- discord/interactions.py | 8 +++++++- discord/webhook/async_.py | 29 ++++++++++++++++++++++------- 5 files changed, 49 insertions(+), 19 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 72b16048cd..aedd44ffad 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1440,6 +1440,7 @@ async def send( poll=None, suppress=None, silent=None, + voice_message=None, ): """|coro| @@ -1567,7 +1568,7 @@ async def send( flags = MessageFlags( suppress_embeds=bool(suppress), suppress_notifications=bool(silent), - is_voice_message=True, + is_voice_message=bool(voice_message), ).value if stickers is not None: diff --git a/discord/file.py b/discord/file.py index 16c102cc92..7ab8223b78 100644 --- a/discord/file.py +++ b/discord/file.py @@ -63,6 +63,10 @@ class File: The description of a file, used by Discord to display alternative text on images. spoiler: :class:`bool` Whether the attachment is a spoiler. + waveform: Optional[:class:`str`] + The base64 encoded bytearray representing a sampled waveform. Currently only for voice messages + duration_secs: Optional[:class:`float`] + The duration of the audio file. Currently only for voice messages """ __slots__ = ( @@ -90,9 +94,9 @@ def __init__( *, description: str | None = None, spoiler: bool = False, + waveform: str | None = None, + duration_secs: float | None = None, ): - self.waveform = "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=" - self.duration_secs = 60.0 if isinstance(fp, io.IOBase): if not (fp.seekable() and fp.readable()): @@ -131,6 +135,8 @@ def __init__( self.filename is not None and self.filename.startswith("SPOILER_") ) self.description = description + self.waveform = waveform + self.duration_secs = duration_secs def reset(self, *, seek: int | bool = True) -> None: # The `seek` parameter is needed because diff --git a/discord/http.py b/discord/http.py index 2297d54134..284ef16053 100644 --- a/discord/http.py +++ b/discord/http.py @@ -410,6 +410,7 @@ async def close(self) -> None: # login management async def static_login(self, token: str) -> user.User: + # TODO: Remove This When Testing Is Done import logging async def on_request_start(session, context, params: aiohttp.TraceRequestStartParams): @@ -418,7 +419,10 @@ async def on_request_start(session, context, params: aiohttp.TraceRequestStartPa async def on_request_chunk_sent(session, context, params: aiohttp.TraceRequestChunkSentParams): with open("output.txt", "a") as file: - file.write(str(params.chunk)) + if byte := str(params.chunk).find(r"\x", 100) != -1: + file.write(str(params.chunk)[:byte] + "\n") + else: + file.write(str(params.chunk) + "\n") # logging.getLogger('aiohttp.client').debug(f'Sent Chunk <{params}>') trace_config = aiohttp.TraceConfig() @@ -587,8 +591,8 @@ def send_multipart_helper( "id": index, "filename": file.filename, "description": file.description, - "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0, + "waveform": file.waveform, + "duration_secs": file.duration_secs, } ) form.append( @@ -655,8 +659,8 @@ def edit_multipart_helper( "id": index, "filename": file.filename, "description": file.description, - "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0 + "waveform": "AADz/+z/Pf+jBCD8pQE++8sDOv5H/WAH6PvR/uP8mfGCGnkCnAg58y77ewGH/U79AwXc9yUGlvloFmcF5fx9+kwFMQV8+GYDaf1oAToEmfm9APcB7gWs/mYEGgB5/VT9sfv3AAYCBALl/kX9jgDsA6UCLvtABNv/g/65/QP+xf2QAxsA8gKI/J3+1f5vBcAC5/ps/mQDigGn+FcEZAIZ/lYClP3a/rwEw/2g++wDpPoMB/36jAKPAC7+2wFo/IoBMgG2Aib9ivxqBFn7JwBn/LUEHP1R/T4EYv77AIoBd/wRBRX+DvwTA+4C0/l3AHQEiv98+n8B1wPj/DkA/gGp/zr7GQJv/HwAevlPBon8UP7bAyj/uAJp/SsCjPssAif/kP7bBIn9sQHq/9j94/+aADH+KQCcASj/1gHr/uYA/P1UAiz+XgEkANL9GgG8Adr+Mv/H/w8BgP+aASf+bAAgAE0AS/4hAWUASP/VAE3/5P5tAd39fQLq/zD/wADU/sgAo//T/zQBg/6nANf/8v/IAHH/iQCd/wQAEACa/3cA8v/s/x8A4v///xkA2v8QAA8A7P8LAPz/AAABAP3/AAAAAP//AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "duration_secs": 10.032 } ) form.append( @@ -664,7 +668,7 @@ def edit_multipart_helper( "name": f"files[{index}]", "value": file.fp, "filename": file.filename, - "content_type": "application/octet-stream", + "content_type": "audio/wav", } ) if "attachments" not in payload: @@ -1291,8 +1295,6 @@ def start_forum_thread( "id": index, "filename": file.filename, "description": file.description, - "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0 } ) form.append( diff --git a/discord/interactions.py b/discord/interactions.py index d106b7af6f..85a58dfd5b 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -840,6 +840,7 @@ async def send_message( files: list[File] = None, poll: Poll = None, delete_after: float = None, + voice_message: bool = False, ) -> Interaction: """|coro| @@ -877,6 +878,10 @@ async def send_message( The poll to send. .. versionadded:: 2.6 + voice_message: :class:`bool` + If the file should be treated as a voice message. + + .. versionadded:: 2.7 Returns ------- @@ -917,8 +922,9 @@ async def send_message( if ephemeral: payload["flags"] = 64 + if voice_message: + payload["flags"] = payload.setdefault("flags", 0) + 8192 - payload["flags"] = 8192 if view is not None: payload["components"] = view.to_components() diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index acb2050c9b..b6be72cc8d 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -156,7 +156,6 @@ async def request( for p in multipart: form_data.add_field(**p) to_send = form_data - print(to_send) try: async with session.request( method, @@ -513,8 +512,10 @@ def create_interaction_response( "id": index, "filename": file.filename, "description": file.description, - "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0, + "duration_secs": file.duration_secs, + "waveform": file.waveform, + # TODO: Fix content_type + #"content_type": "audio/mp3", } ) form.append( @@ -522,9 +523,12 @@ def create_interaction_response( "name": f"files[{index}]", "value": file.fp, "filename": file.filename, - "content_type": "application/octet-stream", + # TODO: Fix content_type + #"content_type": "application/octet-stream", + #"content_type": "audio/mp3", } ) + payload["flags"] = 1 << 13 payload["attachments"] = attachments form[0]["value"] = utils._to_json(payload) @@ -631,6 +635,7 @@ def handle_message_parameters( allowed_mentions: AllowedMentions | None = MISSING, previous_allowed_mentions: AllowedMentions | None = None, suppress: bool = False, + voice_message: bool = False, ) -> ExecuteWebhookParameters: if files is not MISSING and file is not MISSING: raise TypeError("Cannot mix file and files keyword arguments.") @@ -661,7 +666,7 @@ def handle_message_parameters( if username: payload["username"] = username - flags = MessageFlags(suppress_embeds=suppress, ephemeral=ephemeral) + flags = MessageFlags(suppress_embeds=suppress, ephemeral=ephemeral, is_voice_message=voice_message) payload["flags"] = flags.value if applied_tags is not MISSING: @@ -690,8 +695,6 @@ def handle_message_parameters( "value": file.fp, "filename": file.filename, "content_type": "application/octet-stream", - "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0, } ) _attachments.append( @@ -699,6 +702,10 @@ def handle_message_parameters( "id": index, "filename": file.filename, "description": file.description, + "waveform": file.waveform, + "duration_secs": file.duration_secs, + # TODO: Fix content_type + "content_type": "audio/wav", } ) @@ -1586,6 +1593,7 @@ async def send( thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, + voice_message: bool = MISSING, wait: Literal[True], delete_after: float = None, ) -> WebhookMessage: ... @@ -1609,6 +1617,7 @@ async def send( thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, + voice_message: bool = MISSING, wait: Literal[False] = ..., delete_after: float = None, ) -> None: ... @@ -1631,6 +1640,7 @@ async def send( thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, + voice_message: bool = MISSING, wait: bool = False, delete_after: float = None, ) -> WebhookMessage | None: @@ -1713,6 +1723,10 @@ async def send( The poll to send. .. versionadded:: 2.6 + voice_message: :class:`bool` + If the file should be treated as a voice message. + + .. versionadded:: 2.7 Returns ------- @@ -1790,6 +1804,7 @@ async def send( applied_tags=applied_tags, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, + voice_message=voice_message ) adapter = async_context.get() thread_id: int | None = None From d8015b6df367dbc5fb700bfb74e04f100fe76d13 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sun, 22 Sep 2024 15:11:35 -0400 Subject: [PATCH 5/8] Add TODO --- discord/http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/http.py b/discord/http.py index f6e8a63f3f..e758cb0597 100644 --- a/discord/http.py +++ b/discord/http.py @@ -667,6 +667,7 @@ def edit_multipart_helper( "id": index, "filename": file.filename, "description": file.description, + # TODO: Make Editing Work "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", "duration_secs": 60.0, } From 5bec4a801b7e86cb254971d6fdf6db2d53eef44c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:12:06 +0000 Subject: [PATCH 6/8] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/interactions.py | 1 - discord/webhook/async_.py | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 85a58dfd5b..24c8e66528 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -925,7 +925,6 @@ async def send_message( if voice_message: payload["flags"] = payload.setdefault("flags", 0) + 8192 - if view is not None: payload["components"] = view.to_components() diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index b6be72cc8d..6e7a10592c 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -515,7 +515,7 @@ def create_interaction_response( "duration_secs": file.duration_secs, "waveform": file.waveform, # TODO: Fix content_type - #"content_type": "audio/mp3", + # "content_type": "audio/mp3", } ) form.append( @@ -524,8 +524,8 @@ def create_interaction_response( "value": file.fp, "filename": file.filename, # TODO: Fix content_type - #"content_type": "application/octet-stream", - #"content_type": "audio/mp3", + # "content_type": "application/octet-stream", + # "content_type": "audio/mp3", } ) payload["flags"] = 1 << 13 @@ -666,7 +666,9 @@ def handle_message_parameters( if username: payload["username"] = username - flags = MessageFlags(suppress_embeds=suppress, ephemeral=ephemeral, is_voice_message=voice_message) + flags = MessageFlags( + suppress_embeds=suppress, ephemeral=ephemeral, is_voice_message=voice_message + ) payload["flags"] = flags.value if applied_tags is not MISSING: @@ -1804,7 +1806,7 @@ async def send( applied_tags=applied_tags, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, - voice_message=voice_message + voice_message=voice_message, ) adapter = async_context.get() thread_id: int | None = None From 589fc0f65a2308c9ac7181aab7b95bb0a0f2859a Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Wed, 27 Nov 2024 16:52:31 -0600 Subject: [PATCH 7/8] feat!: Replace voice_message kwarg with VoiceMessage subclass of File --- discord/abc.py | 34 ++++--------------- discord/file.py | 67 +++++++++++++++++++++++++++++++------ discord/http.py | 36 +++++++++++--------- discord/interactions.py | 17 ++++------ discord/webhook/async_.py | 70 ++++++++++++++++++--------------------- 5 files changed, 123 insertions(+), 101 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index aedd44ffad..17c9943683 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -45,7 +45,7 @@ from .context_managers import Typing from .enums import ChannelType from .errors import ClientException, InvalidArgument -from .file import File +from .file import File, VoiceMessage from .flags import MessageFlags from .invite import Invite from .iterators import HistoryIterator @@ -1440,7 +1440,6 @@ async def send( poll=None, suppress=None, silent=None, - voice_message=None, ): """|coro| @@ -1568,8 +1567,7 @@ async def send( flags = MessageFlags( suppress_embeds=bool(suppress), suppress_notifications=bool(silent), - is_voice_message=bool(voice_message), - ).value + ) if stickers is not None: stickers = [sticker.id for sticker in stickers] @@ -1615,27 +1613,7 @@ async def send( if file is not None: if not isinstance(file, File): raise InvalidArgument("file parameter must be File") - - try: - data = await state.http.send_files( - channel.id, - files=[file], - allowed_mentions=allowed_mentions, - content=content, - tts=tts, - embed=embed, - embeds=embeds, - nonce=nonce, - enforce_nonce=enforce_nonce, - message_reference=reference, - stickers=stickers, - components=components, - flags=flags, - poll=poll, - ) - finally: - file.close() - + files = [file] elif files is not None: if len(files) > 10: raise InvalidArgument( @@ -1644,6 +1622,8 @@ async def send( elif not all(isinstance(file, File) for file in files): raise InvalidArgument("files parameter must be a list of File") + if files is not None: + flags = flags + MessageFlags(is_voice_message=any(isinstance(f, VoiceMessage) for f in files)) try: data = await state.http.send_files( channel.id, @@ -1658,7 +1638,7 @@ async def send( message_reference=reference, stickers=stickers, components=components, - flags=flags, + flags=flags.value, poll=poll, ) finally: @@ -1677,7 +1657,7 @@ async def send( message_reference=reference, stickers=stickers, components=components, - flags=flags, + flags=flags.value, poll=poll, ) diff --git a/discord/file.py b/discord/file.py index 7ab8223b78..9b15bdd41f 100644 --- a/discord/file.py +++ b/discord/file.py @@ -29,7 +29,7 @@ import os from typing import TYPE_CHECKING -__all__ = ("File",) +__all__ = ("File", "VoiceMessage", ) class File: @@ -63,10 +63,6 @@ class File: The description of a file, used by Discord to display alternative text on images. spoiler: :class:`bool` Whether the attachment is a spoiler. - waveform: Optional[:class:`str`] - The base64 encoded bytearray representing a sampled waveform. Currently only for voice messages - duration_secs: Optional[:class:`float`] - The duration of the audio file. Currently only for voice messages """ __slots__ = ( @@ -77,8 +73,6 @@ class File: "_owner", "_closer", "description", - "waveform", - "duration_secs", ) if TYPE_CHECKING: @@ -94,8 +88,6 @@ def __init__( *, description: str | None = None, spoiler: bool = False, - waveform: str | None = None, - duration_secs: float | None = None, ): if isinstance(fp, io.IOBase): @@ -135,8 +127,6 @@ def __init__( self.filename is not None and self.filename.startswith("SPOILER_") ) self.description = description - self.waveform = waveform - self.duration_secs = duration_secs def reset(self, *, seek: int | bool = True) -> None: # The `seek` parameter is needed because @@ -154,3 +144,58 @@ def close(self) -> None: self.fp.close = self._closer if self._owner: self._closer() + + +class VoiceMessage(File): + """A special case of the File class that represents a voice message. + + .. versionadded:: 2.7 + + .. note:: + + Just like File objects VoiceMessage objects are single use and are not meant to be reused in + multiple requests. + + Attributes + ----------- + fp: Union[:class:`os.PathLike`, :class:`io.BufferedIOBase`] + A audio file-like object opened in binary mode and read mode + or a filename representing a file in the hard drive to + open. + + .. note:: + + If the file-like object passed is opened via ``open`` then the + modes 'rb' should be used. + + To pass binary data, consider usage of ``io.BytesIO``. + + filename: Optional[: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. + description: Optional[:class:`str`] + The description of a file, used by Discord to display alternative text on images. + spoiler: :class:`bool` + Whether the attachment is a spoiler. + waveform: Optional[:class:`str`] + The base64 encoded bytearray representing a sampled waveform. Currently only for voice messages + duration_secs: Optional[:class:`float`] + The duration of the audio file. Currently only for voice messages + """ + + __slots__ = ( + "waveform", + "duration_secs", + ) + + def __init__(self, + fp: str | bytes | os.PathLike | io.BufferedIOBase, + # filename: str | None = None, + waveform: str = "", + duration_secs: float = 0.0, + **kwargs + ): + super().__init__(fp, **kwargs) + self.waveform = waveform + self.duration_secs = duration_secs diff --git a/discord/http.py b/discord/http.py index e758cb0597..5afc3fce21 100644 --- a/discord/http.py +++ b/discord/http.py @@ -44,6 +44,7 @@ LoginFailure, NotFound, ) +from .file import VoiceMessage from .gateway import DiscordClientWebSocketResponse from .utils import MISSING, warn_deprecated @@ -594,15 +595,17 @@ def send_multipart_helper( attachments = [] form.append({"name": "payload_json"}) for index, file in enumerate(files): - attachments.append( - { + attachment_info = { "id": index, "filename": file.filename, "description": file.description, - "waveform": file.waveform, - "duration_secs": file.duration_secs, } - ) + if isinstance(file, VoiceMessage): + attachment_info.update( + waveform=file.waveform, + duration_secs=file.duration_secs, + ) + attachments.append(attachment_info) form.append( { "name": f"files[{index}]", @@ -662,22 +665,23 @@ def edit_multipart_helper( attachments = [] form.append({"name": "payload_json"}) for index, file in enumerate(files): - attachments.append( - { - "id": index, - "filename": file.filename, - "description": file.description, - # TODO: Make Editing Work - "waveform": "37WKcJ6jlLSVnaabsbeip4KPmHJXUUEbExgFJE8J7iNPFggpKQkTNl95dobFqqe2tKubnbSTX3yLVVBFS4iqd4dbKmFvMChwfVRKfWFYWRpLaV9jlYtKWWZde6mtnYiDlGNUgmFAWWdRXGNsf2NBYnNcS1uDjm+qwK2urKe8uKqjZ2KGSjtbLUpTO0iDYSBSg6CzCk1LNDVAZnOAvNiUkLu8r8vPnFw6bXZbbXcn0vUU8q2q38Olyfb0y7OhlnV9u6N4zuAH9uI=", - "duration_secs": 60.0, - } - ) + attachment_info = { + "id": index, + "filename": file.filename, + "description": file.description, + } + if isinstance(file, VoiceMessage): + attachment_info.update( + waveform=file.waveform, + duration_secs=file.duration_secs, + ) + attachments.append(attachment_info) form.append( { "name": f"files[{index}]", "value": file.fp, "filename": file.filename, - "content_type": "audio/wav", + "content_type": "application/octet-stream", } ) if "attachments" not in payload: diff --git a/discord/interactions.py b/discord/interactions.py index 24c8e66528..5aa849bc7e 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -37,7 +37,7 @@ try_enum, ) from .errors import ClientException, InteractionResponded, InvalidArgument -from .file import File +from .file import File, VoiceMessage from .flags import MessageFlags from .guild import Guild from .member import Member @@ -840,7 +840,6 @@ async def send_message( files: list[File] = None, poll: Poll = None, delete_after: float = None, - voice_message: bool = False, ) -> Interaction: """|coro| @@ -878,10 +877,6 @@ async def send_message( The poll to send. .. versionadded:: 2.6 - voice_message: :class:`bool` - If the file should be treated as a voice message. - - .. versionadded:: 2.7 Returns ------- @@ -920,10 +915,7 @@ async def send_message( if content is not None: payload["content"] = str(content) - if ephemeral: - payload["flags"] = 64 - if voice_message: - payload["flags"] = payload.setdefault("flags", 0) + 8192 + flags = MessageFlags(ephemeral=ephemeral) if view is not None: payload["components"] = view.to_components() @@ -961,6 +953,11 @@ async def send_message( elif not all(isinstance(file, File) for file in files): raise InvalidArgument("files parameter must be a list of File") + if any(isinstance(file, VoiceMessage) for file in files): + flags = flags + MessageFlags(is_voice_message=True) + + payload["flags"] = flags.value + parent = self._parent adapter = async_context.get() http = parent._state.http diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 6e7a10592c..6bab760e91 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -47,6 +47,7 @@ InvalidArgument, NotFound, ) +from ..file import VoiceMessage from ..flags import MessageFlags from ..http import Route from ..message import Attachment, Message @@ -507,28 +508,27 @@ def create_interaction_response( attachments = [] files = files or [] for index, file in enumerate(files): - attachments.append( - { - "id": index, - "filename": file.filename, - "description": file.description, - "duration_secs": file.duration_secs, - "waveform": file.waveform, - # TODO: Fix content_type - # "content_type": "audio/mp3", - } - ) + attachment_info = { + "id": index, + "filename": file.filename, + "description": file.description, + } + if isinstance(file, VoiceMessage): + attachment_info.update( + waveform=file.waveform, + duration_secs=file.duration_secs, + ) + attachments.append(attachment_info) form.append( { "name": f"files[{index}]", "value": file.fp, "filename": file.filename, - # TODO: Fix content_type - # "content_type": "application/octet-stream", - # "content_type": "audio/mp3", + "content_type": "application/octet-stream", } ) - payload["flags"] = 1 << 13 + if files and any(isinstance(f, VoiceMessage) for f in files): + payload["flags"] = MessageFlags(is_voice_message=True).value payload["attachments"] = attachments form[0]["value"] = utils._to_json(payload) @@ -635,7 +635,6 @@ def handle_message_parameters( allowed_mentions: AllowedMentions | None = MISSING, previous_allowed_mentions: AllowedMentions | None = None, suppress: bool = False, - voice_message: bool = False, ) -> ExecuteWebhookParameters: if files is not MISSING and file is not MISSING: raise TypeError("Cannot mix file and files keyword arguments.") @@ -667,9 +666,8 @@ def handle_message_parameters( payload["username"] = username flags = MessageFlags( - suppress_embeds=suppress, ephemeral=ephemeral, is_voice_message=voice_message + suppress_embeds=suppress, ephemeral=ephemeral, ) - payload["flags"] = flags.value if applied_tags is not MISSING: payload["applied_tags"] = applied_tags @@ -690,6 +688,7 @@ def handle_message_parameters( files = [file] if files: + voice_message = False for index, file in enumerate(files): multipart_files.append( { @@ -699,21 +698,26 @@ def handle_message_parameters( "content_type": "application/octet-stream", } ) - _attachments.append( - { - "id": index, - "filename": file.filename, - "description": file.description, - "waveform": file.waveform, - "duration_secs": file.duration_secs, - # TODO: Fix content_type - "content_type": "audio/wav", - } - ) + attachment_info = { + "id": index, + "filename": file.filename, + "description": file.description, + } + if isinstance(file, VoiceMessage): + voice_message = True + attachment_info.update( + waveform=file.waveform, + duration_secs=file.duration_secs, + ) + _attachments.append(attachment_info) + if voice_message: + flags = flags + MessageFlags(is_voice_message=True) if _attachments: payload["attachments"] = _attachments + payload["flags"] = flags.value + if multipart_files: multipart.append({"name": "payload_json", "value": utils._to_json(payload)}) payload = None @@ -1595,7 +1599,6 @@ async def send( thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, - voice_message: bool = MISSING, wait: Literal[True], delete_after: float = None, ) -> WebhookMessage: ... @@ -1619,7 +1622,6 @@ async def send( thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, - voice_message: bool = MISSING, wait: Literal[False] = ..., delete_after: float = None, ) -> None: ... @@ -1642,7 +1644,6 @@ async def send( thread: Snowflake = MISSING, thread_name: str | None = None, applied_tags: list[Snowflake] = MISSING, - voice_message: bool = MISSING, wait: bool = False, delete_after: float = None, ) -> WebhookMessage | None: @@ -1725,10 +1726,6 @@ async def send( The poll to send. .. versionadded:: 2.6 - voice_message: :class:`bool` - If the file should be treated as a voice message. - - .. versionadded:: 2.7 Returns ------- @@ -1806,7 +1803,6 @@ async def send( applied_tags=applied_tags, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, - voice_message=voice_message, ) adapter = async_context.get() thread_id: int | None = None From c2dcc0d91101d22008888059d2627074004d990a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:53:01 +0000 Subject: [PATCH 8/8] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/abc.py | 4 +++- discord/file.py | 14 +++++++++----- discord/http.py | 8 ++++---- discord/webhook/async_.py | 3 ++- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 17c9943683..49c4420306 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1623,7 +1623,9 @@ async def send( raise InvalidArgument("files parameter must be a list of File") if files is not None: - flags = flags + MessageFlags(is_voice_message=any(isinstance(f, VoiceMessage) for f in files)) + flags = flags + MessageFlags( + is_voice_message=any(isinstance(f, VoiceMessage) for f in files) + ) try: data = await state.http.send_files( channel.id, diff --git a/discord/file.py b/discord/file.py index 9b15bdd41f..fb0e0908dd 100644 --- a/discord/file.py +++ b/discord/file.py @@ -29,7 +29,10 @@ import os from typing import TYPE_CHECKING -__all__ = ("File", "VoiceMessage", ) +__all__ = ( + "File", + "VoiceMessage", +) class File: @@ -157,7 +160,7 @@ class VoiceMessage(File): multiple requests. Attributes - ----------- + ---------- fp: Union[:class:`os.PathLike`, :class:`io.BufferedIOBase`] A audio file-like object opened in binary mode and read mode or a filename representing a file in the hard drive to @@ -189,13 +192,14 @@ class VoiceMessage(File): "duration_secs", ) - def __init__(self, + def __init__( + self, fp: str | bytes | os.PathLike | io.BufferedIOBase, # filename: str | None = None, waveform: str = "", duration_secs: float = 0.0, - **kwargs - ): + **kwargs, + ): super().__init__(fp, **kwargs) self.waveform = waveform self.duration_secs = duration_secs diff --git a/discord/http.py b/discord/http.py index 5afc3fce21..e8f461befd 100644 --- a/discord/http.py +++ b/discord/http.py @@ -596,10 +596,10 @@ def send_multipart_helper( form.append({"name": "payload_json"}) for index, file in enumerate(files): attachment_info = { - "id": index, - "filename": file.filename, - "description": file.description, - } + "id": index, + "filename": file.filename, + "description": file.description, + } if isinstance(file, VoiceMessage): attachment_info.update( waveform=file.waveform, diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 6bab760e91..1daa44d458 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -666,7 +666,8 @@ def handle_message_parameters( payload["username"] = username flags = MessageFlags( - suppress_embeds=suppress, ephemeral=ephemeral, + suppress_embeds=suppress, + ephemeral=ephemeral, ) if applied_tags is not MISSING: