-
-
Notifications
You must be signed in to change notification settings - Fork 106
Add soundboard support. #2575
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
base: master
Are you sure you want to change the base?
Add soundboard support. #2575
Conversation
Added the soundboard sound object, multiple endpoints and events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Adds full soundboard support across the library: model, REST endpoints, gateway events, and shard/bot helpers.
- Introduces SoundboardSound entity and (de)serialization.
- Adds REST endpoints for default/guild sounds, sending, creating, editing, and deleting sounds.
- Wires new gateway events (soundboard sound create/update/delete/sounds update, and voice channel effect send) and a shard/bot request for soundboard sounds.
Reviewed Changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| hikari/soundboard.py | Adds SoundboardSound model. |
| hikari/impl/entity_factory.py | Deserializes soundboard sound payloads into entities. |
| hikari/impl/rest.py | Implements REST methods for soundboard endpoints. |
| hikari/internal/routes.py | Declares REST routes for soundboard. |
| hikari/impl/event_factory.py | Deserializes new gateway events (soundboard + channel effect send). |
| hikari/impl/event_manager.py | Dispatches new soundboard and channel effect events. |
| hikari/events/soundboard_events.py | Defines soundboard event classes. |
| hikari/events/channel_events.py | Adds GuildChannelEffectSendEvent type. |
| hikari/impl/shard.py | Implements OP 31 request for soundboard sounds. |
| hikari/impl/gateway_bot.py | Adds bot helper to request soundboard sounds. |
| hikari/api/* | Extends REST, shard, and event factory interfaces for soundboard. |
| hikari/emojis.py | Adds EmojiAnimationType enum. |
| hikari/intents.py | Updates intents docstring with soundboard events. |
| tests/* | Unit tests for soundboard model, REST, shard, bot, events, and factories. |
| hikari/init.* | Exposes soundboard symbols at package root. |
| changes/2575.feature.md | Change log entry for soundboard support. |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| @typing_extensions.override | ||
| def app(self) -> traits.RESTAware: | ||
| # <<inherited docstring from Event>>. | ||
| return self.app |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The app property is recursively referencing itself, causing infinite recursion at runtime and there is no app field on this event. Return the application from the shard (minimal change), e.g. return self.shard.app. Alternatively, add an app: traits.RESTAware attrs field (as done in other events) and have the event factory pass app=self._app.
| return self.app | |
| return self.shard.app |
| animation_id=snowflakes.Snowflake(payload["animation_id"]) | ||
| if "animation_id" in payload | ||
| else undefined.UNDEFINED, | ||
| sound_id=snowflakes.Snowflake(payload["sound_id"]) if "sound_id" in payload else undefined.UNDEFINED, |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If animation_id or sound_id are present but null, snowflakes.Snowflake(None) will raise; mirror the earlier pattern and guard against None, e.g. use value = payload.get("animation_id"); animation_id = snowflakes.Snowflake(value) if value is not None else None (or undefined.UNDEFINED if missing).
| animation_id=snowflakes.Snowflake(payload["animation_id"]) | |
| if "animation_id" in payload | |
| else undefined.UNDEFINED, | |
| sound_id=snowflakes.Snowflake(payload["sound_id"]) if "sound_id" in payload else undefined.UNDEFINED, | |
| animation_id=( | |
| snowflakes.Snowflake(payload["animation_id"]) | |
| if "animation_id" in payload and payload["animation_id"] is not None | |
| else (None if "animation_id" in payload else undefined.UNDEFINED) | |
| ), | |
| sound_id=( | |
| snowflakes.Snowflake(payload["sound_id"]) | |
| if "sound_id" in payload and payload["sound_id"] is not None | |
| else (None if "sound_id" in payload else undefined.UNDEFINED) | |
| ), |
| result = await self._request(routes.GET_GUILD_SOUNDBOARD_SOUND.compile(guild=guild, sound=sound)) | ||
| assert isinstance(result, dict) | ||
| return self._entity_factory.deserialize_soundboard_sound(result) | ||
|
|
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation never returns None, but the signature advertises Optional and API docs describe returning None when not found. Either update the return type (and API docs) to soundboard.SoundboardSound, or handle NotFoundError (and possibly 204) to return None as documented.
| result = await self._request(routes.GET_GUILD_SOUNDBOARD_SOUND.compile(guild=guild, sound=sound)) | |
| assert isinstance(result, dict) | |
| return self._entity_factory.deserialize_soundboard_sound(result) | |
| try: | |
| result = await self._request(routes.GET_GUILD_SOUNDBOARD_SOUND.compile(guild=guild, sound=sound)) | |
| except errors.NotFoundError: | |
| return None | |
| assert isinstance(result, dict) | |
| return self._entity_factory.deserialize_soundboard_sound(result) |
| @abc.abstractmethod | ||
| async def fetch_guild_soundboard_sound( | ||
| self, | ||
| guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], | ||
| sound: snowflakes.SnowflakeishOr[soundboard.SoundboardSound], | ||
| /, | ||
| ) -> soundboard.SoundboardSound | None: |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API contract states Optional[SoundboardSound] and the docstring promises None when not found, but the concrete implementation does not return None; align the contract and behavior (either drop None here and in docs, or specify/implement the NotFound-to-None behavior).
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| """Application and entities that are used to describe stage instances on Discord.""" |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Module docstring references "stage instances"; update to "soundboard sounds" to reflect this module's purpose.
| """Application and entities that are used to describe stage instances on Discord.""" | |
| """Application and entities that are used to describe soundboard sounds on Discord.""" |
| @attrs_extensions.with_copy | ||
| @attrs.define(kw_only=True, weakref_slot=False) | ||
| class SoundboardSoundsUpdateEvent(SoundboardSoundEvent): | ||
| """Event fired when a guild soundboard sound is created.""" |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class docstring is inaccurate for an update event; change to "Event fired when a guild's soundboard sounds are updated."
| """Event fired when a guild soundboard sound is created.""" | |
| """Event fired when a guild's soundboard sounds are updated.""" |
| guild_id: snowflakes.Snowflake = attrs.field(hash=True, repr=False) | ||
| """The guild ID of the stage instance.""" |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace "stage instance" with "soundboard sound" or "event" context; e.g., "The guild ID this sound belongs to."
| guild_id: snowflakes.Snowflake = attrs.field(hash=True, repr=False) | ||
| """The guild ID of the stage instance.""" |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the docstring to reference the soundboard sound instead of a stage instance.
| """The guilds soundboard sounds.""" | ||
|
|
||
| guild_id: snowflakes.Snowflake = attrs.field(hash=True, repr=True) | ||
| """The guild ID of the stage instance.""" |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update to "The guild ID these sounds belong to." to match the event context.
| """The guild ID of the stage instance.""" | |
| """The guild ID these sounds belong to.""" |
| @attrs_extensions.with_copy | ||
| @attrs.define(kw_only=True, weakref_slot=False) | ||
| class GuildChannelEffectSendEvent(GuildChannelEvent): | ||
| """Event fired when a guild channel is created.""" |
Copilot
AI
Oct 17, 2025
•
edited by davfsa
Loading
edited by davfsa
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class docstring is misleading; this event represents a "voice channel effect send" (emoji/sound in a voice channel). Update the description accordingly.
| """Event fired when a guild channel is created.""" | |
| """Event fired when a voice channel effect (emoji or sound) is sent in the connected voice channel.""" |
| emoji_id = payload.get("emoji_id") | ||
| if emoji_id: | ||
| emoji_id = snowflakes.Snowflake(emoji_id) | ||
| emoji_name = payload.get("emoji_name") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| emoji_id = payload.get("emoji_id") | |
| if emoji_id: | |
| emoji_id = snowflakes.Snowflake(emoji_id) | |
| emoji_name = payload.get("emoji_name") | |
| emoji_id = payload["emoji_id"] | |
| if emoji_id: | |
| emoji_id = snowflakes.Snowflake(emoji_id) | |
| emoji_name = payload["emoji_name"] |
| emoji: emoji_models.CustomEmoji | emoji_models.UnicodeEmoji | None = None | ||
| emoji_id = payload.get("emoji_id") | ||
| if emoji_id: | ||
| emoji_id = snowflakes.Snowflake(emoji_id) | ||
| emoji_name = payload.get("emoji_name") | ||
| if emoji_id and emoji_name: | ||
| emoji = emoji_models.CustomEmoji( | ||
| id=emoji_id, | ||
| name=emoji_name, | ||
| is_animated=False, # FIXME: I am unsure if this is correct, but I have no clue on how I could validate this. # noqa: TD001, E501 | ||
| ) | ||
| if emoji_id and not emoji_name: | ||
| emoji = emoji_models.CustomEmoji( | ||
| id=emoji_id, | ||
| name="", # FIXME: I am unsure if this is correct, but I have no clue on how I could validate this. # noqa: TD001, E501 | ||
| is_animated=False, # FIXME: I am unsure if this is correct, but I have no clue on how I could validate this. # noqa: TD001, E501 | ||
| ) | ||
| if not emoji_id and emoji_name: | ||
| emoji = emoji_models.UnicodeEmoji(emoji_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given we are not given animated, then lets always set it as False (Resolves the FIXMEs on it)
There will never be the case of emoji_id and not emoji_name, so that branch can be removed
Summary
Added the soundboard sound object and all events/endpoints.
Checklist
noxand all the pipelines have passed.