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 <awesome@stickz.dev>

* Update for discord/discord-api-docs#7207

* Update discord.ts

---------

Co-authored-by: Awesome Stickz <awesome@stickz.dev>
Co-authored-by: ITOH <to@itoh.at>
This commit is contained in:
Fleny
2024-11-05 15:14:37 +01:00
committed by GitHub
parent 87e6cd0c2e
commit dfa7ff4045
27 changed files with 522 additions and 4 deletions

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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'

View File

@@ -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<void> {
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)),
})
}

View File

@@ -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<void> {
if (!bot.events.soundboardSoundCreate) return
const payload = data.d as DiscordSoundboardSound
bot.events.soundboardSoundCreate(bot.transformers.soundboardSound(bot, payload))
}

View File

@@ -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<void> {
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),
})
}

View File

@@ -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<void> {
if (!bot.events.soundboardSoundUpdate) return
const payload = data.d as DiscordSoundboardSound
bot.events.soundboardSoundUpdate(bot.transformers.soundboardSound(bot, payload))
}

View File

@@ -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<void> {
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)),
})
}

View File

@@ -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'

View File

@@ -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<Sku[]>
listSubscriptions: (skuId: BigString, options?: ListSkuSubscriptionsOptions) => Promise<Camelize<DiscordSubscription[]>>
getSubscription: (skuId: BigString, subscriptionId: BigString) => Promise<Camelize<DiscordSubscription>>
sendSoundboardSound: (channelId: BigString, options: SendSoundboardSound) => Promise<void>
listDefaultSoundboardSounds: () => Promise<SoundboardSound[]>
listGuildSoundboardSounds: (guildId: BigString) => Promise<{ items: SoundboardSound[] }>
getGuildSoundboardSound: (guildId: BigString, soundId: BigString) => Promise<SoundboardSound>
createGuildSoundboardSound: (guildId: BigString, options: CreateGuildSoundboardSound, reason?: string) => Promise<SoundboardSound>
modifyGuildSoundboardSound: (
guildId: BigString,
soundId: BigString,
options: ModifyGuildSoundboardSound,
reason?: string,
) => Promise<SoundboardSound>
deleteGuildSoundboardSound: (guildId: BigString, soundId: BigString, reason?: string) => Promise<void>
}

View File

@@ -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<Transformers>, 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<Transformers>, 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,
},
}
}

View File

@@ -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'

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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])

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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<number, BigString[]>()
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<CreateGatewayManagerOptions> {
* @see {@link https://discord.com/developers/docs/topics/gateway#update-voice-state}
*/
leaveVoiceChannel: (guildId: BigString) => Promise<void>
/**
* 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<void>
/** This managers cache related settings. */
cache: {
requestMembers: {

View File

@@ -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<DiscordSubscription>(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<DiscordSoundboardSound[]>(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<DiscordSoundboardSound>(rest.routes.soundboard.guildSound(guildId, soundId))
},
async createGuildSoundboardSound(guildId, options, reason) {
return await rest.post<DiscordSoundboardSound>(rest.routes.soundboard.guildSounds(guildId), {
body: options,
reason,
})
},
async modifyGuildSoundboardSound(guildId, soundId, options, reason) {
return await rest.post<DiscordSoundboardSound>(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

View File

@@ -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}`
},

View File

@@ -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<Camelize<DiscordSubscription>>
/**
* 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<void>
/** Returns an array of soundboard sound objects that can be used by all users. */
listDefaultSoundboardSounds: () => Promise<Camelize<DiscordSoundboardSound>[]>
/**
* 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<DiscordSoundboardSound>[] }>
/**
* 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<Camelize<DiscordSoundboardSound>>
/**
* 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<Camelize<DiscordSoundboardSound>>
/**
* 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<Camelize<DiscordSoundboardSound>>
/**
* 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<void>
}
export type RequestMethods = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT'

View File

@@ -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 */

View File

@@ -143,6 +143,7 @@ import type {
DiscordSelectOption,
DiscordSessionStartLimit,
DiscordSku,
DiscordSoundboardSound,
DiscordStageInstance,
DiscordSticker,
DiscordStickerItem,
@@ -346,3 +347,4 @@ export interface CamelizedDiscordGuildOnboardingOption extends Camelize<DiscordG
export interface CamelizedDiscordEntitlement extends Camelize<DiscordEntitlement> {}
export interface CamelizedDiscordSku extends Camelize<DiscordSku> {}
export interface CamelizedDiscordBulkBan extends Camelize<DiscordBulkBan> {}
export interface CamelizedDiscordSoundboardSound extends Camelize<DiscordSoundboardSound> {}

View File

@@ -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 */

View File

@@ -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
}

View File

@@ -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

View File

@@ -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}`
}