Skip to content

Commit a1e8949

Browse files
committed
feat: ✨ Soundboard (Pycord-Development#2623)
Signed-off-by: Paillat <[email protected]> Signed-off-by: Dorukyum <[email protected]> Co-authored-by: Dasupergrasskakjd <[email protected]> Co-authored-by: Dorukyum <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> (cherry picked from commit 5ef8a52) Signed-off-by: Paillat-dev <[email protected]>
1 parent b0e0840 commit a1e8949

19 files changed

+1191
-12
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ These changes are available on the `master` branch, but have not yet been releas
4747
([#2659](https://github.com/Pycord-Development/pycord/pull/2659))
4848
- Added `VoiceMessage` subclass of `File` to allow voice messages to be sent.
4949
([#2579](https://github.com/Pycord-Development/pycord/pull/2579))
50+
- Added the following soundboard-related features:
51+
- Manage guild soundboard sounds with `Guild.fetch_sounds()`, `Guild.create_sound()`,
52+
`SoundboardSound.edit()`, and `SoundboardSound.delete()`.
53+
- Access Discord default sounds with `Client.fetch_default_sounds()`.
54+
- Play sounds in voice channels with `VoiceChannel.send_soundboard_sound()`.
55+
- New `on_voice_channel_effect_send` event for sound and emoji effects.
56+
- Soundboard limits based on guild premium tier (8-48 slots) in
57+
`Guild.soundboard_limit`.
58+
([#2623](https://github.com/Pycord-Development/pycord/pull/2623))
5059
- Added new `Subscription` object and related methods/events.
5160
([#2564](https://github.com/Pycord-Development/pycord/pull/2564))
5261
- Added `Message.forward_to`, `Message.snapshots`, and other related attributes.

discord/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from .role import *
6666
from .scheduled_events import *
6767
from .shard import *
68+
from .soundboard import *
6869
from .stage_instance import *
6970
from .sticker import *
7071
from .team import *

discord/asset.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ def _from_scheduled_event_image(cls, state, event_id: int, cover_hash: str) -> A
328328
animated=False,
329329
)
330330

331+
@classmethod
332+
def _from_soundboard_sound(cls, state, sound_id: int) -> Asset:
333+
return cls(
334+
state,
335+
url=f"{cls.BASE}/soundboard-sounds/{sound_id}",
336+
key=str(sound_id),
337+
)
338+
331339
def __str__(self) -> str:
332340
return self._url
333341

discord/channel.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
Callable,
3333
Iterable,
3434
Mapping,
35+
NamedTuple,
3536
Sequence,
3637
TypeVar,
3738
overload,
@@ -52,6 +53,7 @@
5253
from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum
5354
from .enums import (
5455
VideoQualityMode,
56+
VoiceChannelEffectAnimationType,
5557
VoiceRegion,
5658
try_enum,
5759
)
@@ -64,6 +66,7 @@
6466
from .object import Object
6567
from .partial_emoji import PartialEmoji, _EmojiTag
6668
from .permissions import PermissionOverwrite, Permissions
69+
from .soundboard import PartialSoundboardSound, SoundboardSound
6770
from .stage_instance import StageInstance
6871
from .threads import Thread
6972
from .utils import MISSING
@@ -80,6 +83,7 @@
8083
"ForumChannel",
8184
"MediaChannel",
8285
"ForumTag",
86+
"VoiceChannelEffectSendEvent",
8387
)
8488

8589
if TYPE_CHECKING:
@@ -101,6 +105,7 @@
101105
from .types.channel import StageChannel as StageChannelPayload
102106
from .types.channel import TextChannel as TextChannelPayload
103107
from .types.channel import VoiceChannel as VoiceChannelPayload
108+
from .types.channel import VoiceChannelEffectSendEvent as VoiceChannelEffectSend
104109
from .types.snowflake import SnowflakeList
105110
from .types.threads import ThreadArchiveDuration
106111
from .ui.view import View
@@ -2162,6 +2167,25 @@ async def set_status(self, status: str | None, *, reason: str | None = None) ->
21622167
"""
21632168
await self._state.http.set_voice_channel_status(self.id, status, reason=reason)
21642169

2170+
async def send_soundboard_sound(self, sound: PartialSoundboardSound) -> None:
2171+
"""|coro|
2172+
2173+
Sends a soundboard sound to the voice channel.
2174+
2175+
Parameters
2176+
----------
2177+
sound: :class:`PartialSoundboardSound`
2178+
The soundboard sound to send.
2179+
2180+
Raises
2181+
------
2182+
Forbidden
2183+
You do not have proper permissions to send the soundboard sound.
2184+
HTTPException
2185+
Sending the soundboard sound failed.
2186+
"""
2187+
await self._state.http.send_soundboard_sound(self.id, sound)
2188+
21652189

21662190
class StageChannel(discord.abc.Messageable, VocalGuildChannel):
21672191
"""Represents a Discord guild stage channel.
@@ -3357,6 +3381,84 @@ def __repr__(self) -> str:
33573381
return f"<PartialMessageable id={self.id} type={self.type!r}>"
33583382

33593383

3384+
class VoiceChannelEffectAnimation(NamedTuple):
3385+
"""Represents an animation that can be sent to a voice channel.
3386+
3387+
.. versionadded:: 2.7
3388+
"""
3389+
3390+
id: int
3391+
type: VoiceChannelEffectAnimationType
3392+
3393+
3394+
class VoiceChannelSoundEffect(PartialSoundboardSound): ...
3395+
3396+
3397+
class VoiceChannelEffectSendEvent:
3398+
"""Represents the payload for an :func:`on_voice_channel_effect_send`.
3399+
3400+
.. versionadded:: 2.7
3401+
3402+
Attributes
3403+
----------
3404+
animation_type: :class:`int`
3405+
The type of animation that is being sent.
3406+
animation_id: :class:`int`
3407+
The ID of the animation that is being sent.
3408+
sound: Optional[:class:`SoundboardSound`]
3409+
The sound that is being sent, could be ``None`` if the effect is not a sound effect.
3410+
guild: :class:`Guild`
3411+
The guild in which the sound is being sent.
3412+
user: :class:`Member`
3413+
The member that sent the sound.
3414+
channel: :class:`VoiceChannel`
3415+
The voice channel in which the sound is being sent.
3416+
data: :class:`dict`
3417+
The raw data sent by the gateway.
3418+
"""
3419+
3420+
__slots__ = (
3421+
"_state",
3422+
"animation_type",
3423+
"animation_id",
3424+
"sound",
3425+
"guild",
3426+
"user",
3427+
"channel",
3428+
"data",
3429+
"emoji",
3430+
)
3431+
3432+
def __init__(
3433+
self,
3434+
data: VoiceChannelEffectSend,
3435+
state: ConnectionState,
3436+
sound: SoundboardSound | PartialSoundboardSound | None = None,
3437+
) -> None:
3438+
self._state = state
3439+
channel_id = int(data["channel_id"])
3440+
user_id = int(data["user_id"])
3441+
guild_id = int(data["guild_id"])
3442+
self.animation_type: VoiceChannelEffectAnimationType = try_enum(
3443+
VoiceChannelEffectAnimationType, data["animation_type"]
3444+
)
3445+
self.animation_id = int(data["animation_id"])
3446+
self.sound = sound
3447+
self.guild = state._get_guild(guild_id)
3448+
self.user = self.guild.get_member(user_id)
3449+
self.channel = self.guild.get_channel(channel_id)
3450+
self.emoji = (
3451+
PartialEmoji(
3452+
name=data["emoji"]["name"],
3453+
animated=data["emoji"]["animated"],
3454+
id=data["emoji"]["id"],
3455+
)
3456+
if data.get("emoji", None)
3457+
else None
3458+
)
3459+
self.data = data
3460+
3461+
33603462
def _guild_channel_factory(channel_type: int):
33613463
value = try_enum(ChannelType, channel_type)
33623464
if value is ChannelType.text:

discord/client.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from .mentions import AllowedMentions
5656
from .monetization import SKU, Entitlement
5757
from .object import Object
58+
from .soundboard import SoundboardSound
5859
from .stage_instance import StageInstance
5960
from .state import ConnectionState
6061
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
@@ -80,6 +81,7 @@
8081
from .member import Member
8182
from .message import Message
8283
from .poll import Poll
84+
from .soundboard import SoundboardSound
8385
from .ui.item import Item
8486
from .voice_client import VoiceProtocol
8587

@@ -2268,3 +2270,46 @@ async def delete_emoji(self, emoji: Snowflake) -> None:
22682270
await self._connection.http.delete_application_emoji(self.application_id, emoji.id)
22692271
if self._connection.cache_app_emojis and self._connection.get_emoji(emoji.id):
22702272
self._connection.remove_emoji(emoji)
2273+
2274+
def get_sound(self, sound_id: int) -> SoundboardSound | None:
2275+
"""Gets a :class:`.Sound` from the bot's sound cache.
2276+
2277+
.. versionadded:: 2.7
2278+
2279+
Parameters
2280+
----------
2281+
sound_id: :class:`int`
2282+
The ID of the sound to get.
2283+
2284+
Returns
2285+
-------
2286+
Optional[:class:`.SoundboardSound`]
2287+
The sound with the given ID.
2288+
"""
2289+
return self._connection._get_sound(sound_id)
2290+
2291+
@property
2292+
def sounds(self) -> list[SoundboardSound]:
2293+
"""A list of all the sounds the bot can see.
2294+
2295+
.. versionadded:: 2.7
2296+
"""
2297+
return self._connection.sounds
2298+
2299+
async def fetch_default_sounds(self) -> list[SoundboardSound]:
2300+
"""|coro|
2301+
2302+
Fetches the bot's default sounds.
2303+
2304+
.. versionadded:: 2.7
2305+
2306+
Returns
2307+
-------
2308+
List[:class:`.SoundboardSound`]
2309+
The bot's default sounds.
2310+
"""
2311+
data = await self._connection.http.get_default_sounds()
2312+
return [
2313+
SoundboardSound(http=self.http, state=self._connection, data=s)
2314+
for s in data
2315+
]

discord/enums.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"PromptType",
7575
"OnboardingMode",
7676
"ReactionType",
77+
"VoiceChannelEffectAnimationType",
7778
"SKUType",
7879
"EntitlementType",
7980
"EntitlementOwnerType",
@@ -996,6 +997,16 @@ class PollLayoutType(Enum):
996997
default = 1
997998

998999

1000+
class VoiceChannelEffectAnimationType(Enum):
1001+
"""Voice channel effect animation type.
1002+
1003+
.. versionadded:: 2.7
1004+
"""
1005+
1006+
premium = 0
1007+
basic = 1
1008+
1009+
9991010
class MessageReferenceType(Enum):
10001011
"""The type of the message reference object"""
10011012

discord/gateway.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ class DiscordWebSocket:
284284
HELLO = 10
285285
HEARTBEAT_ACK = 11
286286
GUILD_SYNC = 12
287+
REQUEST_SOUNDBOARD_SOUNDS = 31
287288

288289
def __init__(self, socket, *, loop):
289290
self.socket = socket
@@ -714,6 +715,15 @@ async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=Fal
714715
_log.debug("Updating our voice state to %s.", payload)
715716
await self.send_as_json(payload)
716717

718+
async def request_soundboard_sounds(self, guild_ids):
719+
payload = {
720+
"op": self.REQUEST_SOUNDBOARD_SOUNDS,
721+
"d": {"guild_ids": guild_ids},
722+
}
723+
724+
_log.debug("Requesting soundboard sounds for guilds %s.", guild_ids)
725+
await self.send_as_json(payload)
726+
717727
async def close(self, code=4000):
718728
if self._keep_alive:
719729
self._keep_alive.stop()

0 commit comments

Comments
 (0)