Skip to content

Commit

Permalink
feat: Voice Message Sending (#2579)
Browse files Browse the repository at this point in the history
Signed-off-by: Ice Wolfy <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: plun1331 <[email protected]>
Co-authored-by: Dorukyum <[email protected]>
Co-authored-by: Lala Sabathil <[email protected]>
  • Loading branch information
5 people authored Dec 28, 2024
1 parent d08d47a commit f8a7c3a
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 60 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#2620](https://github.com/Pycord-Development/pycord/pull/2620))
- Added helper methods to determine the authorizing party of an `Interaction`.
([#2659](https://github.com/Pycord-Development/pycord/pull/2659))
- Added `VoiceMessage` subclass of `File` to allow voice messages to be sent.
([#2579](https://github.com/Pycord-Development/pycord/pull/2579))

### Fixed

Expand Down
34 changes: 9 additions & 25 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1569,7 +1569,7 @@ async def send(
flags = MessageFlags(
suppress_embeds=bool(suppress),
suppress_notifications=bool(silent),
).value
)

if stickers is not None:
stickers = [sticker.id for sticker in stickers]
Expand Down Expand Up @@ -1615,27 +1615,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(
Expand All @@ -1644,6 +1624,10 @@ 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,
Expand All @@ -1658,7 +1642,7 @@ async def send(
message_reference=reference,
stickers=stickers,
components=components,
flags=flags,
flags=flags.value,
poll=poll,
)
finally:
Expand All @@ -1677,7 +1661,7 @@ async def send(
message_reference=reference,
stickers=stickers,
components=components,
flags=flags,
flags=flags.value,
poll=poll,
)

Expand Down
63 changes: 62 additions & 1 deletion discord/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
import os
from typing import TYPE_CHECKING

__all__ = ("File",)
__all__ = (
"File",
"VoiceMessage",
)


class File:
Expand Down Expand Up @@ -89,6 +92,7 @@ def __init__(
description: str | None = None,
spoiler: bool = False,
):

if isinstance(fp, io.IOBase):
if not (fp.seekable() and fp.readable()):
raise ValueError(f"File buffer {fp!r} must be seekable and readable")
Expand Down Expand Up @@ -143,3 +147,60 @@ 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::
Similar to 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.
duration_secs: Optional[:class:`float`]
The duration of the voice message.
"""

__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, filename, **kwargs)
self.waveform = waveform
self.duration_secs = duration_secs
37 changes: 23 additions & 14 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
LoginFailure,
NotFound,
)
from .file import VoiceMessage
from .gateway import DiscordClientWebSocketResponse
from .utils import MISSING, warn_deprecated

Expand Down Expand Up @@ -567,13 +568,17 @@ def send_multipart_helper(
attachments = []
form.append({"name": "payload_json"})
for index, file in enumerate(files):
attachments.append(
{
"id": index,
"filename": file.filename,
"description": file.description,
}
)
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}]",
Expand Down Expand Up @@ -633,13 +638,17 @@ 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,
}
)
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}]",
Expand Down
10 changes: 7 additions & 3 deletions discord/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -957,8 +957,7 @@ async def send_message(
if content is not None:
payload["content"] = str(content)

if ephemeral:
payload["flags"] = 64
flags = MessageFlags(ephemeral=ephemeral)

if view is not None:
payload["components"] = view.to_components()
Expand Down Expand Up @@ -996,6 +995,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
Expand Down
51 changes: 34 additions & 17 deletions discord/webhook/async_.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
InvalidArgument,
NotFound,
)
from ..file import VoiceMessage
from ..flags import MessageFlags
from ..http import Route
from ..message import Attachment, Message
Expand Down Expand Up @@ -507,13 +508,17 @@ def create_interaction_response(
attachments = []
files = files or []
for index, file in enumerate(files):
attachments.append(
{
"id": index,
"filename": file.filename,
"description": file.description,
}
)
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}]",
Expand All @@ -522,7 +527,7 @@ def create_interaction_response(
"content_type": "application/octet-stream",
}
)
payload["attachments"] = attachments
payload["data"]["attachments"] = attachments
form[0]["value"] = utils._to_json(payload)

route = Route(
Expand Down Expand Up @@ -658,8 +663,10 @@ def handle_message_parameters(
if username:
payload["username"] = username

flags = MessageFlags(suppress_embeds=suppress, ephemeral=ephemeral)
payload["flags"] = flags.value
flags = MessageFlags(
suppress_embeds=suppress,
ephemeral=ephemeral,
)

if applied_tags is not MISSING:
payload["applied_tags"] = applied_tags
Expand All @@ -680,6 +687,7 @@ def handle_message_parameters(
files = [file]

if files:
voice_message = False
for index, file in enumerate(files):
multipart_files.append(
{
Expand All @@ -689,17 +697,26 @@ def handle_message_parameters(
"content_type": "application/octet-stream",
}
)
_attachments.append(
{
"id": index,
"filename": file.filename,
"description": file.description,
}
)
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
Expand Down

0 comments on commit f8a7c3a

Please sign in to comment.