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