diff --git a/discord/__init__.py b/discord/__init__.py index fc7bf3c3..9e08e8d3 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -66,6 +66,7 @@ from .sticker import Sticker, GuildSticker, StickerPack from .scheduled_event import GuildScheduledEvent from .monetization import * +from .soundboard import * MISSING = utils.MISSING diff --git a/discord/application_commands.py b/discord/application_commands.py index e0fbe6a1..7856fff4 100644 --- a/discord/application_commands.py +++ b/discord/application_commands.py @@ -1937,7 +1937,7 @@ def __init__( if 32 < len(name) < 1: raise ValueError('The name of the Message-Command has to be 1-32 characters long, got %s.' % len(name)) super().__init__(3, name=name, name_localizations=name_localizations, - default_member_permissions=default_member_permissions, allow_dm=allow_dm, integration_types=integration_types, + default_member_permissions=default_member_permissions, allow_dm=allow_dm, integration_types=inntegration_types, contexts=contexts, **kwargs ) diff --git a/discord/client.py b/discord/client.py index b1fbcdfa..19a9017c 100644 --- a/discord/client.py +++ b/discord/client.py @@ -81,6 +81,7 @@ from .iterators import GuildIterator, EntitlementIterator from .appinfo import AppInfo from .application_commands import * +from .soundboard import SoundboardSound if TYPE_CHECKING: import datetime @@ -467,12 +468,14 @@ async def _run_event(self, coro: Coro, event_name: str, *args, **kwargs): def _schedule_event(self, coro: Coro, event_name: str, *args, **kwargs) -> _ClientEventTask: wrapped = self._run_event(coro, event_name, *args, **kwargs) + #print(coro, event_name, *args, **kwargs) # Schedules the task return _ClientEventTask(original_coro=coro, event_name=event_name, coro=wrapped, loop=self.loop) def dispatch(self, event: str, *args, **kwargs) -> None: log.debug('Dispatching event %s', event) method = 'on_' + event + #print(method) listeners = self._listeners.get(event) if listeners: @@ -744,6 +747,44 @@ async def request_offline_members(self, *guilds): for guild in guilds: await self._connection.chunk_guild(guild) + async def fetch_soundboard_sounds(self, guild_id): + """|coro| + + Requests all soundboard sounds for the given guilds. + + This method retrieves the list of soundboard sounds from the Discord API for each guild ID provided. + + .. note:: + + You must have the :attr:`~Permissions.manage_guild_expressions` permission + in each guild to retrieve its soundboard sounds. + + Parameters + ---------- + guild_ids: List[:class:`int`] + A list of guild IDs to fetch soundboard sounds from. + + Raises + ------- + HTTPException + Retrieving soundboard sounds failed. + NotFound + One of the provided guilds does not exist or is inaccessible. + Forbidden + Missing permissions to view soundboard sounds in one or more guilds. + + Returns + ------- + Dict[:class:`int`, List[:class:`SoundboardSound`]] + A dictionary mapping each guild ID to a list of its soundboard sounds. + """ + guild = self.get_guild(guild_id) + + data = await self.http.all_soundboard_sounds(guild_id) + data = data["items"] + return SoundboardSound._from_list(guild=guild, state=self._connection, data_list=data) + #await self.ws.request_soundboard_sounds(guild_ids) + # hooks async def _call_before_identify_hook(self, shard_id, *, initial=False): diff --git a/discord/gateway.py b/discord/gateway.py index 9fc18479..caaf23ee 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -268,25 +268,28 @@ class DiscordWebSocket: a connection issue. GUILD_SYNC Send only. Requests a guild sync. + REQUEST_SOUNDBOARD_SOUNDs + Send only. Used to request soundboard sounds for a list of guilds. gateway The gateway we are currently connected to. token The authentication token for discord. """ - DISPATCH = 0 - HEARTBEAT = 1 - IDENTIFY = 2 - PRESENCE = 3 - VOICE_STATE = 4 - VOICE_PING = 5 - RESUME = 6 - RECONNECT = 7 - REQUEST_MEMBERS = 8 - INVALIDATE_SESSION = 9 - HELLO = 10 - HEARTBEAT_ACK = 11 - GUILD_SYNC = 12 + DISPATCH = 0 + HEARTBEAT = 1 + IDENTIFY = 2 + PRESENCE = 3 + VOICE_STATE = 4 + VOICE_PING = 5 + RESUME = 6 + RECONNECT = 7 + REQUEST_MEMBERS = 8 + INVALIDATE_SESSION = 9 + HELLO = 10 + HEARTBEAT_ACK = 11 + GUILD_SYNC = 12 + REQUEST_SOUNDBOARD_SOUNDs = 31 def __init__(self, socket, *, loop): self.socket = socket @@ -718,6 +721,18 @@ async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=Fal log.debug('Updating our voice state to %s.', payload) await self.send_as_json(payload) + async def request_soundboard_sounds(self, guild_ids): + if not isinstance(guild_ids, list): + raise TypeError("guild_ids has to be a list.") + + payload = { + 'op': self.REQUEST_SOUNDBOARD_SOUNDs, + 'd': { + 'guild_ids': guild_ids + } + } + await self.send_as_json(payload) + async def close(self, code=4000): if self._keep_alive: self._keep_alive.stop() diff --git a/discord/guild.py b/discord/guild.py index 23271a79..14e9f624 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -42,6 +42,7 @@ TYPE_CHECKING ) from typing_extensions import Literal +import base64 if TYPE_CHECKING: from os import PathLike @@ -93,6 +94,7 @@ from .flags import SystemChannelFlags from .integrations import _integration_factory, Integration from .sticker import GuildSticker +from .soundboard import SoundboardSound from .automod import AutoModRule, AutoModTriggerMetadata, AutoModAction from .application_commands import SlashCommand, MessageCommand, UserCommand, Localizations @@ -1301,6 +1303,250 @@ def pred(m: Member): return utils.find(pred, members) + async def create_soundboard_sound(self, + name: str, + sound: Union[str, bytes, io.IOBase, Path], + volume: Optional[float] = 1.0, + emoji_id: Optional[int] = None, + emoji_name: Optional[str] = None, + auto_trim: bool = False) -> SoundboardSound: + """|coro| + + Creates a new soundboard sound in the guild. + + Requires the ``CREATE_GUILD_EXPRESSIONS`` permission. + Triggers a Guild Soundboard Sound Create Gateway event. + Supports the ``X-Audit-Log-Reason`` header. + + Soundboard sounds must be a valid MP3 or OGG file, + with a maximum size of 512 KB and a maximum duration of 5.2 seconds. + + Examples + ---------- + + :class:`bytes` Rawdata: :: + + with open("sound.mp3", "rb") as f: + sound_bytes = f.read() + + sound = await guild.create_soundboard_sound(name="sound", sound=sound_bytes) + + :class:`str` Base64 encoded: :: + + b64 = base64.b64encode(b"RIFF...").decode("utf-8") + encoded = await guild.create_soundboard_sound(name="sound", sound=b64) + + or with prefix :: + + b64 = base64.b64encode(b"RIFF...").decode("utf-8") + encoded = await guild.create_soundboard_sound(name="sound", sound=f"data:audio/ogg;base64,{b64}") + + :class:`io.IOBase`: :: + + with open("sound.ogg", "rb") as f: + sound = await guild.create_soundboard_sound(name="sound", sound=f) + + Parameters + ---------- + name: :class:`str` + The name of the soundboard sound (2–32 characters). + sound: Union[:class:`str`, :class:`bytes`, :class:`io.IOBase`, :class:`pathlib.Path`] + The MP3 or OGG sound data. Can be a file path, raw bytes, or file-like object. + volume: Optional[:class:`float`] + The volume level of the sound (0.0 to 1.0). Defaults to 1.0. + emoji_id: Optional[:class:`int`] + The ID of a custom emoji to associate with the sound. + emoji_name: Optional[:class:`str`] + The Unicode character of a standard emoji to associate with the sound. + auto_trim: Optional[:class:`bool`] + Takes a random point from the audio material that is max. 5.2 seconds long. + + Raises + ------ + discord.Forbidden + You don't have permission to create this sound. + discord.HTTPException + The creation failed. + ValueError + One of the fields is invalid or the sound exceeds the size/duration limits. + + Returns + ------- + :class:`SoundboardSound` + The created SoundboardSound object. + """ + + if not (2 <= len(name) <= 32): + raise ValueError("Soundboard name must be between 2 and 32 characters.") + + if auto_trim: + sound_trim = SoundboardSound._auto_trim(sound) + _sound = SoundboardSound._encode_sound(sound_trim) + else: + _sound = SoundboardSound._encode_sound(sound) + + data = await self._state.http.create_soundboard_sound(guild_id=self.id, name=name, sound=_sound, volume=volume, emoji_id=emoji_id, emoji_name=emoji_name) + return SoundboardSound(guild=self, state=self._state, data=data) + + async def update_soundboard_sound(self, + name: str, + sound_id: int, + volume: Optional[float] = 1.0, + emoji_id: Optional[int] = None, + emoji_name: Optional[str] = None) -> SoundboardSound: + """|coro| + + Updates an existing soundboard sound in the guild. + + For sounds created by the current user, requires either the ``CREATE_GUILD_EXPRESSIONS`` + or ``MANAGE_GUILD_EXPRESSIONS`` permission. For other sounds, requires the + ``MANAGE_GUILD_EXPRESSIONS`` permission. + + All parameters are optional except ``sound_id``. + Triggers a Guild Soundboard Sound Update Gateway event. + Supports the ``X-Audit-Log-Reason`` header. + + Parameters + ---------- + name: Optional[:class:`str`] + The new name of the sound (2–32 characters). + sound_id: :class:`int` + The ID of the soundboard sound to update. + volume: Optional[:class:`float`] + The volume level of the sound (0.0 to 1.0). + emoji_id: Optional[:class:`int`] + The ID of the emoji to associate with the sound. + emoji_name: Optional[:class:`str`] + The name of the emoji to associate with the sound. + + Raises + ------ + discord.Forbidden + You don't have permission to modify this sound. + discord.HTTPException + The modification failed. + ValueError + One of the fields is invalid or out of range. + + Returns + ------- + :class:`SoundboardSound` + The updated soundboard sound object. + """ + + if not (2 <= len(name) <= 32): + raise ValueError("Soundboard name must be between 2 and 32 characters.") + + data = await self._state.http.update_soundboard_sound(guild_id=self.id, sound_id =sound_id, name=name, volume=volume, emoji_id=emoji_id, emoji_name=emoji_name) + return SoundboardSound(guild=self, state=self._state, data=data) + + async def delete_soundboard_sound(self, sound_id: int) -> SoundboardSound: + """|coro| + + Deletes a soundboard sound from the guild. + + For sounds created by the current user, requires either the ``CREATE_GUILD_EXPRESSIONS`` + or ``MANAGE_GUILD_EXPRESSIONS`` permission. For other sounds, requires the + ``MANAGE_GUILD_EXPRESSIONS`` permission. + + This action triggers a Guild Soundboard Sound Delete Gateway event. + + This endpoint supports the ``X-Audit-Log-Reason`` header. + + Parameters + ---------- + sound_id: :class:`int` + The ID of the soundboard sound to delete. + + Raises + ------ + discord.Forbidden + You don't have permission to delete this sound. + discord.HTTPException + Deleting the sound failed. + + Returns + ------- + :class:`SoundboardSound` + The deleted sound_id. + """ + + await self._state.http.delete_soundboard_sound(guild_id=self.id, sound_id=sound_id) + + async def get_soundboard_sound(self, sound_id: int) -> SoundboardSound: + """|coro| + + Retrieves a specific soundboard sound by its ID. + + Includes the user field if the bot has the ``CREATE_GUILD_EXPRESSIONS`` or ``MANAGE_GUILD_EXPRESSIONS`` permission. + + Parameters + ---------- + sound_id: :class:`int` + The ID of the sound to retrieve. + + Raises + ------ + discord.Forbidden + You do not have permission to fetch this sound. + discord.NotFound + The sound with the given ID does not exist. + discord.HTTPException + Fetching the sound failed. + + Returns + ------- + :class:`SoundboardSound` + The retrieved SoundboardSound object. + """ + + data = await self._state.http.get_soundboard_sound(guild_id=self.id, sound_id=sound_id) + return SoundboardSound(guild=self, state=self._state, data=data) + + async def all_soundboard_sound(self) -> SoundboardSound: + """|coro| + + Retrieves a list of all soundboard sounds in the guild. + + If the bot has either the ``CREATE_GUILD_EXPRESSIONS`` or ``MANAGE_GUILD_EXPRESSIONS`` permission, + the returned sounds will include user-related metadata. + + Raises + ------ + discord.Forbidden + You do not have permission to fetch the soundboard sounds. + discord.HTTPException + Fetching the soundboard sounds failed. + + Returns + ------- + List[:class:`SoundboardSound`] + A list of soundboard sounds available in the guild. + """ + + data = await self._state.http.all_soundboard_sounds(guild_id=self.id) + data = data["items"] + return SoundboardSound._from_list(guild=self, state=self._state, data_list=data) + + async def default_soundboard_sounds(self) -> SoundboardSound: + """|coro| + + Returns the default global soundboard sounds available to all users. + + Raises + ------ + discord.HTTPException + Fetching the sounds failed. + + Returns + ------- + List[:class:`SoundboardSound`] + A list of default SoundboardSound objects. + """ + + data = await self._state.http.default_soundboard_sounds() + return SoundboardSound._from_list(guild=self, state=self._state, data_list=data) + def _create_channel( self, name: str, diff --git a/discord/http.py b/discord/http.py index ee23847a..957207a9 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1190,6 +1190,39 @@ def follow_webhook(self, channel_id, webhook_channel_id, reason=None): # Guild management + def create_soundboard_sound(self, guild_id, volume=1.0, emoji_id=None, emoji_name=None, *, name, sound): + payload = { + "name": name, + "sound": sound, + "volume": volume, + "emoji_id": emoji_id, + "emoji_name": emoji_name + } + + return self.request(Route("POST", "/guilds/{guild_id}/soundboard-sounds", guild_id=guild_id), json=payload) + + def update_soundboard_sound(self, guild_id, sound_id, emoji_id=None, emoji_name=None, *, name, volume): + payload = { + "name": name, + "volume": volume, + "emoji_id": emoji_id, + "emoji_name": emoji_name + } + + return self.request(Route("PATCH", "/guilds/{guild_id}/soundboard-sounds/{sound_id}", guild_id=guild_id, sound_id=sound_id), json=payload) + + def delete_soundboard_sound(self, guild_id, sound_id): + return self.request(Route("DELETE", "/guilds/{guild_id}/soundboard-sounds/{sound_id}", guild_id=guild_id, sound_id=sound_id)) + + def get_soundboard_sound(self, guild_id, sound_id): + return self.request(Route("GET", "/guilds/{guild_id}/soundboard-sounds/{sound_id}", guild_id=guild_id, sound_id=sound_id)) + + def all_soundboard_sounds(self, guild_id): + return self.request(Route("GET", "/guilds/{guild_id}/soundboard-sounds", guild_id=guild_id)) + + def default_soundboard_sounds(self): + return self.request(Route("GET", "/soundboard-default-sounds")) + def get_guilds(self, limit, before=None, after=None): params = { 'limit': limit diff --git a/discord/soundboard.py b/discord/soundboard.py new file mode 100644 index 00000000..904595a5 --- /dev/null +++ b/discord/soundboard.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +from pydub import AudioSegment +from typing import Optional, Union +import io +import os +import base64 +import mimetypes +from pathlib import Path +import secrets + +from .mixins import Hashable +from .abc import Snowflake +from .utils import get as utils_get, snowflake_time + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from datetime import datetime + from .state import ConnectionState + from .guild import Guild + +__all__ = ( + 'SoundboardSound', +) + +class SoundboardSound(Hashable): + """Represents a Soundboard Sound. + + .. versionadded:: 1.7.5.4 + + .. container:: operations + + .. describe:: str(x) + + Returns the name of the SoundboardSounds. + + .. describe:: x == y + + Checks if the Soundboard Sound is equal to another Soundboard Sound. + + .. describe:: hash(x) + + Enables use in sets or as a dictionary key. + + Attributes + ---------- + name: :class:`str` + The sound's name. + sound_id: :class:`int` + The id of the sound. + volume: :class:`float` + The volume of the sound. + emoji_id: :class:`int` + The id for the sound's emoji. + emoji_name: :class:`str` + The name for the sound's emoji. + guild_id: :class:`int` + The id of the guild which this sound's belongs to. + available: :class:`bool` + Whether this guild sound can be used + user: :class:`User` + The user that uploaded the guild sound + """ + + __slots__ = ('sound_id', 'name', 'volume', 'emoji_id', 'emoji_name', 'guild', 'guild_id', 'available', 'user', '_state') + + if TYPE_CHECKING: + name: str + sound_id: SnowflakeID + volume: float + emoji_id: NotRequired[Optional[SnowflakeID]] + emoji_name: NotRequired[Optional[str]] + guild_id: NotRequired[int] + available: bool + user: NotRequired[User] + + def __init__(self, *, guild: Guild, state: ConnectionState, data): + self.guild = guild + self._state = state + self._from_data(data) + # TODO: add Cache + + def __repr__(self): + return f"" + + def __str__(self): + return self.name + + def __eq__(self, other): + return isinstance(other, SoundboardSound) and self.id == other.id + + def __hash__(self): + return hash((self.name, self.sound_id)) + + @property + def created_at(self) -> datetime: + """:class:`datetime.datetime`: Returns the sound's creation time in UTC as a naive datetime.""" + return snowflake_time(self.sound_id) + + @staticmethod + def _auto_trim(input_path: Union[str, bytes, io.IOBase, Path], max_duration_sec: float = 5, max_size_bytes: int = 512 * 1024) -> bytes: + if isinstance(input_path, str): + if input_path.startswith("data:"): + try: + b64_data = input_path.split(",", 1)[1] + input_path = base64.b64decode(b64_data) + except Exception as e: + raise ValueError(f"Invalid base64 data URI: {e}") + elif os.path.exists(input_path): + input_path = Path(input_path) + else: + try: + input_path = base64.b64decode(input_path) + except Exception: + raise ValueError("Invalid base64 string or path") + + if isinstance(input_path, Path): + audio = AudioSegment.from_file(str(input_path)) + elif isinstance(input_path, bytes): + audio = AudioSegment.from_file(io.BytesIO(input_path)) + elif isinstance(input_path, io.IOBase): + input_path.seek(0) + audio = AudioSegment.from_file(input_path) + else: + raise TypeError("Unsupported input type") + + duration_ms = len(audio) + max_duration_ms = int(max_duration_sec * 1000) + + if duration_ms > max_duration_ms: + start = secrets.randbelow(duration_ms - max_duration_ms + 1) + audio = audio[start:start + max_duration_ms] + else: + audio = audio[:max_duration_ms] + + buffer = io.BytesIO() + audio.export(buffer, format="mp3", parameters=["-t", "5", "-write_xing", "0"]) + return buffer.getvalue() + + @staticmethod + def _encode_sound(sound: Union[str, bytes, io.IOBase, Path]) -> str: + raw: bytes + mime_type: str = "audio/ogg" + sound_duration: float + + if isinstance(sound, bytes): + raw = sound + + elif isinstance(sound, io.IOBase): + raw = sound.read() + + elif isinstance(sound, Path): + file_size = os.path.getsize(sound) + if file_size > 512 * 1024: + raise ValueError("The audio file exceeds the maximum file size of 512 KB") + + with open(sound, 'rb') as f: + raw = f.read() + + try: + audio = AudioSegment.from_file(sound) + sound_duration = audio.duration_seconds + except Exception as e: + raise ValueError(f"Error loading the audio file: {e}") + + if sound_duration > 5.2: + raise ValueError("The audio file exceeds the maximum duration of 5.2 seconds") + + mime_type, _ = mimetypes.guess_type(sound) + if mime_type is None: + mime_type = "audio/ogg" + elif sound.suffix == ".mp3": + mime_type = "audio/mpeg" + + elif isinstance(sound, str): + if sound.startswith("data:"): + return sound + try: + base64.b64decode(sound, validate=True) + return f"data:audio/ogg;base64,{sound}" + except Exception: + raise ValueError("Invalid Base64-String") + + else: + raise ValueError("Invalid sound type") + + encoded = base64.b64encode(raw).decode("utf-8") + return f"data:{mime_type};base64,{encoded}" + + def _from_data(self, data): + self.name = data['name'] + self.sound_id = int(data['sound_id']) + self.volume = float(data['volume']) + + self.emoji_id = int(data['emoji_id']) if data.get('emoji_id') else None + self.emoji_name = data.get('emoji_name') + + self.guild_id = self.guild.id + + self.available = data.get('available', True) + + user = data.get('user') + if user: + self.user = self._state.get_user(int(user['id'])) + else: + self.user = None + + @classmethod + def _from_list(cls, guild, state, data_list): + return [cls(guild=guild, state=state, data=data) for data in data_list] + diff --git a/discord/state.py b/discord/state.py index 60d62b23..b65a01ea 100644 --- a/discord/state.py +++ b/discord/state.py @@ -25,6 +25,7 @@ """ from __future__ import annotations +import traceback import asyncio from collections import deque, OrderedDict import copy @@ -62,6 +63,7 @@ from .automod import AutoModRule, AutoModActionPayload from .interactions import BaseInteraction from .monetization import Entitlement, Subscription +from .soundboard import SoundboardSound if TYPE_CHECKING: from .http import HTTPClient @@ -569,6 +571,8 @@ def parse_subscription_delete(self, data): def parse_message_create(self, data): channel, _ = self._get_guild_channel(data) message = Message(channel=channel, data=data, state=self) + #print("DEBUG: parse_message_create aufgerufen von:") + #traceback.print_stack() self.dispatch('message', message) if self._messages is not None: self._messages.append(message) @@ -1359,6 +1363,52 @@ def parse_guild_integrations_update(self, data): else: log.debug('GUILD_INTEGRATIONS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + # def parse_soundboard_sounds(self, data): + # guild = self._get_guild(int(data['guild_id'])) + # soundboard_sounds = data["soundboard_sounds"] + # + # if guild is not None: + # self.dispatch('soundboard_sounds', soundboard_sounds, guild) + # else: + # log.debug('SOUNDBOARD_SOUNDS referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + + def parse_guild_soundboard_sounds_update(self, data): + guild = self._get_guild(int(data['guild_id'])) + sound = SoundboardSound(guild=guild, data=data, state=self) + + if guild is not None: + self.dispatch('soundboard_sounds_update', sound, guild) + else: + log.debug('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + + def parse_guild_soundboard_sound_create(self, data): + guild = self._get_guild(int(data['guild_id'])) + sound = SoundboardSound(guild=guild, data=data, state=self) + + if guild is not None: + self.dispatch('soundboard_create', sound, guild) + else: + log.debug('GUILD_SOUNDBOARD_SOUND_CREATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + + def parse_guild_soundboard_sound_update(self, data): + guild = self._get_guild(int(data['guild_id'])) + sound = SoundboardSound(guild=guild, data=data, state=self) + + if guild is not None: + self.dispatch('soundboard_update', sound, guild) + else: + log.debug('GUILD_SOUNDBOARD_SOUND_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + + def parse_guild_soundboard_sound_delete(self, data): + print(f"parse_guild_soundboard_sound_delete: {data}") + guild = self._get_guild(int(data['guild_id'])) + #sound = SoundboardSound(guild=guild, data=data, state=self) + + if guild is not None: + self.dispatch('soundboard_delete', data["sound_id"], guild) + else: + log.debug('GUILD_SOUNDBOARD_SOUND_DELETE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + def parse_webhooks_update(self, data): channel = self.get_channel(int(data['channel_id'])) if channel is not None: diff --git a/discord/types/guild.py b/discord/types/guild.py index 4ff147ca..fcec6a3d 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -61,6 +61,7 @@ 'WelcomeScreenChannel', 'ScheduledEventEntityMetadata', 'ScheduledEvent', + 'SoundboardSound' ) DefaultMessageNotificationLevel = Literal[0, 1] @@ -355,4 +356,14 @@ class ScheduledEvent(TypedDict): creator: NotRequired[User] user_count: NotRequired[int] image: NotRequired[Optional[str]] - broadcast_to_directory_channels: NotRequired[bool] \ No newline at end of file + broadcast_to_directory_channels: NotRequired[bool] + +class SoundboardSound(TypedDict): + name: str + sound_id: SnowflakeID + volume: float + emoji_id: NotRequired[Optional[SnowflakeID]] + emoji_name: NotRequired[Optional[str]] + guild_id: NotRequired[int] + available: bool + user: NotRequired[User] diff --git a/discord/types/message.py b/discord/types/message.py index 679e29b1..3d8e7004 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -71,16 +71,16 @@ TextInputStyle = Literal[1, 2] SelectDefaultValueType = Literal['user', 'role', 'channel'] MessageType = Literal[ - 0, # Default - 1, # Recipient Add - 2, # Recipient Remove - 3, # Call - 4, # Channel Name Change - 5, # Channel Icon Change - 6, # Channel Pin - 7, # Guild Member Join - 8, # User Premium Guild Subscription - 9, # User Premium Guild Subscription Tier 1 + 0, # Default + 1, # Recipient Add + 2, # Recipient Remove + 3, # Call + 4, # Channel Name Change + 5, # Channel Icon Change + 6, # Channel Pin + 7, # Guild Member Join + 8, # User Premium Guild Subscription + 9, # User Premium Guild Subscription Tier 1 10, # User Premium Guild Subscription Tier 2 11, # User Premium Guild Subscription Tier 3 12, # Channel Follow Add @@ -101,7 +101,7 @@ 28, # Stage end 29, # Stage speaker change 31, # Stage topic change - 32 # Guild application premium subscription + 32 # Guild application premium subscription ] EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link'] MessageActivityType = Literal[1, 2, 3, 5] diff --git a/docs/api/events.rst b/docs/api/events.rst index fe52c04f..4bb36db3 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -964,7 +964,7 @@ to handle it, which defaults to print a traceback and ignoring the exception. :param interaction: he Interaction-object with all his attributes and methods to respond to the interaction :type interaction: :class:`~discord.ModalSubmitInteraction` -.. function:: on_application_command_permissions_update(guild, command, new_permissions): +.. function:: on_application_command_permissions_update(guild, command, new_permissions) Called when the permissions for an application command are updated. @@ -1021,7 +1021,7 @@ to handle it, which defaults to print a traceback and ignoring the exception. :param stage_instance: The stage instance that was deleted. :type stage_instance: :class:`StageInstance` -.. function:: on_voice_channel_effect_send(channel, payload): +.. function:: on_voice_channel_effect_send(channel, payload) Called when a user uses a voice effect in a :class:`VoiceChannel`. @@ -1029,3 +1029,39 @@ to handle it, which defaults to print a traceback and ignoring the exception. :type channel: :class:`VoiceChannel` :param payload: The payload containing info about the effect. :type payload: :class:`VoiceChannelEffectSendEvent` + +.. function:: on_soundboard_sounds_update(sounds, guild) + + Called when multiple guild :class:`SoundboardSound` are updated. + + :param sounds: A list of sounds being updated. + :type sounds: List[:class:`SoundboardSound`] + :param guild: The guild where the sounds were updated. + :type guild: :class:`Guild` + +.. function:: on_soundboard_create(sound, guild) + + Called when a user creates a :class:`SoundboardSound`. + + :param sound: The sound being created. + :type sound: :class:`SoundboardSound` + :param guild: The guild where the sound was created. + :type guild: :class:`Guild` + +.. function:: on_soundboard_update(sound, guild) + + Called when a user updated a :class:`SoundboardSound`. + + :param sound: The sound being updated. + :type sound: :class:`SoundboardSound` + :param guild: The guild where the sound was updated. + :type guild: :class:`Guild` + +.. function:: on_soundboard_delete(sound_id, guild) + + Called when a user deletes a :class:`SoundboardSound`. + + :param sound_id: The sound_id being deleted. + :type sound: :class:`SoundboardSound` + :param guild: The guild where the sound was deleted. + :type guild: :class:`Guild` diff --git a/docs/api/models.rst b/docs/api/models.rst index 7543bf79..5ce01624 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -514,6 +514,14 @@ Sticker :members: :exclude-members: pack, pack_id, sort_value +SoundboardSound +~~~~~~~~ + +.. attributetable:: SoundboardSound + +.. autoclass:: SoundboardSound() + :members: + VoiceRegionInfo ~~~~~~~~~~~~~~~~ diff --git a/docs/requirements.txt b/docs/requirements.txt index 5c9af9f9..533c9478 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -11,3 +11,4 @@ typing-extensions aiohttp>=3.9.1,<4 colorama color-pprint +pydub diff --git a/requirements.txt b/requirements.txt index 47b03688..7f462f48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ attrs multidict idna color-pprint>=0.0.3 -colorama \ No newline at end of file +colorama +pydub