Skip to content

Commit 5ef8a52

Browse files
Paillat-devDasupergrasskakjdDorukyumpre-commit-ci[bot]
authored
feat: ✨ Soundboard (#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>
1 parent 9fd9b3e commit 5ef8a52

21 files changed

+1192
-11
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
@@ -331,6 +331,14 @@ def _from_scheduled_event_image(
331331
animated=False,
332332
)
333333

334+
@classmethod
335+
def _from_soundboard_sound(cls, state, sound_id: int) -> Asset:
336+
return cls(
337+
state,
338+
url=f"{cls.BASE}/soundboard-sounds/{sound_id}",
339+
key=str(sound_id),
340+
)
341+
334342
def __str__(self) -> str:
335343
return self._url
336344

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
@@ -79,6 +82,7 @@
7982
"ForumChannel",
8083
"MediaChannel",
8184
"ForumTag",
85+
"VoiceChannelEffectSendEvent",
8286
)
8387

8488
if TYPE_CHECKING:
@@ -100,6 +104,7 @@
100104
from .types.channel import StageChannel as StageChannelPayload
101105
from .types.channel import TextChannel as TextChannelPayload
102106
from .types.channel import VoiceChannel as VoiceChannelPayload
107+
from .types.channel import VoiceChannelEffectSendEvent as VoiceChannelEffectSend
103108
from .types.snowflake import SnowflakeList
104109
from .types.threads import ThreadArchiveDuration
105110
from .ui.view import View
@@ -2243,6 +2248,25 @@ async def set_status(
22432248
"""
22442249
await self._state.http.set_voice_channel_status(self.id, status, reason=reason)
22452250

2251+
async def send_soundboard_sound(self, sound: PartialSoundboardSound) -> None:
2252+
"""|coro|
2253+
2254+
Sends a soundboard sound to the voice channel.
2255+
2256+
Parameters
2257+
----------
2258+
sound: :class:`PartialSoundboardSound`
2259+
The soundboard sound to send.
2260+
2261+
Raises
2262+
------
2263+
Forbidden
2264+
You do not have proper permissions to send the soundboard sound.
2265+
HTTPException
2266+
Sending the soundboard sound failed.
2267+
"""
2268+
await self._state.http.send_soundboard_sound(self.id, sound)
2269+
22462270

22472271
class StageChannel(discord.abc.Messageable, VocalGuildChannel):
22482272
"""Represents a Discord guild stage channel.
@@ -3491,6 +3515,84 @@ def __repr__(self) -> str:
34913515
return f"<PartialMessageable id={self.id} type={self.type!r}>"
34923516

34933517

3518+
class VoiceChannelEffectAnimation(NamedTuple):
3519+
"""Represents an animation that can be sent to a voice channel.
3520+
3521+
.. versionadded:: 2.7
3522+
"""
3523+
3524+
id: int
3525+
type: VoiceChannelEffectAnimationType
3526+
3527+
3528+
class VoiceChannelSoundEffect(PartialSoundboardSound): ...
3529+
3530+
3531+
class VoiceChannelEffectSendEvent:
3532+
"""Represents the payload for an :func:`on_voice_channel_effect_send`.
3533+
3534+
.. versionadded:: 2.7
3535+
3536+
Attributes
3537+
----------
3538+
animation_type: :class:`int`
3539+
The type of animation that is being sent.
3540+
animation_id: :class:`int`
3541+
The ID of the animation that is being sent.
3542+
sound: Optional[:class:`SoundboardSound`]
3543+
The sound that is being sent, could be ``None`` if the effect is not a sound effect.
3544+
guild: :class:`Guild`
3545+
The guild in which the sound is being sent.
3546+
user: :class:`Member`
3547+
The member that sent the sound.
3548+
channel: :class:`VoiceChannel`
3549+
The voice channel in which the sound is being sent.
3550+
data: :class:`dict`
3551+
The raw data sent by the gateway.
3552+
"""
3553+
3554+
__slots__ = (
3555+
"_state",
3556+
"animation_type",
3557+
"animation_id",
3558+
"sound",
3559+
"guild",
3560+
"user",
3561+
"channel",
3562+
"data",
3563+
"emoji",
3564+
)
3565+
3566+
def __init__(
3567+
self,
3568+
data: VoiceChannelEffectSend,
3569+
state: ConnectionState,
3570+
sound: SoundboardSound | PartialSoundboardSound | None = None,
3571+
) -> None:
3572+
self._state = state
3573+
channel_id = int(data["channel_id"])
3574+
user_id = int(data["user_id"])
3575+
guild_id = int(data["guild_id"])
3576+
self.animation_type: VoiceChannelEffectAnimationType = try_enum(
3577+
VoiceChannelEffectAnimationType, data["animation_type"]
3578+
)
3579+
self.animation_id = int(data["animation_id"])
3580+
self.sound = sound
3581+
self.guild = state._get_guild(guild_id)
3582+
self.user = self.guild.get_member(user_id)
3583+
self.channel = self.guild.get_channel(channel_id)
3584+
self.emoji = (
3585+
PartialEmoji(
3586+
name=data["emoji"]["name"],
3587+
animated=data["emoji"]["animated"],
3588+
id=data["emoji"]["id"],
3589+
)
3590+
if data.get("emoji", None)
3591+
else None
3592+
)
3593+
self.data = data
3594+
3595+
34943596
def _guild_channel_factory(channel_type: int):
34953597
value = try_enum(ChannelType, channel_type)
34963598
if value is ChannelType.text:

discord/client.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from .mentions import AllowedMentions
5454
from .monetization import SKU, Entitlement
5555
from .object import Object
56+
from .soundboard import SoundboardSound
5657
from .stage_instance import StageInstance
5758
from .state import ConnectionState
5859
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
@@ -72,6 +73,7 @@
7273
from .member import Member
7374
from .message import Message
7475
from .poll import Poll
76+
from .soundboard import SoundboardSound
7577
from .ui.item import Item
7678
from .voice_client import VoiceProtocol
7779

@@ -2312,3 +2314,46 @@ async def delete_emoji(self, emoji: Snowflake) -> None:
23122314
)
23132315
if self._connection.cache_app_emojis and self._connection.get_emoji(emoji.id):
23142316
self._connection.remove_emoji(emoji)
2317+
2318+
def get_sound(self, sound_id: int) -> SoundboardSound | None:
2319+
"""Gets a :class:`.Sound` from the bot's sound cache.
2320+
2321+
.. versionadded:: 2.7
2322+
2323+
Parameters
2324+
----------
2325+
sound_id: :class:`int`
2326+
The ID of the sound to get.
2327+
2328+
Returns
2329+
-------
2330+
Optional[:class:`.SoundboardSound`]
2331+
The sound with the given ID.
2332+
"""
2333+
return self._connection._get_sound(sound_id)
2334+
2335+
@property
2336+
def sounds(self) -> list[SoundboardSound]:
2337+
"""A list of all the sounds the bot can see.
2338+
2339+
.. versionadded:: 2.7
2340+
"""
2341+
return self._connection.sounds
2342+
2343+
async def fetch_default_sounds(self) -> list[SoundboardSound]:
2344+
"""|coro|
2345+
2346+
Fetches the bot's default sounds.
2347+
2348+
.. versionadded:: 2.7
2349+
2350+
Returns
2351+
-------
2352+
List[:class:`.SoundboardSound`]
2353+
The bot's default sounds.
2354+
"""
2355+
data = await self._connection.http.get_default_sounds()
2356+
return [
2357+
SoundboardSound(http=self.http, state=self._connection, data=s)
2358+
for s in data
2359+
]

discord/enums.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"PromptType",
7373
"OnboardingMode",
7474
"ReactionType",
75+
"VoiceChannelEffectAnimationType",
7576
"SKUType",
7677
"EntitlementType",
7778
"EntitlementOwnerType",
@@ -1075,6 +1076,16 @@ class PollLayoutType(Enum):
10751076
default = 1
10761077

10771078

1079+
class VoiceChannelEffectAnimationType(Enum):
1080+
"""Voice channel effect animation type.
1081+
1082+
.. versionadded:: 2.7
1083+
"""
1084+
1085+
premium = 0
1086+
basic = 1
1087+
1088+
10781089
class MessageReferenceType(Enum):
10791090
"""The type of the message reference object"""
10801091

discord/gateway.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ class DiscordWebSocket:
291291
HELLO = 10
292292
HEARTBEAT_ACK = 11
293293
GUILD_SYNC = 12
294+
REQUEST_SOUNDBOARD_SOUNDS = 31
294295

295296
def __init__(self, socket, *, loop):
296297
self.socket = socket
@@ -731,6 +732,15 @@ async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=Fal
731732
_log.debug("Updating our voice state to %s.", payload)
732733
await self.send_as_json(payload)
733734

735+
async def request_soundboard_sounds(self, guild_ids):
736+
payload = {
737+
"op": self.REQUEST_SOUNDBOARD_SOUNDS,
738+
"d": {"guild_ids": guild_ids},
739+
}
740+
741+
_log.debug("Requesting soundboard sounds for guilds %s.", guild_ids)
742+
await self.send_as_json(payload)
743+
734744
async def close(self, code=4000):
735745
if self._keep_alive:
736746
self._keep_alive.stop()

0 commit comments

Comments
 (0)