Skip to content

Add support to send voice messages #10230

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

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4480fea
Add VoiceMessageFile class
Jul 16, 2025
31344ca
Add abc.channel.send_voice_message to allow sending voice messages
Jul 16, 2025
aa5c26f
Start work on sending the voice messages (not working)
Jul 16, 2025
a7270b2
First working version of sending voice messages
Jul 16, 2025
4a40683
Start cleaning up parts of the code
Jul 16, 2025
9a4617e
More cleanup
Jul 16, 2025
f9ca81a
Found a much simpler method to send voice messages
Jul 16, 2025
eb62338
`size` method no longer needed
Jul 17, 2025
27bca43
Remove unncessary code
Jul 17, 2025
d1747b9
Doc fixes and made `duration` a required field
Jul 17, 2025
2a96d13
Remove print statements
blord0 Jul 17, 2025
8332ca3
Move `VoiceMessageFile` into `File`
Jul 17, 2025
5231d51
Remove final reference to `VoiceMessageFile`
Jul 17, 2025
2e6bfd3
Merge branch 'Rapptz:master' into voice-messages
blord0 Jul 18, 2025
60030d8
Add error checking
Jul 18, 2025
1d2ab9c
Fix error checking
Jul 18, 2025
50cb4f6
Merge branch 'Rapptz:master' into voice-messages
blord0 Jul 23, 2025
3dd7f8f
Rename duation to duration
Jul 28, 2025
e5cca7d
Add File.voice attribute
Jul 28, 2025
8f1d548
Change checking for voice messages to use File.voice
Jul 28, 2025
9936b0d
Formatting change
Jul 28, 2025
8bc906e
Add real generation of waveforms for Opus files
Jul 28, 2025
dd2fd33
Formatting
Jul 28, 2025
bb4de89
Calculate correct number of points per sample
Jul 28, 2025
0f3bc42
Change TypeError to ValueError
Jul 28, 2025
8bea5c3
Change waveform data to be input as a list of ints
Jul 29, 2025
394b16e
Fix doc issues
Jul 29, 2025
0f1ded6
Merge branch 'Rapptz:master' into voice-messages
blord0 Jul 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -74,7 +74,7 @@
T = TypeVar('T', bound=VoiceProtocol)

if TYPE_CHECKING:
from typing_extensions import Self

Check warning on line 77 in discord/abc.py

View workflow job for this annotation

GitHub Actions / check 3.x

Import "typing_extensions" could not be resolved from source (reportMissingModuleSource)

from .client import Client
from .user import ClientUser
Expand Down Expand Up @@ -1915,6 +1915,31 @@
# 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
Expand Down
40 changes: 40 additions & 0 deletions discord/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@

import os
import io
import base64

from .utils import MISSING

# fmt: off
__all__ = (
'File',
'VoiceMessageFile',
)
# fmt: on

Expand Down Expand Up @@ -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
75 changes: 72 additions & 3 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -243,7 +243,10 @@
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:
Expand All @@ -269,6 +272,9 @@
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(
Expand Down Expand Up @@ -617,7 +623,9 @@
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
Expand Down Expand Up @@ -868,6 +876,10 @@
*,
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)
Expand Down Expand Up @@ -1058,6 +1070,63 @@
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

Check failure on line 1085 in discord/http.py

View workflow job for this annotation

GitHub Actions / check 3.x

Import "MessageFlags" is not accessed (reportUnusedImport)
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"]

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)

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

def kick(self, user_id: Snowflake, guild_id: Snowflake, reason: Optional[str] = None) -> Response[None]:
Expand Down
Loading