Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
56a9a7d
Initial commit
codeofandrin Apr 9, 2023
92f0e4d
Add support for soundboard sound events
codeofandrin Apr 9, 2023
e95280b
Add guild feature SOUNDBOARD
codeofandrin Apr 9, 2023
8706ebf
Remove print from debugging
codeofandrin Apr 11, 2023
9aade03
Add audit log events + docs change in SoundboardSound
codeofandrin Apr 15, 2023
bd5f6d9
Merge branch 'master' into soundboard_vc_effects
codeofandrin Oct 21, 2023
bc6e8ac
Further implementing soundboard sounds
codeofandrin Oct 26, 2023
c1b0d48
Add request_soundboard_sounds, general code and docs fixes
codeofandrin Oct 29, 2023
2c9c2f2
Fix errors, add missing things
codeofandrin Oct 31, 2023
b3d5199
Add get_soundboard_sound and soundboard_sounds property to Client
codeofandrin Nov 2, 2023
0bad90f
Merge branch 'master' into soundboard_vc_effects
codeofandrin Jan 17, 2024
f1f0247
Merge branch 'master' into soundboard_vc_effects
codeofandrin Jan 27, 2024
3ee03bc
Make user optional in VoiceChannelEffect
codeofandrin Jan 27, 2024
95f828c
Coerce sound_volume
codeofandrin Jan 27, 2024
c801b0e
Change representation of VoiceChannelSoundEffect
codeofandrin Jan 27, 2024
b68392d
Make BaseSoundboardSound inherit from AssetMixin
codeofandrin Jan 27, 2024
b788057
Add new helper function _get_mime_type_for_audio
codeofandrin Jan 27, 2024
0f61f5e
Change from Sequence to List for soundboard_sounds
codeofandrin Jan 27, 2024
58fd35e
Merge branch 'master' into soundboard_vc_effects
codeofandrin Aug 6, 2024
3f49bc4
Fix lint
codeofandrin Aug 6, 2024
51ed284
Remove emoji_id from default sound
codeofandrin Aug 6, 2024
a051cde
Partially remove user_id
codeofandrin Aug 7, 2024
6b0c1c1
Change versionadded from 2.4 to 2.5
codeofandrin Aug 7, 2024
eb792aa
Add support for get methods
codeofandrin Aug 7, 2024
2b97985
Remove guild_id attr
codeofandrin Aug 15, 2024
004bbda
Add VoiceChannel.send_sound
codeofandrin Aug 15, 2024
cca1013
[docs] Add notes to fetch_soundboard_sound(s)
codeofandrin Aug 15, 2024
fb3552d
[docs] Update perms and file formats for create sound
codeofandrin Aug 15, 2024
5d70143
Make edit parameters keyword-only
codeofandrin Aug 15, 2024
5136d09
[docs] Update required perms for edit and delete
codeofandrin Aug 15, 2024
d68216d
Update events
codeofandrin Aug 15, 2024
62c8db6
Update intent flags to expressions
codeofandrin Aug 15, 2024
39a1ac3
Remove requesting sounds via gateway
codeofandrin Sep 15, 2024
1a0d2d5
Merge remote-tracking branch 'upstream/master' into soundboard_vc_eff…
codeofandrin Sep 15, 2024
2b60974
Add MORE_SOUNDBOARD guild feature
codeofandrin Sep 17, 2024
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
1 change: 1 addition & 0 deletions discord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from .threads import *
from .automod import *
from .poll import *
from .soundboard import *


class VersionInfo(NamedTuple):
Expand Down
6 changes: 6 additions & 0 deletions discord/audit_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAc
return [AutoModRuleAction.from_data(action) for action in data]


def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji:
return PartialEmoji(name=data)


E = TypeVar('E', bound=enums.Enum)


Expand Down Expand Up @@ -341,6 +345,8 @@ class AuditLogChanges:
'available_tags': (None, _transform_forum_tags),
'flags': (None, _transform_overloaded_flags),
'default_reaction_emoji': (None, _transform_default_reaction),
'emoji_name': ('emoji', _transform_default_emoji),
'user_id': ('user', _transform_member_id)
}
# fmt: on

Expand Down
163 changes: 161 additions & 2 deletions discord/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@
import discord.abc
from .scheduled_event import ScheduledEvent
from .permissions import PermissionOverwrite, Permissions
from .enums import ChannelType, ForumLayoutType, ForumOrderType, PrivacyLevel, try_enum, VideoQualityMode, EntityType
from .enums import (
ChannelType,
ForumLayoutType,
ForumOrderType,
PrivacyLevel,
try_enum,
VideoQualityMode,
EntityType,
VoiceChannelEffectAnimationType,
)
from .mixins import Hashable
from . import utils
from .utils import MISSING
Expand All @@ -58,6 +67,8 @@
from .partial_emoji import _EmojiTag, PartialEmoji
from .flags import ChannelFlags
from .http import handle_message_parameters
from .object import Object
from .soundboard import BaseSoundboardSound, SoundboardDefaultSound

__all__ = (
'TextChannel',
Expand All @@ -69,14 +80,15 @@
'ForumChannel',
'GroupChannel',
'PartialMessageable',
'VoiceChannelEffect',
'VoiceChannelSoundEffect',
)

if TYPE_CHECKING:
from typing_extensions import Self

from .types.threads import ThreadArchiveDuration
from .role import Role
from .object import Object
from .member import Member, VoiceState
from .abc import Snowflake, SnowflakeTime
from .embeds import Embed
Expand All @@ -100,8 +112,11 @@
ForumChannel as ForumChannelPayload,
MediaChannel as MediaChannelPayload,
ForumTag as ForumTagPayload,
VoiceChannelEffect as VoiceChannelEffectPayload,
)
from .types.snowflake import SnowflakeList
from .types.soundboard import BaseSoundboardSound as BaseSoundboardSoundPayload
from .soundboard import SoundboardSound

OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object])

Expand All @@ -111,6 +126,121 @@ class ThreadWithMessage(NamedTuple):
message: Message


class VoiceChannelEffectAnimation(NamedTuple):
id: int
type: VoiceChannelEffectAnimationType


class VoiceChannelSoundEffect(BaseSoundboardSound):
"""Represents a Discord voice channel sound effect.

.. versionadded:: 2.5

.. container:: operations

.. describe:: x == y

Checks if two sound effects are equal.

.. describe:: x != y

Checks if two sound effects are not equal.

.. describe:: hash(x)

Returns the sound effect's hash.

Attributes
------------
id: :class:`int`
The ID of the sound.
volume: :class:`float`
The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%).
"""

__slots__ = ('_state',)

def __init__(self, *, state: ConnectionState, id: int, volume: float):
data: BaseSoundboardSoundPayload = {
'sound_id': id,
'volume': volume,
}
super().__init__(state=state, data=data)

def __repr__(self) -> str:
return f"<{self.__class__.__name__} id={self.id} volume={self.volume}>"

@property
def created_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the snowflake's creation time in UTC.
Returns ``None`` if it's a default sound."""
if self.is_default():
return None
else:
return utils.snowflake_time(self.id)

def is_default(self) -> bool:
""":class:`bool`: Whether it's a default sound or not."""
# if it's smaller than the Discord Epoch it cannot be a snowflake
return self.id < utils.DISCORD_EPOCH


class VoiceChannelEffect:
"""Represents a Discord voice channel effect.

.. versionadded:: 2.5

Attributes
------------
channel: :class:`VoiceChannel`
The channel in which the effect is sent.
user: Optional[:class:`Member`]
The user who sent the effect. ``None`` if not found in cache.
animation: Optional[:class:`VoiceChannelEffectAnimation`]
The animation the effect has. Returns ``None`` if the effect has no animation.
emoji: Optional[:class:`PartialEmoji`]
The emoji of the effect.
sound: Optional[:class:`VoiceChannelSoundEffect`]
The sound of the effect. Returns ``None`` if it's an emoji effect.
"""

__slots__ = ('channel', 'user', 'animation', 'emoji', 'sound')

def __init__(self, *, state: ConnectionState, data: VoiceChannelEffectPayload, guild: Guild):
self.channel: VoiceChannel = guild.get_channel(int(data['channel_id'])) # type: ignore # will always be a VoiceChannel
Comment thread
codeofandrin marked this conversation as resolved.
self.user: Optional[Member] = guild.get_member(int(data['user_id']))
self.animation: Optional[VoiceChannelEffectAnimation] = None

animation_id = data.get('animation_id')
if animation_id is not None:
animation_type = try_enum(VoiceChannelEffectAnimationType, data['animation_type']) # type: ignore # cannot be None here
self.animation = VoiceChannelEffectAnimation(id=animation_id, type=animation_type)

emoji = data.get('emoji')
self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict(emoji) if emoji is not None else None
self.sound: Optional[VoiceChannelSoundEffect] = None

sound_id: Optional[int] = utils._get_as_snowflake(data, 'sound_id')
if sound_id is not None:
sound_volume = data.get('sound_volume') or 0.0
self.sound = VoiceChannelSoundEffect(state=state, id=sound_id, volume=sound_volume)

def __repr__(self) -> str:
attrs = [
('channel', self.channel),
('user', self.user),
('animation', self.animation),
('emoji', self.emoji),
('sound', self.sound),
]
inner = ' '.join('%s=%r' % t for t in attrs)
return f"<{self.__class__.__name__} {inner}>"

def is_sound(self) -> bool:
""":class:`bool`: Whether the effect is a sound or not."""
return self.sound is not None


class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
"""Represents a Discord guild text channel.

Expand Down Expand Up @@ -1456,6 +1586,35 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona
# the payload will always be the proper channel payload
return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore

async def send_sound(self, sound: Union[SoundboardSound, SoundboardDefaultSound], /) -> None:
"""|coro|

Sends a soundboard sound for this channel.

You must have :attr:`~Permissions.speak` and :attr:`~Permissions.use_soundboard` to do this.
Additionally, you must have :attr:`~Permissions.use_external_sounds` if the sound is from
a different guild.

.. versionadded:: 2.5

Parameters
-----------
sound: Union[:class:`SoundboardSound`, :class:`SoundboardDefaultSound`]
The sound to send for this channel.

Raises
-------
Forbidden
You do not have permissions to send a sound for this channel.
HTTPException
Sending the sound failed.
"""
payload = {'sound_id': sound.id}
if not isinstance(sound, SoundboardDefaultSound) and self.guild.id != sound.guild.id:
payload['source_guild_id'] = sound.guild.id

await self._state.http.send_soundboard_sound(self.id, **payload)


class StageChannel(VocalGuildChannel):
"""Represents a Discord guild stage channel.
Expand Down
46 changes: 46 additions & 0 deletions discord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
from .stage_instance import StageInstance
from .threads import Thread
from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
from .soundboard import SoundboardDefaultSound, SoundboardSound

if TYPE_CHECKING:
from types import TracebackType
Expand Down Expand Up @@ -383,6 +384,14 @@ def stickers(self) -> Sequence[GuildSticker]:
"""
return self._connection.stickers

@property
def soundboard_sounds(self) -> List[SoundboardSound]:
"""List[:class:`.SoundboardSound`]: The soundboard sounds that the connected client has.

.. versionadded:: 2.5
"""
return self._connection.soundboard_sounds

@property
def cached_messages(self) -> Sequence[Message]:
"""Sequence[:class:`.Message`]: Read-only list of messages the connected client has cached.
Expand Down Expand Up @@ -1109,6 +1118,23 @@ def get_sticker(self, id: int, /) -> Optional[GuildSticker]:
"""
return self._connection.get_sticker(id)

def get_soundboard_sound(self, id: int, /) -> Optional[SoundboardSound]:
"""Returns a soundboard sound with the given ID.

.. versionadded:: 2.5

Parameters
----------
id: :class:`int`
The ID to search for.

Returns
--------
Optional[:class:`.SoundboardSound`]
The soundboard sound or ``None`` if not found.
"""
return self._connection.get_soundboard_sound(id)

def get_all_channels(self) -> Generator[GuildChannel, None, None]:
"""A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'.

Expand Down Expand Up @@ -2964,6 +2990,26 @@ async def fetch_premium_sticker_pack(self, sticker_pack_id: int, /) -> StickerPa
data = await self.http.get_sticker_pack(sticker_pack_id)
return StickerPack(state=self._connection, data=data)

async def fetch_soundboard_default_sounds(self) -> List[SoundboardDefaultSound]:
"""|coro|

Retrieves all default soundboard sounds.

.. versionadded:: 2.5

Raises
-------
HTTPException
Retrieving the default soundboard sounds failed.

Returns
---------
List[:class:`.SoundboardDefaultSound`]
All default soundboard sounds.
"""
data = await self.http.get_soundboard_default_sounds()
return [SoundboardDefaultSound(state=self._connection, data=sound) for sound in data]

async def create_dm(self, user: Snowflake) -> DMChannel:
"""|coro|

Expand Down
12 changes: 12 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
'EntitlementType',
'EntitlementOwnerType',
'PollLayoutType',
'VoiceChannelEffectAnimationType',
)


Expand Down Expand Up @@ -377,6 +378,9 @@ class AuditLogAction(Enum):
thread_update = 111
thread_delete = 112
app_command_permission_update = 121
soundboard_sound_create = 130
soundboard_sound_update = 131
soundboard_sound_delete = 132
automod_rule_create = 140
automod_rule_update = 141
automod_rule_delete = 142
Expand Down Expand Up @@ -447,6 +451,9 @@ def category(self) -> Optional[AuditLogActionCategory]:
AuditLogAction.automod_timeout_member: None,
AuditLogAction.creator_monetization_request_created: None,
AuditLogAction.creator_monetization_terms_accepted: None,
AuditLogAction.soundboard_sound_create: AuditLogActionCategory.create,
AuditLogAction.soundboard_sound_update: AuditLogActionCategory.update,
AuditLogAction.soundboard_sound_delete: AuditLogActionCategory.delete,
}
# fmt: on
return lookup[self]
Expand Down Expand Up @@ -835,6 +842,11 @@ class ReactionType(Enum):
burst = 1


class VoiceChannelEffectAnimationType(Enum):
premium = 0
basic = 1


def create_unknown_value(cls: Type[E], val: Any) -> E:
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'unknown_{val}'
Expand Down
Loading