From 4480fea5d7016401322ae28bb531e761f4050a36 Mon Sep 17 00:00:00 2001 From: blord0 Date: Wed, 16 Jul 2025 19:50:19 +0100 Subject: [PATCH 01/25] Add VoiceMessageFile class --- discord/file.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/discord/file.py b/discord/file.py index 7e4df415b241..86eb98bcf179 100644 --- a/discord/file.py +++ b/discord/file.py @@ -27,12 +27,14 @@ import os import io +import base64 from .utils import MISSING # fmt: off __all__ = ( 'File', + 'VoiceMessageFile', ) # fmt: on @@ -157,3 +159,41 @@ def to_dict(self, index: int) -> Dict[str, Any]: payload['description'] = self.description return payload + + +class VoiceMessageFile(File): + """A file object used for sending voice messages. + + This is a subclass of :class:`File` that is specifically used for sending voice messages. + + .. versionadded:: 2.6 + """ + + def __init__( + self, + fp: Union[str, bytes, os.PathLike[Any], io.BufferedIOBase], + duration: float = 5.0, + waveform: Optional[str] = None, + ): + super().__init__(fp, filename="voice-message.ogg", spoiler=False) + self.duration = duration + self._waveform = waveform + self.uploaded_filename = None + + def to_dict(self, index: int) -> Dict[str, Any]: + payload = super().to_dict(index) + payload['duration_secs'] = self.duration + payload['waveform'] = self.waveform + if self.uploaded_filename is not None: + payload['uploaded_filename'] = self.uploaded_filename + return payload + + @property + def waveform(self) -> str: + """:class:`bytes`: The waveform data for the voice message.""" + if self._waveform is None: + return base64.b64encode(os.urandom(256)).decode('utf-8') + return self._waveform + + def size(self): + return 47194 # Placeholder, figure out how to get size \ No newline at end of file From 31344ca86d550c119da976e21f816fb76c3682aa Mon Sep 17 00:00:00 2001 From: blord0 Date: Wed, 16 Jul 2025 19:50:45 +0100 Subject: [PATCH 02/25] Add abc.channel.send_voice_message to allow sending voice messages --- discord/abc.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index 713398a7db4d..4720a42fcd39 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -55,7 +55,7 @@ from .permissions import PermissionOverwrite, Permissions from .role import Role from .invite import Invite -from .file import File +from .file import File, VoiceMessageFile from .http import handle_message_parameters from .voice_client import VoiceClient, VoiceProtocol from .sticker import GuildSticker, StickerItem @@ -1915,6 +1915,31 @@ async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Op # There's no data left after this break + async def send_voice_message(self, file: VoiceMessageFile): + """|coro| + + Sends a voice message to the destination. + + Parameters + ----------- + file: :class:`~discord.VoiceMessageFile` + The voice message file to send. + + Raises + ------- + ~discord.HTTPException + Sending the voice message failed. + ~discord.Forbidden + You do not have the proper permissions to send the voice message. + + Returns + -------- + :class:`~discord.Message` + The message that was sent. + """ + channel = await self._get_channel() + data = await self._state.http.send_voice_message(channel.id, file) + return self._state.create_message(channel=channel, data=data) class Connectable(Protocol): """An ABC that details the common operations on a channel that can From aa5c26fcaca2652ef00a9e66f98802268bd4f2a1 Mon Sep 17 00:00:00 2001 From: blord0 Date: Wed, 16 Jul 2025 19:51:41 +0100 Subject: [PATCH 03/25] Start work on sending the voice messages (not working) --- discord/http.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/discord/http.py b/discord/http.py index 02fd1e136db4..9963acb60af5 100644 --- a/discord/http.py +++ b/discord/http.py @@ -53,7 +53,7 @@ from .errors import HTTPException, RateLimited, Forbidden, NotFound, LoginFailure, DiscordServerError, GatewayNotFound from .gateway import DiscordClientWebSocketResponse -from .file import File +from .file import File, VoiceMessageFile from .mentions import AllowedMentions from . import __version__, utils from .utils import MISSING @@ -243,7 +243,10 @@ def handle_message_parameters( file_index = 0 attachments_payload = [] for attachment in attachments: - if isinstance(attachment, File): + if isinstance(attachment, VoiceMessageFile): + attachments_payload.append(attachment.to_dict(file_index)) + file_index += 1 + elif isinstance(attachment, File): attachments_payload.append(attachment.to_dict(file_index)) file_index += 1 else: @@ -269,6 +272,9 @@ def handle_message_parameters( multipart = [] if files: multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)}) + print(";;;;") + print(utils._to_json(payload)) + print(";;;;") payload = None for index, file in enumerate(files): multipart.append( @@ -617,7 +623,9 @@ async def request( headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ') kwargs['headers'] = headers - + print("=-=-=-=-=-=-=-=-=-=-=-=") + print(headers) + print("=-=-=-=-=-=-=-=-=-=-=-=") # Proxy support if self.proxy is not None: kwargs['proxy'] = self.proxy @@ -868,6 +876,10 @@ def send_message( *, params: MultipartParameters, ) -> Response[message.Message]: + print(":::::") + print(params.payload) + print(params.multipart) + print(":::::") r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) if params.files: return self.request(r, files=params.files, form=params.multipart) @@ -1058,6 +1070,47 @@ def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Op def pins_from(self, channel_id: Snowflake) -> Response[List[message.Message]]: return self.request(Route('GET', '/channels/{channel_id}/pins', channel_id=channel_id)) + async def send_voice_message(self, channel_id: Snowflake, VoiceMessage: VoiceMessageFile): + """|coro| + + Sends a voice message to the specified channel. + + Parameters + ----------- + channel_id: :class:`~discord.abc.Snowflake` + The ID of the channel to send the voice message to. + VoiceMessage: :class:`~discord.VoiceMessageFile` + The voice message file to send. This should be an instance of :class:`~discord.VoiceMessageFile`. + """ + from .message import MessageFlags + uploadRoute = Route('POST', '/channels/{channel_id}/attachments', channel_id=channel_id) + payload = { + "files": [{ + "filename": "voice-message.ogg", + "file_size": VoiceMessage.size(), + "id": 0, + }] + } + response = await self.request(uploadRoute, json=payload) + + upload_data = response['attachments'][0] + upload_url = upload_data["upload_url"] + uploaded_filename = upload_data["upload_filename"] + + x: Optional[aiohttp.ClientResponse] = None + x = await self.__session.request("PUT", upload_url, headers={"Content-Type": "audio/ogg"}, data=VoiceMessage.fp.read()) + print("*********") + print(upload_url) + print(x.read()) + print("*********") + + VoiceMessage.uploaded_filename = uploaded_filename + + r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) + params = handle_message_parameters(file=VoiceMessage, flags=MessageFlags(voice=True)) + return await self.request(r, files=params.files, form=params.multipart) + + # Member management def kick(self, user_id: Snowflake, guild_id: Snowflake, reason: Optional[str] = None) -> Response[None]: From a7270b288ba1cfea522f1c9ca5e28c097e004d49 Mon Sep 17 00:00:00 2001 From: blord0 Date: Wed, 16 Jul 2025 20:08:00 +0100 Subject: [PATCH 04/25] First working version of sending voice messages --- discord/http.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/discord/http.py b/discord/http.py index 9963acb60af5..78433c0ffbb3 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1097,18 +1097,34 @@ async def send_voice_message(self, channel_id: Snowflake, VoiceMessage: VoiceMes upload_url = upload_data["upload_url"] uploaded_filename = upload_data["upload_filename"] - x: Optional[aiohttp.ClientResponse] = None - x = await self.__session.request("PUT", upload_url, headers={"Content-Type": "audio/ogg"}, data=VoiceMessage.fp.read()) - print("*********") - print(upload_url) - print(x.read()) - print("*********") + import requests + + t = requests.put(upload_url, headers={"Content-Type": "audio/ogg"}, data=VoiceMessage.fp) + print(f"Status code: {t.status_code}") + + # x = await self.__session.request("PUT", upload_url, headers={"Content-Type": "audio/ogg"}, data=VoiceMessage.fp) + # print("*********") + # print(upload_url) + # print(x.read()) + # print("*********") VoiceMessage.uploaded_filename = uploaded_filename r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) - params = handle_message_parameters(file=VoiceMessage, flags=MessageFlags(voice=True)) - return await self.request(r, files=params.files, form=params.multipart) + + message_payload = { + "flags": 8192, # IS_VOICE_MESSAGE + "attachments": [VoiceMessage.to_dict(0)], + } + + headers = {"Authorization": f"Bot {self.token}", + "Content-Type": "application/json"} + + res = requests.post("" + r.url, headers=headers, json=message_payload) + return res.json() + + # params = handle_message_parameters(file=VoiceMessage, flags=MessageFlags(voice=True)) + # return await self.request(r, files=params.files, form=params.multipart) # Member management From 4a406832edde6feac82a7b39be713f427e5976cb Mon Sep 17 00:00:00 2001 From: blord0 Date: Wed, 16 Jul 2025 22:55:34 +0100 Subject: [PATCH 05/25] Start cleaning up parts of the code --- discord/abc.py | 1 + discord/file.py | 6 +++--- discord/http.py | 52 +++++++++++++++---------------------------------- 3 files changed, 20 insertions(+), 39 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 4720a42fcd39..52e3b8e5885b 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1941,6 +1941,7 @@ async def send_voice_message(self, file: VoiceMessageFile): data = await self._state.http.send_voice_message(channel.id, file) return self._state.create_message(channel=channel, data=data) + class Connectable(Protocol): """An ABC that details the common operations on a channel that can connect to a voice server. diff --git a/discord/file.py b/discord/file.py index 86eb98bcf179..df4aa4223551 100644 --- a/discord/file.py +++ b/discord/file.py @@ -179,7 +179,7 @@ def __init__( self.duration = duration self._waveform = waveform self.uploaded_filename = None - + def to_dict(self, index: int) -> Dict[str, Any]: payload = super().to_dict(index) payload['duration_secs'] = self.duration @@ -194,6 +194,6 @@ def waveform(self) -> str: if self._waveform is None: return base64.b64encode(os.urandom(256)).decode('utf-8') return self._waveform - + def size(self): - return 47194 # Placeholder, figure out how to get size \ No newline at end of file + return 47194 # Placeholder, figure out how to get size diff --git a/discord/http.py b/discord/http.py index 78433c0ffbb3..9467a632919b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -243,10 +243,7 @@ def handle_message_parameters( file_index = 0 attachments_payload = [] for attachment in attachments: - if isinstance(attachment, VoiceMessageFile): - attachments_payload.append(attachment.to_dict(file_index)) - file_index += 1 - elif isinstance(attachment, File): + if isinstance(attachment, File): attachments_payload.append(attachment.to_dict(file_index)) file_index += 1 else: @@ -272,9 +269,6 @@ def handle_message_parameters( multipart = [] if files: multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)}) - print(";;;;") - print(utils._to_json(payload)) - print(";;;;") payload = None for index, file in enumerate(files): multipart.append( @@ -876,10 +870,6 @@ def send_message( *, params: MultipartParameters, ) -> Response[message.Message]: - print(":::::") - print(params.payload) - print(params.multipart) - print(":::::") r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) if params.files: return self.request(r, files=params.files, form=params.multipart) @@ -1070,26 +1060,18 @@ def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Op def pins_from(self, channel_id: Snowflake) -> Response[List[message.Message]]: return self.request(Route('GET', '/channels/{channel_id}/pins', channel_id=channel_id)) - async def send_voice_message(self, channel_id: Snowflake, VoiceMessage: VoiceMessageFile): - """|coro| - - Sends a voice message to the specified channel. - - Parameters - ----------- - channel_id: :class:`~discord.abc.Snowflake` - The ID of the channel to send the voice message to. - VoiceMessage: :class:`~discord.VoiceMessageFile` - The voice message file to send. This should be an instance of :class:`~discord.VoiceMessageFile`. - """ + async def send_voice_message(self, channel_id: Snowflake, voice_message: VoiceMessageFile): from .message import MessageFlags + uploadRoute = Route('POST', '/channels/{channel_id}/attachments', channel_id=channel_id) payload = { - "files": [{ - "filename": "voice-message.ogg", - "file_size": VoiceMessage.size(), - "id": 0, - }] + "files": [ + { + "filename": "voice-message.ogg", + "file_size": voice_message.size(), + "id": 0, + } + ] } response = await self.request(uploadRoute, json=payload) @@ -1099,34 +1081,32 @@ async def send_voice_message(self, channel_id: Snowflake, VoiceMessage: VoiceMes import requests - t = requests.put(upload_url, headers={"Content-Type": "audio/ogg"}, data=VoiceMessage.fp) + t = requests.put(upload_url, headers={"Content-Type": "audio/ogg"}, data=voice_message.fp) print(f"Status code: {t.status_code}") - # x = await self.__session.request("PUT", upload_url, headers={"Content-Type": "audio/ogg"}, data=VoiceMessage.fp) + # x = await self.__session.request("PUT", upload_url, headers={"Content-Type": "audio/ogg"}, data=voice_message.fp) # print("*********") # print(upload_url) # print(x.read()) # print("*********") - VoiceMessage.uploaded_filename = uploaded_filename + voice_message.uploaded_filename = uploaded_filename r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) message_payload = { "flags": 8192, # IS_VOICE_MESSAGE - "attachments": [VoiceMessage.to_dict(0)], + "attachments": [voice_message.to_dict(0)], } - headers = {"Authorization": f"Bot {self.token}", - "Content-Type": "application/json"} + headers = {"Authorization": f"Bot {self.token}", "Content-Type": "application/json"} res = requests.post("" + r.url, headers=headers, json=message_payload) return res.json() - # params = handle_message_parameters(file=VoiceMessage, flags=MessageFlags(voice=True)) + # params = handle_message_parameters(file=voice_message, flags=MessageFlags(voice=True)) # return await self.request(r, files=params.files, form=params.multipart) - # Member management def kick(self, user_id: Snowflake, guild_id: Snowflake, reason: Optional[str] = None) -> Response[None]: From 9a4617ec4fbc07a255975ef10a3cbc80dfef75ba Mon Sep 17 00:00:00 2001 From: blord0 Date: Wed, 16 Jul 2025 23:20:34 +0100 Subject: [PATCH 06/25] More cleanup --- discord/http.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/discord/http.py b/discord/http.py index 9467a632919b..f2ea685eaaf9 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1060,10 +1060,8 @@ def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Op def pins_from(self, channel_id: Snowflake) -> Response[List[message.Message]]: return self.request(Route('GET', '/channels/{channel_id}/pins', channel_id=channel_id)) - async def send_voice_message(self, channel_id: Snowflake, voice_message: VoiceMessageFile): - from .message import MessageFlags - - uploadRoute = Route('POST', '/channels/{channel_id}/attachments', channel_id=channel_id) + async def send_voice_message(self, channel_id: Snowflake, voice_message: VoiceMessageFile) -> Any: + upload_route = Route('POST', '/channels/{channel_id}/attachments', channel_id=channel_id) payload = { "files": [ { @@ -1073,7 +1071,7 @@ async def send_voice_message(self, channel_id: Snowflake, voice_message: VoiceMe } ] } - response = await self.request(uploadRoute, json=payload) + response = await self.request(upload_route, json=payload) upload_data = response['attachments'][0] upload_url = upload_data["upload_url"] @@ -1101,11 +1099,9 @@ async def send_voice_message(self, channel_id: Snowflake, voice_message: VoiceMe headers = {"Authorization": f"Bot {self.token}", "Content-Type": "application/json"} - res = requests.post("" + r.url, headers=headers, json=message_payload) - return res.json() - - # params = handle_message_parameters(file=voice_message, flags=MessageFlags(voice=True)) - # return await self.request(r, files=params.files, form=params.multipart) + response = await self.__session.request("post", r.url, headers=headers, json=message_payload) + data = await json_or_text(response) + return data # Member management From f9ca81a385fb72e12e9ca7c4a542e5d5c29501f1 Mon Sep 17 00:00:00 2001 From: blord0 Date: Wed, 16 Jul 2025 23:30:06 +0100 Subject: [PATCH 07/25] Found a much simpler method to send voice messages --- discord/abc.py | 13 +++++++++---- discord/http.py | 45 +-------------------------------------------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 52e3b8e5885b..4e10e1305d16 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1404,6 +1404,7 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., + voice: bool = ..., ) -> Message: ... @@ -1425,6 +1426,7 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., + voice: bool = ..., ) -> Message: ... @@ -1446,6 +1448,7 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., + voice: bool = ..., ) -> Message: ... @@ -1467,6 +1470,7 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., + voice: bool = ..., ) -> Message: ... @@ -1489,6 +1493,7 @@ async def send( suppress_embeds: bool = False, silent: bool = False, poll: Optional[Poll] = None, + voice: bool = False, ) -> Message: """|coro| @@ -1624,12 +1629,13 @@ async def send( if view and not hasattr(view, '__discord_ui_view__'): raise TypeError(f'view parameter must be View not {view.__class__.__name__}') - if suppress_embeds or silent: + if suppress_embeds or silent or voice: from .message import MessageFlags # circular import flags = MessageFlags._from_value(0) flags.suppress_embeds = suppress_embeds flags.suppress_notifications = silent + flags.voice = voice else: flags = MISSING @@ -1937,9 +1943,8 @@ async def send_voice_message(self, file: VoiceMessageFile): :class:`~discord.Message` The message that was sent. """ - channel = await self._get_channel() - data = await self._state.http.send_voice_message(channel.id, file) - return self._state.create_message(channel=channel, data=data) + + return await self.send(file=file, voice=True) class Connectable(Protocol): diff --git a/discord/http.py b/discord/http.py index f2ea685eaaf9..592bbb884c2f 100644 --- a/discord/http.py +++ b/discord/http.py @@ -53,7 +53,7 @@ from .errors import HTTPException, RateLimited, Forbidden, NotFound, LoginFailure, DiscordServerError, GatewayNotFound from .gateway import DiscordClientWebSocketResponse -from .file import File, VoiceMessageFile +from .file import File from .mentions import AllowedMentions from . import __version__, utils from .utils import MISSING @@ -1060,49 +1060,6 @@ def unpin_message(self, channel_id: Snowflake, message_id: Snowflake, reason: Op def pins_from(self, channel_id: Snowflake) -> Response[List[message.Message]]: return self.request(Route('GET', '/channels/{channel_id}/pins', channel_id=channel_id)) - async def send_voice_message(self, channel_id: Snowflake, voice_message: VoiceMessageFile) -> Any: - upload_route = Route('POST', '/channels/{channel_id}/attachments', channel_id=channel_id) - payload = { - "files": [ - { - "filename": "voice-message.ogg", - "file_size": voice_message.size(), - "id": 0, - } - ] - } - response = await self.request(upload_route, json=payload) - - upload_data = response['attachments'][0] - upload_url = upload_data["upload_url"] - uploaded_filename = upload_data["upload_filename"] - - import requests - - t = requests.put(upload_url, headers={"Content-Type": "audio/ogg"}, data=voice_message.fp) - print(f"Status code: {t.status_code}") - - # x = await self.__session.request("PUT", upload_url, headers={"Content-Type": "audio/ogg"}, data=voice_message.fp) - # print("*********") - # print(upload_url) - # print(x.read()) - # print("*********") - - voice_message.uploaded_filename = uploaded_filename - - r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) - - message_payload = { - "flags": 8192, # IS_VOICE_MESSAGE - "attachments": [voice_message.to_dict(0)], - } - - headers = {"Authorization": f"Bot {self.token}", "Content-Type": "application/json"} - - response = await self.__session.request("post", r.url, headers=headers, json=message_payload) - data = await json_or_text(response) - return data - # Member management def kick(self, user_id: Snowflake, guild_id: Snowflake, reason: Optional[str] = None) -> Response[None]: From eb623381a3041ddc7c266367b9ed177333d3fe46 Mon Sep 17 00:00:00 2001 From: blord0 Date: Thu, 17 Jul 2025 02:57:00 +0100 Subject: [PATCH 08/25] `size` method no longer needed --- discord/file.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/file.py b/discord/file.py index df4aa4223551..ccac77cd056c 100644 --- a/discord/file.py +++ b/discord/file.py @@ -194,6 +194,3 @@ def waveform(self) -> str: if self._waveform is None: return base64.b64encode(os.urandom(256)).decode('utf-8') return self._waveform - - def size(self): - return 47194 # Placeholder, figure out how to get size From 27bca439184c87d9b751f2269df3c5043f758120 Mon Sep 17 00:00:00 2001 From: blord0 Date: Thu, 17 Jul 2025 13:04:57 +0100 Subject: [PATCH 09/25] Remove unncessary code --- discord/abc.py | 34 ++++++++-------------------------- discord/file.py | 3 --- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 4e10e1305d16..84a9349e659c 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -55,7 +55,7 @@ from .permissions import PermissionOverwrite, Permissions from .role import Role from .invite import Invite -from .file import File, VoiceMessageFile +from .file import File from .http import handle_message_parameters from .voice_client import VoiceClient, VoiceProtocol from .sticker import GuildSticker, StickerItem @@ -1584,6 +1584,13 @@ async def send( The poll to send with this message. .. versionadded:: 2.4 + voice: :class:`bool` + If the message is a voice message. + + .. warning:: + + `file` attribute must be a :class:`discord.VoiceMessageFile` for this to work. Content must also be `None` + .. versionadded:: 2.6 Raises -------- @@ -1921,31 +1928,6 @@ async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Op # There's no data left after this break - async def send_voice_message(self, file: VoiceMessageFile): - """|coro| - - Sends a voice message to the destination. - - Parameters - ----------- - file: :class:`~discord.VoiceMessageFile` - The voice message file to send. - - Raises - ------- - ~discord.HTTPException - Sending the voice message failed. - ~discord.Forbidden - You do not have the proper permissions to send the voice message. - - Returns - -------- - :class:`~discord.Message` - The message that was sent. - """ - - return await self.send(file=file, voice=True) - class Connectable(Protocol): """An ABC that details the common operations on a channel that can diff --git a/discord/file.py b/discord/file.py index ccac77cd056c..8155e4fd1e0c 100644 --- a/discord/file.py +++ b/discord/file.py @@ -178,14 +178,11 @@ def __init__( super().__init__(fp, filename="voice-message.ogg", spoiler=False) self.duration = duration self._waveform = waveform - self.uploaded_filename = None def to_dict(self, index: int) -> Dict[str, Any]: payload = super().to_dict(index) payload['duration_secs'] = self.duration payload['waveform'] = self.waveform - if self.uploaded_filename is not None: - payload['uploaded_filename'] = self.uploaded_filename return payload @property From d1747b9efab08be4b00de6975fa32d9a4ecc0afd Mon Sep 17 00:00:00 2001 From: blord0 Date: Thu, 17 Jul 2025 13:18:53 +0100 Subject: [PATCH 10/25] Doc fixes and made `duration` a required field --- discord/file.py | 8 +++++++- docs/api.rst | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/discord/file.py b/discord/file.py index 8155e4fd1e0c..e46d49411ddf 100644 --- a/discord/file.py +++ b/discord/file.py @@ -167,12 +167,18 @@ class VoiceMessageFile(File): This is a subclass of :class:`File` that is specifically used for sending voice messages. .. versionadded:: 2.6 + + Attributes + ----------- + duration: :class:`float` + The duration of the voice message in seconds. Does not need to be accurate + """ def __init__( self, fp: Union[str, bytes, os.PathLike[Any], io.BufferedIOBase], - duration: float = 5.0, + duration: float, waveform: Optional[str] = None, ): super().__init__(fp, filename="voice-message.ogg", spoiler=False) diff --git a/docs/api.rst b/docs/api.rst index c7d9e351f503..5f58fa480172 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5632,6 +5632,14 @@ File .. autoclass:: File :members: +VoiceMessageFile +~~~~~~~~~~~~~~~~~ + +.. attributetable:: VoiceMessageFile + +.. autoclass:: VoiceMessageFile + :members: + Colour ~~~~~~ From 2a96d138105ee89411b36c20b79176f9c1fcc061 Mon Sep 17 00:00:00 2001 From: blord0 <68508813+blord0@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:22:04 +0100 Subject: [PATCH 11/25] Remove print statements --- discord/http.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/http.py b/discord/http.py index 592bbb884c2f..544bf2becf69 100644 --- a/discord/http.py +++ b/discord/http.py @@ -617,9 +617,6 @@ async def request( headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ') kwargs['headers'] = headers - print("=-=-=-=-=-=-=-=-=-=-=-=") - print(headers) - print("=-=-=-=-=-=-=-=-=-=-=-=") # Proxy support if self.proxy is not None: kwargs['proxy'] = self.proxy From 8332ca3f2844419eac7b972caad26b3263e85464 Mon Sep 17 00:00:00 2001 From: blord0 Date: Thu, 17 Jul 2025 13:36:20 +0100 Subject: [PATCH 12/25] Move `VoiceMessageFile` into `File` --- discord/file.py | 54 ++++++++++++++----------------------------------- discord/http.py | 1 + docs/api.rst | 8 -------- 3 files changed, 16 insertions(+), 47 deletions(-) diff --git a/discord/file.py b/discord/file.py index e46d49411ddf..804c3c0e64a7 100644 --- a/discord/file.py +++ b/discord/file.py @@ -34,7 +34,6 @@ # fmt: off __all__ = ( 'File', - 'VoiceMessageFile', ) # fmt: on @@ -79,7 +78,7 @@ class File: .. versionadded:: 2.0 """ - __slots__ = ('fp', '_filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer') + __slots__ = ('fp', '_filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer', 'duation', '_waveform') def __init__( self, @@ -88,6 +87,8 @@ def __init__( *, spoiler: bool = MISSING, description: Optional[str] = None, + duration: Optional[float] = None, + waveform: Optional[str] = None, ): if isinstance(fp, io.IOBase): if not (fp.seekable() and fp.readable()): @@ -119,6 +120,8 @@ def __init__( self.spoiler: bool = spoiler self.description: Optional[str] = description + self.duation = duration + self._waveform = waveform @property def filename(self) -> str: @@ -128,6 +131,13 @@ def filename(self) -> str: """ return 'SPOILER_' + self._filename if self.spoiler else self._filename + @property + def waveform(self) -> str: + """:class:`str`: The waveform data for the voice message.""" + if self._waveform is None: + return base64.b64encode(os.urandom(256)).decode('utf-8') + return self._waveform + @filename.setter def filename(self, value: str) -> None: self._filename, self.spoiler = _strip_spoiler(value) @@ -158,42 +168,8 @@ def to_dict(self, index: int) -> Dict[str, Any]: if self.description is not None: payload['description'] = self.description - return payload - - -class VoiceMessageFile(File): - """A file object used for sending voice messages. - - This is a subclass of :class:`File` that is specifically used for sending voice messages. - - .. versionadded:: 2.6 - - Attributes - ----------- - duration: :class:`float` - The duration of the voice message in seconds. Does not need to be accurate - - """ - - def __init__( - self, - fp: Union[str, bytes, os.PathLike[Any], io.BufferedIOBase], - duration: float, - waveform: Optional[str] = None, - ): - super().__init__(fp, filename="voice-message.ogg", spoiler=False) - self.duration = duration - self._waveform = waveform + if self.duation is not None: + payload['duration_secs'] = self.duation + payload['waveform'] = self.waveform - def to_dict(self, index: int) -> Dict[str, Any]: - payload = super().to_dict(index) - payload['duration_secs'] = self.duration - payload['waveform'] = self.waveform return payload - - @property - def waveform(self) -> str: - """:class:`bytes`: The waveform data for the voice message.""" - if self._waveform is None: - return base64.b64encode(os.urandom(256)).decode('utf-8') - return self._waveform diff --git a/discord/http.py b/discord/http.py index 544bf2becf69..02fd1e136db4 100644 --- a/discord/http.py +++ b/discord/http.py @@ -617,6 +617,7 @@ async def request( headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ') kwargs['headers'] = headers + # Proxy support if self.proxy is not None: kwargs['proxy'] = self.proxy diff --git a/docs/api.rst b/docs/api.rst index 5f58fa480172..c7d9e351f503 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5632,14 +5632,6 @@ File .. autoclass:: File :members: -VoiceMessageFile -~~~~~~~~~~~~~~~~~ - -.. attributetable:: VoiceMessageFile - -.. autoclass:: VoiceMessageFile - :members: - Colour ~~~~~~ From 5231d514f0107b5586c6b84355ea267c1b04fbe1 Mon Sep 17 00:00:00 2001 From: blord0 Date: Thu, 17 Jul 2025 13:40:16 +0100 Subject: [PATCH 13/25] Remove final reference to `VoiceMessageFile` --- discord/abc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 84a9349e659c..0cdf6d771fcd 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1587,9 +1587,6 @@ async def send( voice: :class:`bool` If the message is a voice message. - .. warning:: - - `file` attribute must be a :class:`discord.VoiceMessageFile` for this to work. Content must also be `None` .. versionadded:: 2.6 Raises From 60030d8ea145fcf6337169f4edb1a04dad609ed9 Mon Sep 17 00:00:00 2001 From: blord0 Date: Fri, 18 Jul 2025 15:41:57 +0100 Subject: [PATCH 14/25] Add error checking --- discord/abc.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index 0cdf6d771fcd..c33396b9e32f 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1643,6 +1643,16 @@ async def send( else: flags = MISSING + if voice: + if content is not None: + raise TypeError('Cannot send content with a voice message') + if embeds is not None: + raise TypeError('Cannot send embeds with a voice message') + if file is None: + raise TypeError('A voice message must have a file') + if file.duation is None: + raise TypeError('A voice message file must have a duration') + if nonce is None: nonce = secrets.randbits(64) From 1d2ab9c78e4545a24cb7f56525b6fa2831f784b1 Mon Sep 17 00:00:00 2001 From: blord0 Date: Fri, 18 Jul 2025 17:33:24 +0100 Subject: [PATCH 15/25] Fix error checking --- discord/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index c33396b9e32f..2e0f971b794f 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1646,7 +1646,7 @@ async def send( if voice: if content is not None: raise TypeError('Cannot send content with a voice message') - if embeds is not None: + if embed is not None or embeds is not None: raise TypeError('Cannot send embeds with a voice message') if file is None: raise TypeError('A voice message must have a file') From 3dd7f8fefb59202cd57d5478cf21b9f8d01f56c8 Mon Sep 17 00:00:00 2001 From: blord0 Date: Mon, 28 Jul 2025 01:24:29 +0100 Subject: [PATCH 16/25] Rename duation to duration --- discord/file.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/discord/file.py b/discord/file.py index 804c3c0e64a7..baf7102fbb76 100644 --- a/discord/file.py +++ b/discord/file.py @@ -120,7 +120,7 @@ def __init__( self.spoiler: bool = spoiler self.description: Optional[str] = description - self.duation = duration + self.duration = duration self._waveform = waveform @property @@ -133,7 +133,9 @@ def filename(self) -> str: @property def waveform(self) -> str: - """:class:`str`: The waveform data for the voice message.""" + """:class:`str`: The waveform data for the voice message. + + .. versionadded:: 2.6""" if self._waveform is None: return base64.b64encode(os.urandom(256)).decode('utf-8') return self._waveform @@ -168,8 +170,8 @@ def to_dict(self, index: int) -> Dict[str, Any]: if self.description is not None: payload['description'] = self.description - if self.duation is not None: - payload['duration_secs'] = self.duation + if self.duration is not None: + payload['duration_secs'] = self.duration payload['waveform'] = self.waveform return payload From e5cca7db9219626a1c5bbee9c05ac54bd54e5e59 Mon Sep 17 00:00:00 2001 From: blord0 Date: Mon, 28 Jul 2025 01:26:27 +0100 Subject: [PATCH 17/25] Add File.voice attribute --- discord/file.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/discord/file.py b/discord/file.py index baf7102fbb76..5f173e4c9d7c 100644 --- a/discord/file.py +++ b/discord/file.py @@ -76,9 +76,35 @@ class File: The file description to display, currently only supported for images. .. versionadded:: 2.0 + + voice: :class:`bool` + Whether the file is a voice message. If left unspecified, the :attr:`~File.duration` is used + to determine if the file is a voice message. + + .. note:: + + Voice files must be an audio only format. Known supported formats are: mp3, ogg, wav, aac, and flac. + + .. versionadded:: 2.6 + + duration: Optional[:class:`float`] + The duration of the voice message in seconds + + .. versionadded:: 2.6 """ - __slots__ = ('fp', '_filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer', 'duation', '_waveform') + __slots__ = ( + 'fp', + '_filename', + 'spoiler', + 'description', + '_original_pos', + '_owner', + '_closer', + 'duration', + '_waveform', + 'voice', + ) def __init__( self, @@ -87,6 +113,7 @@ def __init__( *, spoiler: bool = MISSING, description: Optional[str] = None, + voice: bool = MISSING, duration: Optional[float] = None, waveform: Optional[str] = None, ): @@ -123,6 +150,13 @@ def __init__( self.duration = duration self._waveform = waveform + if voice is MISSING: + voice = duration is not None + self.voice = voice + + if duration is None and voice: + raise TypeError('Voice messages must have a duration') + @property def filename(self) -> str: """:class:`str`: The filename to display when uploading to Discord. From 8f1d548f91fe6b27cec73ac66d2451a9d3d98df6 Mon Sep 17 00:00:00 2001 From: blord0 Date: Mon, 28 Jul 2025 01:26:50 +0100 Subject: [PATCH 18/25] Change checking for voice messages to use File.voice --- discord/abc.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 2e0f971b794f..4fa98b59ee17 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1404,7 +1404,6 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., - voice: bool = ..., ) -> Message: ... @@ -1426,7 +1425,6 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., - voice: bool = ..., ) -> Message: ... @@ -1448,7 +1446,6 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., - voice: bool = ..., ) -> Message: ... @@ -1470,7 +1467,6 @@ async def send( suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., - voice: bool = ..., ) -> Message: ... @@ -1493,7 +1489,6 @@ async def send( suppress_embeds: bool = False, silent: bool = False, poll: Optional[Poll] = None, - voice: bool = False, ) -> Message: """|coro| @@ -1584,10 +1579,6 @@ async def send( The poll to send with this message. .. versionadded:: 2.4 - voice: :class:`bool` - If the message is a voice message. - - .. versionadded:: 2.6 Raises -------- @@ -1633,6 +1624,14 @@ async def send( if view and not hasattr(view, '__discord_ui_view__'): raise TypeError(f'view parameter must be View not {view.__class__.__name__}') + voice = False + if file is not None and file.voice: + if content is not None: + raise TypeError('Cannot send content with a voice message') + if embed is not None or embeds is not None: + raise TypeError('Cannot send embeds with a voice message') + voice = True + if suppress_embeds or silent or voice: from .message import MessageFlags # circular import @@ -1643,16 +1642,6 @@ async def send( else: flags = MISSING - if voice: - if content is not None: - raise TypeError('Cannot send content with a voice message') - if embed is not None or embeds is not None: - raise TypeError('Cannot send embeds with a voice message') - if file is None: - raise TypeError('A voice message must have a file') - if file.duation is None: - raise TypeError('A voice message file must have a duration') - if nonce is None: nonce = secrets.randbits(64) From 9936b0d21691deff634869b0ba01c46c36b56fff Mon Sep 17 00:00:00 2001 From: blord0 Date: Mon, 28 Jul 2025 01:52:10 +0100 Subject: [PATCH 19/25] Formatting change --- discord/file.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/file.py b/discord/file.py index 5f173e4c9d7c..b27aca7eeb9b 100644 --- a/discord/file.py +++ b/discord/file.py @@ -83,7 +83,9 @@ class File: .. note:: - Voice files must be an audio only format. Known supported formats are: mp3, ogg, wav, aac, and flac. + Voice files must be an audio only format. + + A *non-exhaustive* list of supported formats are: `mp3`, `ogg`, `wav`, `aac`, and `flac`. .. versionadded:: 2.6 From 8bc906e7650a00897b43b23feb3f1e707591d7be Mon Sep 17 00:00:00 2001 From: blord0 Date: Mon, 28 Jul 2025 18:31:08 +0100 Subject: [PATCH 20/25] Add real generation of waveforms for Opus files --- discord/file.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/discord/file.py b/discord/file.py index b27aca7eeb9b..80631792e6d6 100644 --- a/discord/file.py +++ b/discord/file.py @@ -28,6 +28,9 @@ import os import io import base64 +from .oggparse import OggStream +from .opus import Decoder +import struct from .utils import MISSING @@ -85,8 +88,8 @@ class File: Voice files must be an audio only format. - A *non-exhaustive* list of supported formats are: `mp3`, `ogg`, `wav`, `aac`, and `flac`. - + A *non-exhaustive* list of supported formats are: `ogg`, `mp3`, `wav`, `aac`, and `flac`. + .. versionadded:: 2.6 duration: Optional[:class:`float`] @@ -171,9 +174,18 @@ def filename(self) -> str: def waveform(self) -> str: """:class:`str`: The waveform data for the voice message. + .. note:: + If a waveform was not given, it will be generated + + Only supports generating the waveform for Opus format files, other files will be given a random waveform + .. versionadded:: 2.6""" if self._waveform is None: - return base64.b64encode(os.urandom(256)).decode('utf-8') + try: + self._waveform = self.generate_waveform() + except Exception: + self._waveform = base64.b64encode(os.urandom(256)).decode('utf-8') + self.reset() return self._waveform @filename.setter @@ -206,8 +218,60 @@ def to_dict(self, index: int) -> Dict[str, Any]: if self.description is not None: payload['description'] = self.description - if self.duration is not None: + if self.voice: payload['duration_secs'] = self.duration payload['waveform'] = self.waveform return payload + + def generate_waveform(self) -> str: + self.reset() + ogg = OggStream(self.fp) # type: ignore + decoder = Decoder() + waveform: list[int] = [] + prefixes = [b'OpusHead', b'OpusTags'] + for packet in ogg.iter_packets(): + if packet[:8] in prefixes: + continue + + if b'vorbis' in packet: + raise TypeError("File format is 'vorbis'. Format of 'opus' is required for waveform generation") + + # these are PCM bytes in 16-bit signed little-endian form + decoded = decoder.decode(packet, fec=False) + + # 16 bits -> 2 bytes per sample + num_samples = len(decoded) // 2 + + # https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment + format = '<' + 'h' * num_samples + samples: tuple[int] = struct.unpack(format, decoded) + + waveform.extend(samples) + + # Make sure all values are positive + for i in range(len(waveform)): + if waveform[i] < 0: + waveform[i] = -waveform[i] + + # TODO: Figure out how discord sets the sample count + # Voice message I've been using has 40 samples, so using that for now + points_per_sample = len(waveform) // 40 + sample_waveform: list[int] = [] + + total, count = 0, 0 + # Average out the amplitudes for each point within a sample + for i in range(len(waveform)): + total += waveform[i] + count += 1 + if i % points_per_sample == 0: + sample_waveform.append(total // count) + total, count = 0, 0 + + # Maximum value of a waveform is 0xff (255) + highest = max(sample_waveform) + mult = 255 / highest + for i in range(len(sample_waveform)): + sample_waveform[i] = int(sample_waveform[i] * mult) + + return base64.b64encode(bytes(sample_waveform)).decode('utf-8') From dd2fd338b1a0d8bb94c53ed77b9444b11715eb16 Mon Sep 17 00:00:00 2001 From: blord0 Date: Mon, 28 Jul 2025 18:32:06 +0100 Subject: [PATCH 21/25] Formatting --- discord/file.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/file.py b/discord/file.py index 80631792e6d6..414eb876c002 100644 --- a/discord/file.py +++ b/discord/file.py @@ -89,7 +89,7 @@ class File: Voice files must be an audio only format. A *non-exhaustive* list of supported formats are: `ogg`, `mp3`, `wav`, `aac`, and `flac`. - + .. versionadded:: 2.6 duration: Optional[:class:`float`] @@ -178,7 +178,7 @@ def waveform(self) -> str: If a waveform was not given, it will be generated Only supports generating the waveform for Opus format files, other files will be given a random waveform - + .. versionadded:: 2.6""" if self._waveform is None: try: @@ -223,10 +223,10 @@ def to_dict(self, index: int) -> Dict[str, Any]: payload['waveform'] = self.waveform return payload - + def generate_waveform(self) -> str: self.reset() - ogg = OggStream(self.fp) # type: ignore + ogg = OggStream(self.fp) # type: ignore decoder = Decoder() waveform: list[int] = [] prefixes = [b'OpusHead', b'OpusTags'] @@ -238,22 +238,22 @@ def generate_waveform(self) -> str: raise TypeError("File format is 'vorbis'. Format of 'opus' is required for waveform generation") # these are PCM bytes in 16-bit signed little-endian form - decoded = decoder.decode(packet, fec=False) + decoded = decoder.decode(packet, fec=False) # 16 bits -> 2 bytes per sample num_samples = len(decoded) // 2 - + # https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment format = '<' + 'h' * num_samples samples: tuple[int] = struct.unpack(format, decoded) waveform.extend(samples) - + # Make sure all values are positive for i in range(len(waveform)): if waveform[i] < 0: waveform[i] = -waveform[i] - + # TODO: Figure out how discord sets the sample count # Voice message I've been using has 40 samples, so using that for now points_per_sample = len(waveform) // 40 From bb4de89d5ca612bb9e8e84dc16f00df1519e59d4 Mon Sep 17 00:00:00 2001 From: blord0 Date: Mon, 28 Jul 2025 19:20:31 +0100 Subject: [PATCH 22/25] Calculate correct number of points per sample --- discord/file.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/discord/file.py b/discord/file.py index 414eb876c002..206f9d780edb 100644 --- a/discord/file.py +++ b/discord/file.py @@ -225,6 +225,8 @@ def to_dict(self, index: int) -> Dict[str, Any]: return payload def generate_waveform(self) -> str: + if not self.voice: + raise TypeError("Cannot produce waveform for non voice file") self.reset() ogg = OggStream(self.fp) # type: ignore decoder = Decoder() @@ -254,9 +256,9 @@ def generate_waveform(self) -> str: if waveform[i] < 0: waveform[i] = -waveform[i] - # TODO: Figure out how discord sets the sample count - # Voice message I've been using has 40 samples, so using that for now - points_per_sample = len(waveform) // 40 + point_count: int = self.duration * 10 # type: ignore + point_count = min(point_count, 255) + points_per_sample: int = len(waveform) // point_count sample_waveform: list[int] = [] total, count = 0, 0 @@ -274,4 +276,5 @@ def generate_waveform(self) -> str: for i in range(len(sample_waveform)): sample_waveform[i] = int(sample_waveform[i] * mult) + print(len(sample_waveform)) return base64.b64encode(bytes(sample_waveform)).decode('utf-8') From 0f3bc42878167c9a2b0aaca664742abe3df0ab95 Mon Sep 17 00:00:00 2001 From: blord0 Date: Tue, 29 Jul 2025 00:43:03 +0100 Subject: [PATCH 23/25] Change TypeError to ValueError --- discord/file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/file.py b/discord/file.py index 206f9d780edb..e6d856bc9688 100644 --- a/discord/file.py +++ b/discord/file.py @@ -226,7 +226,7 @@ def to_dict(self, index: int) -> Dict[str, Any]: def generate_waveform(self) -> str: if not self.voice: - raise TypeError("Cannot produce waveform for non voice file") + raise ValueError("Cannot produce waveform for non voice file") self.reset() ogg = OggStream(self.fp) # type: ignore decoder = Decoder() @@ -237,7 +237,7 @@ def generate_waveform(self) -> str: continue if b'vorbis' in packet: - raise TypeError("File format is 'vorbis'. Format of 'opus' is required for waveform generation") + raise ValueError("File format is 'vorbis'. Format of 'opus' is required for waveform generation") # these are PCM bytes in 16-bit signed little-endian form decoded = decoder.decode(packet, fec=False) From 8bea5c3aa9ad3ba01ee06c3714a5e8ef2601f2db Mon Sep 17 00:00:00 2001 From: blord0 Date: Tue, 29 Jul 2025 01:49:52 +0100 Subject: [PATCH 24/25] Change waveform data to be input as a list of ints --- discord/file.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/discord/file.py b/discord/file.py index e6d856bc9688..5fa359c903dc 100644 --- a/discord/file.py +++ b/discord/file.py @@ -120,7 +120,7 @@ def __init__( description: Optional[str] = None, voice: bool = MISSING, duration: Optional[float] = None, - waveform: Optional[str] = None, + waveform: Optional[list[int]] = None, ): if isinstance(fp, io.IOBase): if not (fp.seekable() and fp.readable()): @@ -153,6 +153,13 @@ def __init__( self.spoiler: bool = spoiler self.description: Optional[str] = description self.duration = duration + if waveform is not None: + if len(waveform) > 256: + raise ValueError("Waveforms have a maximum of 256 values") + elif max(waveform) > 255: + raise ValueError("Maximum value of ints is 255 for waveforms") + elif min(waveform) < 0: + raise ValueError("Minimum value of ints is 0 for waveforms") self._waveform = waveform if voice is MISSING: @@ -171,8 +178,8 @@ def filename(self) -> str: return 'SPOILER_' + self._filename if self.spoiler else self._filename @property - def waveform(self) -> str: - """:class:`str`: The waveform data for the voice message. + def waveform(self) -> list[int]: + """:class:`list[int]`: The waveform data for the voice message. .. note:: If a waveform was not given, it will be generated @@ -184,7 +191,7 @@ def waveform(self) -> str: try: self._waveform = self.generate_waveform() except Exception: - self._waveform = base64.b64encode(os.urandom(256)).decode('utf-8') + self._waveform = list(os.urandom(256)) self.reset() return self._waveform @@ -220,11 +227,11 @@ def to_dict(self, index: int) -> Dict[str, Any]: if self.voice: payload['duration_secs'] = self.duration - payload['waveform'] = self.waveform + payload['waveform'] = base64.b64encode(bytes(self.waveform)).decode('utf-8') return payload - def generate_waveform(self) -> str: + def generate_waveform(self) -> list[int]: if not self.voice: raise ValueError("Cannot produce waveform for non voice file") self.reset() @@ -276,5 +283,4 @@ def generate_waveform(self) -> str: for i in range(len(sample_waveform)): sample_waveform[i] = int(sample_waveform[i] * mult) - print(len(sample_waveform)) - return base64.b64encode(bytes(sample_waveform)).decode('utf-8') + return sample_waveform From 394b16ee50e50a2a37be1bb74946f5ce09939183 Mon Sep 17 00:00:00 2001 From: blord0 Date: Tue, 29 Jul 2025 01:56:53 +0100 Subject: [PATCH 25/25] Fix doc issues --- discord/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/file.py b/discord/file.py index 5fa359c903dc..1d8314fa00c9 100644 --- a/discord/file.py +++ b/discord/file.py @@ -179,7 +179,7 @@ def filename(self) -> str: @property def waveform(self) -> list[int]: - """:class:`list[int]`: The waveform data for the voice message. + """List[:class:`int`]: The waveform data for the voice message. .. note:: If a waveform was not given, it will be generated