From dfa7ff4045c7f13859636f34014f688127380b3a Mon Sep 17 00:00:00 2001 From: Fleny Date: Tue, 5 Nov 2024 15:14:37 +0100 Subject: [PATCH] feat(bot,rest,gateway,utils,types)!: Add soundboard support (#3919) * Add soundboard support * Add rest endpoints * add comment to gateway requestSoundboardSounds * Apply suggestions from code review Co-authored-by: Awesome Stickz * Update for discord/discord-api-docs#7207 * Update discord.ts --------- Co-authored-by: Awesome Stickz Co-authored-by: ITOH --- packages/bot/src/bot.ts | 6 ++ packages/bot/src/handlers.ts | 5 ++ packages/bot/src/handlers/index.ts | 1 + .../GUILD_SOUNDBOARD_SOUNDS_UPDATE.ts | 13 +++ .../GUILD_SOUNDBOARD_SOUND_CREATE.ts | 10 +++ .../GUILD_SOUNDBOARD_SOUND_DELETE.ts | 13 +++ .../GUILD_SOUNDBOARD_SOUND_UPDATE.ts | 10 +++ .../handlers/soundboard/SOUNDBOARD_SOUNDS.ts | 13 +++ packages/bot/src/handlers/soundboard/index.ts | 5 ++ packages/bot/src/helpers.ts | 41 +++++++++ packages/bot/src/transformers.ts | 30 ++++++- packages/bot/src/transformers/index.ts | 2 + .../bot/src/transformers/soundboardSound.ts | 18 ++++ packages/bot/src/transformers/subscription.ts | 3 +- .../bot/src/transformers/toggles/guild.ts | 19 ++++- packages/bot/src/transformers/types.ts | 20 +++++ packages/bot/src/typings.ts | 5 ++ packages/gateway/src/manager.ts | 49 +++++++++++ packages/rest/src/manager.ts | 39 +++++++++ packages/rest/src/routes.ts | 15 ++++ packages/rest/src/types.ts | 84 +++++++++++++++++++ packages/rest/src/typings/routes.ts | 11 +++ packages/types/src/camel.ts | 2 + packages/types/src/discord.ts | 51 +++++++++++ packages/types/src/discordeno.ts | 34 ++++++++ packages/types/src/shared.ts | 23 ++++- packages/utils/src/urls.ts | 4 + 27 files changed, 522 insertions(+), 4 deletions(-) create mode 100644 packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUNDS_UPDATE.ts create mode 100644 packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_CREATE.ts create mode 100644 packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_DELETE.ts create mode 100644 packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_UPDATE.ts create mode 100644 packages/bot/src/handlers/soundboard/SOUNDBOARD_SOUNDS.ts create mode 100644 packages/bot/src/handlers/soundboard/index.ts create mode 100644 packages/bot/src/transformers/soundboardSound.ts diff --git a/packages/bot/src/bot.ts b/packages/bot/src/bot.ts index 31cb2749a..5b375d6a1 100644 --- a/packages/bot/src/bot.ts +++ b/packages/bot/src/bot.ts @@ -31,6 +31,7 @@ import type { PresenceUpdate, Role, ScheduledEvent, + SoundboardSound, Sticker, Subscription, ThreadMember, @@ -288,4 +289,9 @@ export interface EventHandlers { subscriptionDelete: (subscription: Subscription) => unknown messagePollVoteAdd: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown messagePollVoteRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown + soundboardSoundCreate: (payload: SoundboardSound) => unknown + soundboardSoundUpdate: (payload: SoundboardSound) => unknown + soundboardSoundDelete: (payload: { soundId: bigint; guildId: bigint }) => unknown + soundboardSoundsUpdate: (payload: { soundboardSounds: SoundboardSound[]; guildId: bigint }) => unknown + soundboardSounds: (payload: { soundboardSounds: SoundboardSound[]; guildId: bigint }) => unknown } diff --git a/packages/bot/src/handlers.ts b/packages/bot/src/handlers.ts index 057cbfa83..27b980861 100644 --- a/packages/bot/src/handlers.ts +++ b/packages/bot/src/handlers.ts @@ -75,6 +75,11 @@ export function createBotGatewayHandlers( SUBSCRIPTION_CREATE: options.SUBSCRIPTION_CREATE ?? handlers.handleSubscriptionCreate, SUBSCRIPTION_UPDATE: options.SUBSCRIPTION_UPDATE ?? handlers.handleSubscriptionUpdate, SUBSCRIPTION_DELETE: options.SUBSCRIPTION_DELETE ?? handlers.handleSubscriptionDelete, + GUILD_SOUNDBOARD_SOUND_CREATE: options.GUILD_SOUNDBOARD_SOUND_CREATE ?? handlers.handleGuildSoundboardSoundCreate, + GUILD_SOUNDBOARD_SOUND_DELETE: options.GUILD_SOUNDBOARD_SOUND_DELETE ?? handlers.handleGuildSoundboardSoundDelete, + GUILD_SOUNDBOARD_SOUND_UPDATE: options.GUILD_SOUNDBOARD_SOUND_UPDATE ?? handlers.handleGuildSoundboardSoundUpdate, + GUILD_SOUNDBOARD_SOUNDS_UPDATE: options.GUILD_SOUNDBOARD_SOUNDS_UPDATE ?? handlers.handleGuildSoundboardSoundsUpdate, + SOUNDBOARD_SOUNDS: options.SOUNDBOARD_SOUNDS ?? handlers.handleSoundboardSounds, } } diff --git a/packages/bot/src/handlers/index.ts b/packages/bot/src/handlers/index.ts index 9f95d83df..c1d11272e 100644 --- a/packages/bot/src/handlers/index.ts +++ b/packages/bot/src/handlers/index.ts @@ -12,4 +12,5 @@ export * from './poll/index.js' export * from './roles/index.js' export * from './voice/index.js' export * from './webhooks/index.js' +export * from './soundboard/index.js' export * from './subscriptions/index.js' diff --git a/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUNDS_UPDATE.ts b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUNDS_UPDATE.ts new file mode 100644 index 000000000..681fa2bf7 --- /dev/null +++ b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUNDS_UPDATE.ts @@ -0,0 +1,13 @@ +import type { DiscordGatewayPayload, DiscordSoundboardSoundsUpdate } from '@discordeno/types' +import type { Bot } from '../../index.js' + +export async function handleGuildSoundboardSoundsUpdate(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.soundboardSoundsUpdate) return + + const payload = data.d as DiscordSoundboardSoundsUpdate + + bot.events.soundboardSoundsUpdate({ + guildId: bot.transformers.snowflake(payload.guild_id), + soundboardSounds: payload.soundboard_sounds.map((sound) => bot.transformers.soundboardSound(bot, sound)), + }) +} diff --git a/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_CREATE.ts b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_CREATE.ts new file mode 100644 index 000000000..f458b4e4d --- /dev/null +++ b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_CREATE.ts @@ -0,0 +1,10 @@ +import type { DiscordGatewayPayload, DiscordSoundboardSound } from '@discordeno/types' +import type { Bot } from '../../index.js' + +export async function handleGuildSoundboardSoundCreate(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.soundboardSoundCreate) return + + const payload = data.d as DiscordSoundboardSound + + bot.events.soundboardSoundCreate(bot.transformers.soundboardSound(bot, payload)) +} diff --git a/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_DELETE.ts b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_DELETE.ts new file mode 100644 index 000000000..75f07c5ff --- /dev/null +++ b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_DELETE.ts @@ -0,0 +1,13 @@ +import type { DiscordGatewayPayload, DiscordSoundboardSoundDelete } from '@discordeno/types' +import type { Bot } from '../../index.js' + +export async function handleGuildSoundboardSoundDelete(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.soundboardSoundDelete) return + + const payload = data.d as DiscordSoundboardSoundDelete + + bot.events.soundboardSoundDelete({ + guildId: bot.transformers.snowflake(payload.guild_id), + soundId: bot.transformers.snowflake(payload.sound_id), + }) +} diff --git a/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_UPDATE.ts b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_UPDATE.ts new file mode 100644 index 000000000..4fb26f5ac --- /dev/null +++ b/packages/bot/src/handlers/soundboard/GUILD_SOUNDBOARD_SOUND_UPDATE.ts @@ -0,0 +1,10 @@ +import type { DiscordGatewayPayload, DiscordSoundboardSound } from '@discordeno/types' +import type { Bot } from '../../index.js' + +export async function handleGuildSoundboardSoundUpdate(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.soundboardSoundUpdate) return + + const payload = data.d as DiscordSoundboardSound + + bot.events.soundboardSoundUpdate(bot.transformers.soundboardSound(bot, payload)) +} diff --git a/packages/bot/src/handlers/soundboard/SOUNDBOARD_SOUNDS.ts b/packages/bot/src/handlers/soundboard/SOUNDBOARD_SOUNDS.ts new file mode 100644 index 000000000..befa70885 --- /dev/null +++ b/packages/bot/src/handlers/soundboard/SOUNDBOARD_SOUNDS.ts @@ -0,0 +1,13 @@ +import type { DiscordGatewayPayload, DiscordSoundboardSounds } from '@discordeno/types' +import type { Bot } from '../../index.js' + +export async function handleSoundboardSounds(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.soundboardSounds) return + + const payload = data.d as DiscordSoundboardSounds + + bot.events.soundboardSounds({ + guildId: bot.transformers.snowflake(payload.guild_id), + soundboardSounds: payload.soundboard_sounds.map((sound) => bot.transformers.soundboardSound(bot, sound)), + }) +} diff --git a/packages/bot/src/handlers/soundboard/index.ts b/packages/bot/src/handlers/soundboard/index.ts new file mode 100644 index 000000000..79f8a1948 --- /dev/null +++ b/packages/bot/src/handlers/soundboard/index.ts @@ -0,0 +1,5 @@ +export * from './GUILD_SOUNDBOARD_SOUNDS_UPDATE.js' +export * from './GUILD_SOUNDBOARD_SOUND_DELETE.js' +export * from './GUILD_SOUNDBOARD_SOUND_UPDATE.js' +export * from './GUILD_SOUNDBOARD_SOUND_CREATE.js' +export * from './SOUNDBOARD_SOUNDS.js' diff --git a/packages/bot/src/helpers.ts b/packages/bot/src/helpers.ts index c783c16f9..6cd496649 100644 --- a/packages/bot/src/helpers.ts +++ b/packages/bot/src/helpers.ts @@ -41,6 +41,7 @@ import type { CreateGuildEmoji, CreateGuildFromTemplate, CreateGuildRole, + CreateGuildSoundboardSound, CreateGuildStickerOptions, CreateMessageOptions, CreateScheduledEvent, @@ -89,10 +90,12 @@ import type { ModifyGuildChannelPositions, ModifyGuildEmoji, ModifyGuildMember, + ModifyGuildSoundboardSound, ModifyGuildTemplate, ModifyRolePositions, ModifyWebhook, SearchMembers, + SendSoundboardSound, StartThreadWithMessage, StartThreadWithoutMessage, UpsertGlobalApplicationCommandOptions, @@ -120,6 +123,7 @@ import type { Role, ScheduledEvent, Sku, + SoundboardSound, StageInstance, Sticker, StickerPack, @@ -770,6 +774,31 @@ export function createBotHelpers(bot: Bot): BotHelpers { listSubscriptions: async (skuId, options) => { return await bot.rest.listSubscriptions(skuId, options) }, + sendSoundboardSound: async (channelId, options) => { + await bot.rest.sendSoundboardSound(channelId, options) + }, + listDefaultSoundboardSounds: async () => { + return (await bot.rest.listDefaultSoundboardSounds()).map((sound) => bot.transformers.soundboardSound(bot, snakelize(sound))) + }, + listGuildSoundboardSounds: async (guildId) => { + const res = await bot.rest.listGuildSoundboardSounds(guildId) + + return { + items: res.items.map((sound) => bot.transformers.soundboardSound(bot, snakelize(sound))), + } + }, + getGuildSoundboardSound: async (guildId, soundId) => { + return bot.transformers.soundboardSound(bot, snakelize(await bot.rest.getGuildSoundboardSound(guildId, soundId))) + }, + createGuildSoundboardSound: async (guildId, options, reason) => { + return bot.transformers.soundboardSound(bot, snakelize(await bot.rest.createGuildSoundboardSound(guildId, options, reason))) + }, + modifyGuildSoundboardSound: async (guildId, soundId, options, reason) => { + return bot.transformers.soundboardSound(bot, snakelize(await bot.rest.modifyGuildSoundboardSound(guildId, soundId, options, reason))) + }, + deleteGuildSoundboardSound: async (guildId, soundId, reason) => { + await bot.rest.deleteGuildSoundboardSound(guildId, soundId, reason) + }, } } @@ -1015,4 +1044,16 @@ export interface BotHelpers { listSkus: (applicationId: BigString) => Promise listSubscriptions: (skuId: BigString, options?: ListSkuSubscriptionsOptions) => Promise> getSubscription: (skuId: BigString, subscriptionId: BigString) => Promise> + sendSoundboardSound: (channelId: BigString, options: SendSoundboardSound) => Promise + listDefaultSoundboardSounds: () => Promise + listGuildSoundboardSounds: (guildId: BigString) => Promise<{ items: SoundboardSound[] }> + getGuildSoundboardSound: (guildId: BigString, soundId: BigString) => Promise + createGuildSoundboardSound: (guildId: BigString, options: CreateGuildSoundboardSound, reason?: string) => Promise + modifyGuildSoundboardSound: ( + guildId: BigString, + soundId: BigString, + options: ModifyGuildSoundboardSound, + reason?: string, + ) => Promise + deleteGuildSoundboardSound: (guildId: BigString, soundId: BigString, reason?: string) => Promise } diff --git a/packages/bot/src/transformers.ts b/packages/bot/src/transformers.ts index 361241ca1..f8392a8c3 100644 --- a/packages/bot/src/transformers.ts +++ b/packages/bot/src/transformers.ts @@ -51,6 +51,7 @@ import type { DiscordScheduledEvent, DiscordScheduledEventRecurrenceRule, DiscordSku, + DiscordSoundboardSound, DiscordStageInstance, DiscordSticker, DiscordStickerPack, @@ -116,6 +117,7 @@ import { type ScheduledEvent, type ScheduledEventRecurrenceRule, type Sku, + type SoundboardSound, type StageInstance, type Sticker, type StickerPack, @@ -184,9 +186,11 @@ import { transformScheduledEvent, transformScheduledEventRecurrenceRule, transformSku, + transformSoundboardSound, transformStageInstance, transformSticker, transformStickerPack, + transformSubscription, transformTeam, transformTeamToDiscordTeam, transformTemplate, @@ -206,7 +210,6 @@ import { transformCreateApplicationCommandToDiscordCreateApplicationCommand, transformInteractionResponseToDiscordInteractionResponse, } from './transformers/reverse/index.js' -import { transformSubscription } from './transformers/subscription.js' import type { BotInteractionResponse, DiscordComponent, @@ -280,6 +283,7 @@ export interface Transformers { scheduledEvent: (bot: Bot, payload: DiscordScheduledEvent, scheduledEvent: ScheduledEvent) => any scheduledEventRecurrenceRule: (bot: Bot, payload: DiscordScheduledEventRecurrenceRule, scheduledEvent: ScheduledEventRecurrenceRule) => any sku: (bot: Bot, payload: DiscordSku, sku: Sku) => any + soundboardSound: (bot: Bot, payload: DiscordSoundboardSound, soundboardSound: SoundboardSound) => any stageInstance: (bot: Bot, payload: DiscordStageInstance, stageInstance: StageInstance) => any sticker: (bot: Bot, payload: DiscordSticker, sticker: Sticker) => any stickerPack: (bot: Bot, payload: DiscordStickerPack, stickerPack: StickerPack) => any @@ -360,6 +364,7 @@ export interface Transformers { scheduledEvent: (bot: Bot, payload: DiscordScheduledEvent) => ScheduledEvent scheduledEventRecurrenceRule: (bot: Bot, payload: DiscordScheduledEventRecurrenceRule) => ScheduledEventRecurrenceRule sku: (bot: Bot, payload: DiscordSku) => Sku + soundboardSound: (bot: Bot, payload: DiscordSoundboardSound) => SoundboardSound snowflake: (snowflake: BigString) => bigint stageInstance: (bot: Bot, payload: DiscordStageInstance) => StageInstance sticker: (bot: Bot, payload: DiscordSticker) => Sticker @@ -849,6 +854,16 @@ export interface TransformersDesiredProperties { text: boolean emoji: boolean } + soundboardSound: { + name: boolean + soundId: boolean + volume: boolean + emojiId: boolean + emojiName: boolean + guildId: boolean + available: boolean + user: boolean + } } export interface CreateTransformerOptions { @@ -918,6 +933,7 @@ export function createTransformers(options: RecursivePartial, opts scheduledEvent: options.customizers?.scheduledEvent ?? defaultCustomizer, scheduledEventRecurrenceRule: options.customizers?.scheduledEventRecurrenceRule ?? defaultCustomizer, sku: options.customizers?.sku ?? defaultCustomizer, + soundboardSound: options.customizers?.soundboardSound ?? defaultCustomizer, stageInstance: options.customizers?.stageInstance ?? defaultCustomizer, sticker: options.customizers?.sticker ?? defaultCustomizer, stickerPack: options.customizers?.stickerPack ?? defaultCustomizer, @@ -999,6 +1015,7 @@ export function createTransformers(options: RecursivePartial, opts scheduledEvent: options.scheduledEvent ?? transformScheduledEvent, scheduledEventRecurrenceRule: options.scheduledEventRecurrenceRule ?? transformScheduledEventRecurrenceRule, sku: options.sku ?? transformSku, + soundboardSound: options.soundboardSound ?? transformSoundboardSound, snowflake: options.snowflake ?? snowflakeToBigint, stageInstance: options.stageInstance ?? transformStageInstance, sticker: options.sticker ?? transformSticker, @@ -1534,5 +1551,16 @@ export function createDesiredPropertiesObject( emoji: defaultValue, ...desiredProperties.pollMedia, }, + soundboardSound: { + name: defaultValue, + soundId: defaultValue, + volume: defaultValue, + emojiId: defaultValue, + emojiName: defaultValue, + guildId: defaultValue, + available: defaultValue, + user: defaultValue, + ...desiredProperties.soundboardSound, + }, } } diff --git a/packages/bot/src/transformers/index.ts b/packages/bot/src/transformers/index.ts index 4347f7564..384faf43a 100644 --- a/packages/bot/src/transformers/index.ts +++ b/packages/bot/src/transformers/index.ts @@ -36,6 +36,8 @@ export * from './template.js' export * from './threadMember.js' export * from './toggles/index.js' export * from './types.js' +export * from './subscription.js' +export * from './soundboardSound.js' export * from './user.js' export * from './voiceRegion.js' export * from './voiceState.js' diff --git a/packages/bot/src/transformers/soundboardSound.ts b/packages/bot/src/transformers/soundboardSound.ts new file mode 100644 index 000000000..dfc5ecaaf --- /dev/null +++ b/packages/bot/src/transformers/soundboardSound.ts @@ -0,0 +1,18 @@ +import type { DiscordSoundboardSound } from '@discordeno/types' +import type { Bot, SoundboardSound } from '../index.js' + +export function transformSoundboardSound(bot: Bot, payload: DiscordSoundboardSound): SoundboardSound { + const props = bot.transformers.desiredProperties.soundboardSound + const soundboardSound = {} as SoundboardSound + + if (props.name && payload.name) soundboardSound.name = payload.name + if (props.soundId && payload.sound_id) soundboardSound.soundId = bot.transformers.snowflake(payload.sound_id) + if (props.volume && payload.volume) soundboardSound.volume = payload.volume + if (props.emojiId && payload.emoji_id) soundboardSound.emojiId = bot.transformers.snowflake(payload.emoji_id) + if (props.emojiName && payload.emoji_name) soundboardSound.emojiName = payload.emoji_name + if (props.guildId && payload.guild_id) soundboardSound.guildId = bot.transformers.snowflake(payload.guild_id) + if (props.available && payload.available) soundboardSound.available = payload.available + if (props.user && payload.user) soundboardSound.user = bot.transformers.user(bot, payload.user) + + return bot.transformers.customizers.soundboardSound(bot, payload, soundboardSound) +} diff --git a/packages/bot/src/transformers/subscription.ts b/packages/bot/src/transformers/subscription.ts index aaee9ae7f..2a3ebd1a4 100644 --- a/packages/bot/src/transformers/subscription.ts +++ b/packages/bot/src/transformers/subscription.ts @@ -8,7 +8,8 @@ export function transformSubscription(bot: Bot, payload: DiscordSubscription): S if (props.id && payload.id) subscription.id = bot.transformers.snowflake(payload.id) if (props.userId && payload.user_id) subscription.userId = bot.transformers.snowflake(payload.user_id) if (props.skuIds && payload.sku_ids) subscription.skuIds = payload.sku_ids.map((skuId) => bot.transformers.snowflake(skuId)) - if (props.entitlementIds && payload.entitlement_ids) subscription.entitlementIds = payload.entitlement_ids.map((entitlementId) => bot.transformers.snowflake(entitlementId)) + if (props.entitlementIds && payload.entitlement_ids) + subscription.entitlementIds = payload.entitlement_ids.map((entitlementId) => bot.transformers.snowflake(entitlementId)) if (props.currentPeriodStart && payload.current_period_start) subscription.currentPeriodStart = Date.parse(payload.current_period_start) if (props.currentPeriodEnd && payload.current_period_end) subscription.currentPeriodEnd = Date.parse(payload.current_period_end) if (props.status && payload.status) subscription.status = payload.status diff --git a/packages/bot/src/transformers/toggles/guild.ts b/packages/bot/src/transformers/toggles/guild.ts index b1405484d..8e3d5a14e 100644 --- a/packages/bot/src/transformers/toggles/guild.ts +++ b/packages/bot/src/transformers/toggles/guild.ts @@ -16,6 +16,7 @@ const featureNames = [ 'invitesDisabled', 'inviteSplash', 'memberVerificationGateEnabled', + 'moreSoundboard', 'moreStickers', 'news', 'partnered', @@ -44,7 +45,7 @@ export const GuildToggle = { premiumProgressBarEnabled: 1n << 4n, // GUILD FEATURES ARE BELOW THIS - // MISSING VALUES IN THE BITFIELD: 24, 25, 26 + // MISSING VALUES IN THE BITFIELD: 26, 35+ /** Whether the guild has access to set an animated guild banner image */ animatedBanner: 1n << 11n, @@ -74,6 +75,8 @@ export const GuildToggle = { inviteSplash: 1n << 5n, /** Whether the guild has enabled [Membership Screening](https://discord.com/developers/docs/resources/guild#membership-screening-object) */ memberVerificationGateEnabled: 1n << 19n, + /** Whether the guild has more soundboard sound slot */ + moreSoundboard: 1n << 24n, /** Whether the guild has increased custom sticker slots */ moreStickers: 1n << 23n, /** Whether the guild has access to create news channels */ @@ -90,6 +93,8 @@ export const GuildToggle = { roleSubscriptionsAvailableForPurchase: 1n << 33n, /** Whether the guild has enabled role subscriptions. */ roleSubscriptionsEnabled: 1n << 34n, + /** Whether the guild has created soundboard sounds. */ + soundboard: 1n << 25n, /** Whether the guild has enabled ticketed events */ ticketedEventsEnabled: 1n << 21n, /** Whether the guild has access to set a vanity URL */ @@ -132,6 +137,7 @@ export class GuildToggles extends ToggleBitfieldBigint { if (guild.features.includes(GuildFeatures.InvitesDisabled)) this.add(GuildToggle.invitesDisabled) if (guild.features.includes(GuildFeatures.InviteSplash)) this.add(GuildToggle.inviteSplash) if (guild.features.includes(GuildFeatures.MemberVerificationGateEnabled)) this.add(GuildToggle.memberVerificationGateEnabled) + if (guild.features.includes(GuildFeatures.MoreSoundboard)) this.add(GuildToggle.moreSoundboard) if (guild.features.includes(GuildFeatures.MoreStickers)) this.add(GuildToggle.moreStickers) if (guild.features.includes(GuildFeatures.News)) this.add(GuildToggle.news) if (guild.features.includes(GuildFeatures.Partnered)) this.add(GuildToggle.partnered) @@ -140,6 +146,7 @@ export class GuildToggles extends ToggleBitfieldBigint { if (guild.features.includes(GuildFeatures.RoleIcons)) this.add(GuildToggle.roleIcons) if (guild.features.includes(GuildFeatures.RoleSubscriptionsAvailableForPurchase)) this.add(GuildToggle.roleSubscriptionsAvailableForPurchase) if (guild.features.includes(GuildFeatures.RoleSubscriptionsEnabled)) this.add(GuildToggle.roleSubscriptionsEnabled) + if (guild.features.includes(GuildFeatures.Soundboard)) this.add(GuildToggle.soundboard) if (guild.features.includes(GuildFeatures.TicketedEventsEnabled)) this.add(GuildToggle.ticketedEventsEnabled) if (guild.features.includes(GuildFeatures.VanityUrl)) this.add(GuildToggle.vanityUrl) if (guild.features.includes(GuildFeatures.Verified)) this.add(GuildToggle.verified) @@ -260,6 +267,11 @@ export class GuildToggles extends ToggleBitfieldBigint { return this.has('memberVerificationGateEnabled') } + /** Whether the guild has more soundboard sound slot */ + get moreSoundboard(): boolean { + return this.has('moreSoundboard') + } + /** Whether the guild can be previewed before joining via Membership Screening or the directory */ get previewEnabled(): boolean { return this.has('previewEnabled') @@ -320,6 +332,11 @@ export class GuildToggles extends ToggleBitfieldBigint { return this.has('roleSubscriptionsEnabled') } + /** Whether the guild has created soundboard sounds. */ + get soundboard(): boolean { + return this.has('soundboard') + } + /** Checks whether or not the permissions exist in this */ has(permissions: GuildToggleKeys | GuildToggleKeys[]): boolean { if (!Array.isArray(permissions)) return super.contains(GuildToggle[permissions]) diff --git a/packages/bot/src/transformers/types.ts b/packages/bot/src/transformers/types.ts index 7bfde7356..5db5835ae 100644 --- a/packages/bot/src/transformers/types.ts +++ b/packages/bot/src/transformers/types.ts @@ -1763,3 +1763,23 @@ export interface Subscription { /** ISO3166-1 alpha-2 country code of the payment source used to purchase the subscription. Missing unless queried with a private OAuth scope. */ country?: string } + +/** https://discord.com/developers/docs/resources/soundboard#soundboard-sound-object-soundboard-sound-structure */ +export interface SoundboardSound { + /** The name of this sound */ + name: string + /** The id of this sound */ + soundId: bigint + /** The volume of this sound, from 0 to 1 */ + volume: number + /** The id of this sound's custom emoji */ + emojiId?: bigint + /** The unicode character of this sound's standard emoji */ + emojiName?: string + /** The id of the guild this sound is in */ + guildId?: bigint + /** Whether this sound can be used, may be false due to loss of Server Boosts */ + available: boolean + /** The user who created this sound */ + user?: User +} diff --git a/packages/bot/src/typings.ts b/packages/bot/src/typings.ts index 78f5968dc..8a6281a95 100644 --- a/packages/bot/src/typings.ts +++ b/packages/bot/src/typings.ts @@ -212,4 +212,9 @@ export interface BotGatewayHandlerOptions { SUBSCRIPTION_DELETE: typeof handlers.handleSubscriptionDelete MESSAGE_POLL_VOTE_ADD: typeof handlers.handleMessagePollVoteAdd MESSAGE_POLL_VOTE_REMOVE: typeof handlers.handleMessagePollVoteRemove + GUILD_SOUNDBOARD_SOUND_CREATE: typeof handlers.handleGuildSoundboardSoundCreate + GUILD_SOUNDBOARD_SOUND_DELETE: typeof handlers.handleGuildSoundboardSoundDelete + GUILD_SOUNDBOARD_SOUND_UPDATE: typeof handlers.handleGuildSoundboardSoundUpdate + GUILD_SOUNDBOARD_SOUNDS_UPDATE: typeof handlers.handleGuildSoundboardSoundsUpdate + SOUNDBOARD_SOUNDS: typeof handlers.handleSoundboardSounds } diff --git a/packages/gateway/src/manager.ts b/packages/gateway/src/manager.ts index 49da1a10e..a24e5880b 100644 --- a/packages/gateway/src/manager.ts +++ b/packages/gateway/src/manager.ts @@ -562,6 +562,35 @@ export function createGatewayManager(options: CreateGatewayManagerOptions): Gate }, }) }, + + async requestSoundboardSounds(guildIds) { + /** + * Discord will send the events for the guilds that are "under the shard" that sends the opcode. + * For this reason we need to group the ids with the shard the calculateShardId method gives + */ + + const map = new Map() + + for (const guildId of guildIds) { + const shardId = gateway.calculateShardId(guildId) + + const ids = map.get(shardId) ?? [] + map.set(shardId, ids) + + ids.push(guildId) + } + + await Promise.all( + [...map.entries()].map(([shardId, ids]) => + gateway.sendPayload(shardId, { + op: GatewayOpcodes.RequestSoundboardSounds, + d: { + guild_ids: ids, + }, + }), + ), + ) + }, } return gateway @@ -822,6 +851,26 @@ export interface GatewayManager extends Required { * @see {@link https://discord.com/developers/docs/topics/gateway#update-voice-state} */ leaveVoiceChannel: (guildId: BigString) => Promise + /** + * Used to request soundboard sounds for a list of guilds. + * + * This function sends multiple (see remarks) _Request Soundboard Sounds_ gateway command over the gateway behind the scenes. + * + * @param guildIds - The guilds to get the sounds from + * + * @remarks + * Fires a _Soundboard Sounds_ gateway event. + * + * ⚠️ Discord will send the _Soundboard Sounds_ for each of the guild ids + * however you may not receive the same number of events as the ids passed to _Request Soundboard Sounds_ for one of the following reasons: + * - The bot is not in the server provided + * - The shard the message has been sent from does not receive events for the specified guild + * + * To avoid this Discordeno will automatically try to group the ids based on what shard they will need to be sent, but this involves sending multiple messages in multiple shards + * + * @see {@link https://discord.com/developers/docs/topics/gateway-events#request-soundboard-sounds} + */ + requestSoundboardSounds: (guildIds: BigString[]) => Promise /** This managers cache related settings. */ cache: { requestMembers: { diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index b4db1d71a..e80c94fcf 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -38,6 +38,7 @@ import { type DiscordRole, type DiscordScheduledEvent, type DiscordSku, + type DiscordSoundboardSound, type DiscordStageInstance, type DiscordSticker, type DiscordStickerPack, @@ -1638,6 +1639,44 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return await rest.get(rest.routes.monetization.subscription(skuId, subscriptionId)) }, + async sendSoundboardSound(channelId, options) { + await rest.post(rest.routes.soundboard.sendSound(channelId), { + body: options, + }) + }, + + async listDefaultSoundboardSounds() { + return await rest.get(rest.routes.soundboard.listDefault()) + }, + + async listGuildSoundboardSounds(guildId) { + return await rest.get<{ items: DiscordSoundboardSound[] }>(rest.routes.soundboard.guildSounds(guildId)) + }, + + async getGuildSoundboardSound(guildId, soundId) { + return await rest.get(rest.routes.soundboard.guildSound(guildId, soundId)) + }, + + async createGuildSoundboardSound(guildId, options, reason) { + return await rest.post(rest.routes.soundboard.guildSounds(guildId), { + body: options, + reason, + }) + }, + + async modifyGuildSoundboardSound(guildId, soundId, options, reason) { + return await rest.post(rest.routes.soundboard.guildSound(guildId, soundId), { + body: options, + reason, + }) + }, + + async deleteGuildSoundboardSound(guildId, soundId, reason) { + return await rest.delete(rest.routes.soundboard.guildSound(guildId, soundId), { + reason, + }) + }, + preferSnakeCase(enabled: boolean) { const camelizer = enabled ? (x: any) => x : camelize diff --git a/packages/rest/src/routes.ts b/packages/rest/src/routes.ts index 4e518828e..6d21798cb 100644 --- a/packages/rest/src/routes.ts +++ b/packages/rest/src/routes.ts @@ -612,6 +612,21 @@ export function createRoutes(): RestRoutes { }, }, + soundboard: { + sendSound: (channelId) => { + return `/channels/${channelId}` + }, + listDefault: () => { + return `/soundboard-default-sounds` + }, + guildSounds: (guildId) => { + return `/guilds/${guildId}/soundboard-sounds` + }, + guildSound: (guildId, soundId) => { + return `/guilds/${guildId}/soundboard-sounds/${soundId}` + }, + }, + applicationEmoji(applicationId, emojiId) { return `/applications/${applicationId}/emojis/${emojiId}` }, diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index b99ca9539..2ac1e5a10 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -71,6 +71,7 @@ import type { CreateGuildEmoji, CreateGuildFromTemplate, CreateGuildRole, + CreateGuildSoundboardSound, CreateGuildStickerOptions, CreateMessageOptions, CreateScheduledEvent, @@ -86,6 +87,7 @@ import type { // eslint-disable-next-line @typescript-eslint/no-unused-vars DiscordInteraction, DiscordInteractionCallbackResponse, + DiscordSoundboardSound, DiscordSubscription, EditApplication, EditAutoModerationRuleOptions, @@ -128,12 +130,14 @@ import type { ModifyGuildChannelPositions, ModifyGuildEmoji, ModifyGuildMember, + ModifyGuildSoundboardSound, ModifyGuildTemplate, ModifyRolePositions, ModifyWebhook, ScheduledEventEntityType, ScheduledEventStatus, SearchMembers, + SendSoundboardSound, StartThreadWithMessage, StartThreadWithoutMessage, UpsertGlobalApplicationCommandOptions, @@ -3042,6 +3046,86 @@ export interface RestManager { * @param skuId - The id of the sku of get the subscriptions for */ getSubscription: (skuId: BigString, subscriptionId: BigString) => Promise> + /** + * Send a soundboard sound to a voice channel the user is connected to. + * + * @param channelId - The id of the voice channel + * + * @remarks + * Fires a _Voice Channel Effect Send_ Gateway event. + * + * Requires the `SPEAK` and `USE_SOUNDBOARD` permissions, and also the `USE_EXTERNAL_SOUNDS` permission if the sound is from a different server. + * Additionally, requires the user to be connected to the voice channel, having a voice state without `deaf`, `self_deaf`, `mute`, or `suppress` enabled. + */ + sendSoundboardSound: (channelId: BigString, options: SendSoundboardSound) => Promise + /** Returns an array of soundboard sound objects that can be used by all users. */ + listDefaultSoundboardSounds: () => Promise[]> + /** + * Returns a list of the guild's soundboard sounds. + * + * @param guildId - The guild to get the sounds from + * + * @remarks + * Includes `user` fields if the bot has the `CREATE_GUILD_EXPRESSIONS` or `MANAGE_GUILD_EXPRESSIONS` permission. + */ + listGuildSoundboardSounds: (guildId: BigString) => Promise<{ items: Camelize[] }> + /** + * Returns a soundboard sound object for the given sound id. + * + * @param guildId - The guild to get the sounds from + * @param soundId - The sound id + * + * @remarks + * Includes `user` fields if the bot has the `CREATE_GUILD_EXPRESSIONS` or `MANAGE_GUILD_EXPRESSIONS` permission. + */ + getGuildSoundboardSound: (guildId: BigString, soundId: BigString) => Promise> + /** + * Create a new soundboard sound for the guild. + * + * @param guildId - The guild to create the sounds in + * @param options - The options to create the sound + * @param reason - The audit log reason + * + * @remarks + * Fires a _Guild Soundboard Sound Create_ Gateway event. + * + * Requires the `CREATE_GUILD_EXPRESSIONS` permission. + */ + createGuildSoundboardSound: (guildId: BigString, options: CreateGuildSoundboardSound, reason?: string) => Promise> + /** + * Modify the given soundboard sound. + * + * @param guildId - The guild to create the sounds in + * @param soundId - The sound id to update + * @param options - The options to update the sound + * @param reason - The audit log reason + * + * @remarks + * Fires a _Guild Soundboard Sound Update_ Gateway event. + * + * 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. + */ + modifyGuildSoundboardSound: ( + guildId: BigString, + soundId: BigString, + options: ModifyGuildSoundboardSound, + reason?: string, + ) => Promise> + /** + * Delete the given soundboard sound. + * + * @param guildId - The guild to create the sounds in + * @param soundId - The sound id to delete + * @param reason - The audit log reason + * + * @remarks + * Fires a _Guild Soundboard Sound Delete_ Gateway event. + * + * 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. + */ + deleteGuildSoundboardSound: (guildId: BigString, soundId: BigString, reason?: string) => Promise } export type RequestMethods = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT' diff --git a/packages/rest/src/typings/routes.ts b/packages/rest/src/typings/routes.ts index 92a65f694..209a760c0 100644 --- a/packages/rest/src/typings/routes.ts +++ b/packages/rest/src/typings/routes.ts @@ -280,6 +280,17 @@ export interface RestRoutes { /** Route to get a SKU subscription */ subscription: (skuId: BigString, subscriptionId: BigString) => string } + /** Routes related to soundboard sounds */ + soundboard: { + /** Send a soundboard sound to a voice channel the user is connected to. */ + sendSound: (channelId: BigString) => string + /** List the discord default soundboard sounds */ + listDefault: () => string + /** Route for list/create a guild sounds */ + guildSounds: (guildId: BigString) => string + /** Route for get/edit/delete of a guild sound */ + guildSound: (guildId: BigString, soundId: BigString) => string + } /** Route to list / create an application emoji */ applicationEmojis: (applicationId: BigString) => string /** Route to get / edit / delete an application emoji */ diff --git a/packages/types/src/camel.ts b/packages/types/src/camel.ts index 48e7a5d83..a88830066 100644 --- a/packages/types/src/camel.ts +++ b/packages/types/src/camel.ts @@ -143,6 +143,7 @@ import type { DiscordSelectOption, DiscordSessionStartLimit, DiscordSku, + DiscordSoundboardSound, DiscordStageInstance, DiscordSticker, DiscordStickerItem, @@ -346,3 +347,4 @@ export interface CamelizedDiscordGuildOnboardingOption extends Camelize {} export interface CamelizedDiscordSku extends Camelize {} export interface CamelizedDiscordBulkBan extends Camelize {} +export interface CamelizedDiscordSoundboardSound extends Camelize {} diff --git a/packages/types/src/discord.ts b/packages/types/src/discord.ts index 6735d4c02..07ce5b1e4 100644 --- a/packages/types/src/discord.ts +++ b/packages/types/src/discord.ts @@ -922,6 +922,13 @@ export interface DiscordGuild { stickers?: DiscordSticker[] /** The id of the channel where admins and moderators of Community guilds receive safety alerts from Discord */ safety_alerts_channel_id: string | null + /** + * Soundboard sounds in the guild + * + * @remarks + * Only sent by the gateway + */ + soundboard_sounds?: DiscordSoundboardSound[] } /** https://discord.com/developers/docs/topics/permissions#role-object-role-structure */ @@ -3828,6 +3835,50 @@ export interface DiscordBulkBan { failed_users: string[] } +/** https://discord.com/developers/docs/resources/soundboard#soundboard-sound-object-soundboard-sound-structure */ +export interface DiscordSoundboardSound { + /** The name of this sound */ + name: string + /** The id of this sound */ + sound_id: string + /** The volume of this sound, from 0 to 1 */ + volume: number + /** The id of this sound's custom emoji */ + emoji_id: string | null + /** The unicode character of this sound's standard emoji */ + emoji_name: string | null + /** The id of the guild this sound is in */ + guild_id?: string + /** Whether this sound can be used, may be false due to loss of Server Boosts */ + available: boolean + /** The user who created this sound */ + user?: DiscordUser +} + +/** https://discord.com/developers/docs/topics/gateway-events#guild-soundboard-sound-delete-guild-soundboard-sound-delete-event-fields */ +export interface DiscordSoundboardSoundDelete { + /** ID of the sound that was deleted */ + sound_id: string + /** ID of the guild the sound was in */ + guild_id: string +} + +/** https://discord.com/developers/docs/topics/gateway-events#guild-soundboard-sounds-update-guild-soundboard-sounds-update-event-fields */ +export interface DiscordSoundboardSoundsUpdate { + /** The guild's soundboard sounds */ + soundboard_sounds: DiscordSoundboardSound[] + /** ID of the guild the sound was in */ + guild_id: string +} + +/** https://discord.com/developers/docs/topics/gateway-events#soundboard-sounds-soundboard-sounds-event-fields */ +export interface DiscordSoundboardSounds { + /** The guild's soundboard sounds */ + soundboard_sounds: DiscordSoundboardSound[] + /** ID of the guild the sound was in */ + guild_id: string +} + /** https://discord.com/developers/docs/events/webhook-events#payload-structure */ export interface DiscordEventWebhookEvent { /** Version scheme for the webhook event. Currently always 1 */ diff --git a/packages/types/src/discordeno.ts b/packages/types/src/discordeno.ts index 58341b8fc..11b66924b 100644 --- a/packages/types/src/discordeno.ts +++ b/packages/types/src/discordeno.ts @@ -1598,3 +1598,37 @@ export interface ListSkuSubscriptionsOptions { /** User ID for which to return subscriptions. Required except for OAuth queries. */ userId?: BigString } + +/** https://discord.com/developers/docs/resources/soundboard#send-soundboard-sound-json-params */ +export interface SendSoundboardSound { + /** The id of the soundboard sound to play */ + soundId: BigString + /** The id of the guild the soundboard sound is from, required to play sounds from different servers */ + sourceGuildId?: BigString +} + +/** https://discord.com/developers/docs/resources/soundboard#create-guild-soundboard-sound-json-params */ +export interface CreateGuildSoundboardSound { + /** Name of the soundboard sound (2-32 characters) */ + name: string + /** The mp3 or ogg sound data, base64 encoded, similar to image data */ + sound: string + /** The volume of the soundboard sound, from 0 to 1, defaults to 1 */ + volume?: number | null + /** The id of the custom emoji for the soundboard sound */ + emojiId?: BigString | null + /** The unicode character of a standard emoji for the soundboard sound */ + emojiName?: string | null +} + +/** https://canary.discord.com/developers/docs/resources/soundboard#modify-guild-soundboard-sound-json-params */ +export interface ModifyGuildSoundboardSound { + /** Name of the soundboard sound (2-32 characters) */ + name: string + /** The volume of the soundboard sound, from 0 to 1, defaults to 1 */ + volume: number | null + /** The id of the custom emoji for the soundboard sound */ + emojiId: BigString | null + /** The unicode character of a standard emoji for the soundboard sound */ + emojiName: string | null +} diff --git a/packages/types/src/shared.ts b/packages/types/src/shared.ts index 8c50c84b9..7b454ca88 100644 --- a/packages/types/src/shared.ts +++ b/packages/types/src/shared.ts @@ -336,6 +336,8 @@ export enum GuildFeatures { WelcomeScreenEnabled = 'WELCOME_SCREEN_ENABLED', /** Guild has enabled [Membership Screening](https://discord.com/developers/docs/resources/guild#membership-screening-object) */ MemberVerificationGateEnabled = 'MEMBER_VERIFICATION_GATE_ENABLED', + /** Guild has increased custom soundboard sound slots. */ + MoreSoundboard = 'MORE_SOUNDBOARD', /** Guild can be previewed before joining via Membership Screening or the directory */ PreviewEnabled = 'PREVIEW_ENABLED', /** Guild has enabled ticketed events */ @@ -348,6 +350,8 @@ export enum GuildFeatures { RoleSubscriptionsAvailableForPurchase = 'ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE', /** Guild has enabled role subscriptions. */ RoleSubscriptionsEnabled = 'ROLE_SUBSCRIPTIONS_ENABLED', + /** Guild has created soundboard sounds. */ + Soundboard = 'SOUNDBOARD', /** Guild has set up auto moderation rules */ AutoModeration = 'AUTO_MODERATION', /** Guild has paused invites, preventing new users from joining */ @@ -639,6 +643,12 @@ export enum AuditLogEvents { ThreadDelete, /** Permissions were updated for a command */ ApplicationCommandPermissionUpdate = 121, + /** Soundboard sound was created */ + SoundboardSoundCreate = 130, + /** Soundboard sound was updated */ + SoundboardSoundUpdate, + /** Soundboard sound was deleted */ + SoundboardSoundDelete, /** Auto moderation rule was created */ AutoModerationRuleCreate = 140, /** Auto moderation rule was updated */ @@ -874,6 +884,8 @@ export enum GatewayOpcodes { Hello, /** Sent in response to receiving a heartbeat to acknowledge that it has been received. */ HeartbeatACK, + /** Used to request soundboard sounds for a list of guilds. */ + RequestSoundboardSounds = 31, } export type GatewayDispatchEventNames = @@ -914,6 +926,11 @@ export type GatewayDispatchEventNames = | 'GUILD_SCHEDULED_EVENT_DELETE' | 'GUILD_SCHEDULED_EVENT_USER_ADD' | 'GUILD_SCHEDULED_EVENT_USER_REMOVE' + | 'GUILD_SOUNDBOARD_SOUND_CREATE' + | 'GUILD_SOUNDBOARD_SOUND_UPDATE' + | 'GUILD_SOUNDBOARD_SOUND_DELETE' + | 'GUILD_SOUNDBOARD_SOUNDS_UPDATE' + | 'SOUNDBOARD_SOUNDS' | 'INTEGRATION_CREATE' | 'INTEGRATION_UPDATE' | 'INTEGRATION_DELETE' @@ -991,8 +1008,12 @@ export enum GatewayIntents { /** * - GUILD_EMOJIS_UPDATE * - GUILD_STICKERS_UPDATE + * - GUILD_SOUNDBOARD_SOUND_CREATE + * - GUILD_SOUNDBOARD_SOUND_UPDATE + * - GUILD_SOUNDBOARD_SOUND_DELETE + * - GUILD_SOUNDBOARD_SOUNDS_UPDATE */ - GuildEmojisAndStickers = 1 << 3, + GuildExpressions = 1 << 3, /** * - GUILD_INTEGRATIONS_UPDATE * - INTEGRATION_CREATE diff --git a/packages/utils/src/urls.ts b/packages/utils/src/urls.ts index c05e8d898..10f473741 100644 --- a/packages/utils/src/urls.ts +++ b/packages/utils/src/urls.ts @@ -7,3 +7,7 @@ export function skuLink(appId: BigString, skuId: BigString): string { export function storeLink(appId: BigString): string { return `https://discord.com/application-directory/${appId}/store` } + +export function soundLink(soundId: BigString): string { + return `https://cdn.discordapp.com/soundboard-sounds/${soundId}` +}