Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Voice Message Sending #2579

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
34 changes: 9 additions & 25 deletions discord/abc.py
Original file line number Diff line number Diff line change
@@ -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
@@ -1567,7 +1567,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]
@@ -1613,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(
@@ -1642,6 +1622,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,
@@ -1656,7 +1640,7 @@ async def send(
message_reference=reference,
stickers=stickers,
components=components,
flags=flags,
flags=flags.value,
poll=poll,
)
finally:
@@ -1675,7 +1659,7 @@ async def send(
message_reference=reference,
stickers=stickers,
components=components,
flags=flags,
flags=flags.value,
poll=poll,
)

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

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


class File:
@@ -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")
@@ -143,3 +147,59 @@ 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
66 changes: 51 additions & 15 deletions discord/http.py
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@
LoginFailure,
NotFound,
)
from .file import VoiceMessage
from .gateway import DiscordClientWebSocketResponse
from .utils import MISSING, warn_deprecated

@@ -410,9 +411,36 @@ 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
):
# 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:
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()
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
@@ -567,13 +595,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}]",
@@ -633,13 +665,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}]",
10 changes: 7 additions & 3 deletions discord/interactions.py
Original file line number Diff line number Diff line change
@@ -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
@@ -915,8 +915,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()
@@ -954,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
51 changes: 35 additions & 16 deletions discord/webhook/async_.py
Original file line number Diff line number Diff line change
@@ -47,6 +47,7 @@
InvalidArgument,
NotFound,
)
from ..file import VoiceMessage
from ..flags import MessageFlags
from ..http import Route
from ..message import Attachment, Message
@@ -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}]",
@@ -522,6 +527,8 @@ def create_interaction_response(
"content_type": "application/octet-stream",
}
)
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)

@@ -658,8 +665,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
@@ -680,6 +689,7 @@ def handle_message_parameters(
files = [file]

if files:
voice_message = False
for index, file in enumerate(files):
multipart_files.append(
{
@@ -689,17 +699,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