Skip to content

Commit b61e513

Browse files
committed
feat: add soundboard
1 parent e3e3c21 commit b61e513

26 files changed

+743
-16
lines changed

packages/discord.js/src/client/Client.js

+15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const { ClientPresence } = require('../structures/ClientPresence.js');
1919
const { GuildPreview } = require('../structures/GuildPreview.js');
2020
const { GuildTemplate } = require('../structures/GuildTemplate.js');
2121
const { Invite } = require('../structures/Invite.js');
22+
const { SoundboardSound } = require('../structures/SoundboardSound.js');
2223
const { Sticker } = require('../structures/Sticker.js');
2324
const { StickerPack } = require('../structures/StickerPack.js');
2425
const { VoiceRegion } = require('../structures/VoiceRegion.js');
@@ -537,6 +538,20 @@ class Client extends BaseClient {
537538
return new Collection(data.sticker_packs.map(stickerPack => [stickerPack.id, new StickerPack(this, stickerPack)]));
538539
}
539540

541+
/**
542+
* Obtains the list of default soundboard sounds.
543+
* @returns {Promise<Collection<string, SoundboardSound>>}
544+
* @example
545+
* client.fetchDefaultSoundboardSounds()
546+
* .then(sounds => console.log(`Available soundboard sounds are: ${sounds.map(sound => sound.name).join(', ')}`))
547+
* .catch(console.error);
548+
*/
549+
async fetchDefaultSoundboardSounds() {
550+
const data = await this.rest.get(Routes.soundboardDefaultSounds());
551+
552+
return new Collection(data.map(sound => [sound.sound_id, new SoundboardSound(this, sound)]));
553+
}
554+
540555
/**
541556
* Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds.
542557
* @param {GuildResolvable} guild The guild to fetch the preview for

packages/discord.js/src/client/actions/Action.js

+4
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ class Action {
131131
return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false);
132132
}
133133

134+
getSoundboardSound(data, guild) {
135+
return this.getPayload(data, guild.soundboardSounds, data.sound_id, Partials.SoundboardSound);
136+
}
137+
134138
spreadInjectedData(data) {
135139
return Object.fromEntries(Object.getOwnPropertySymbols(data).map(symbol => [symbol, data[symbol]]));
136140
}

packages/discord.js/src/client/actions/ActionsManager.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class ActionsManager {
2727
this.register(require('./GuildScheduledEventDelete.js').GuildScheduledEventDeleteAction);
2828
this.register(require('./GuildScheduledEventUserAdd.js').GuildScheduledEventUserAddAction);
2929
this.register(require('./GuildScheduledEventUserRemove.js').GuildScheduledEventUserRemoveAction);
30+
this.register(require('./GuildSoundboardSoundDelete.js').GuildSoundboardSoundDeleteAction);
3031
this.register(require('./GuildStickerCreate.js').GuildStickerCreateAction);
3132
this.register(require('./GuildStickerDelete.js').GuildStickerDeleteAction);
3233
this.register(require('./GuildStickerUpdate.js').GuildStickerUpdateAction);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const { Action } = require('./Action.js');
4+
const { Events } = require('../../util/Events.js');
5+
6+
class GuildSoundboardSoundDeleteAction extends Action {
7+
handle(data) {
8+
const guild = this.client.guilds.cache.get(data.guild_id);
9+
10+
if (!guild) return {};
11+
12+
const soundboardSound = this.getSoundboardSound(data, guild);
13+
14+
if (soundboardSound) {
15+
guild.soundboardSounds.cache.delete(soundboardSound.soundId);
16+
17+
/**
18+
* Emitted whenever a soundboard sound is deleted in a guild.
19+
* @event Client#guildSoundboardSoundDelete
20+
* @param {SoundboardSound} soundboardSound The soundboard sound that was deleted
21+
*/
22+
this.client.emit(Events.GuildSoundboardSoundDelete, soundboardSound);
23+
}
24+
25+
return { soundboardSound };
26+
}
27+
}
28+
29+
exports.GuildSoundboardSoundDeleteAction = GuildSoundboardSoundDeleteAction;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
const { Collection } = require('@discordjs/collection');
4+
const { Events } = require('../../../util/Events.js');
5+
6+
module.exports = (client, { d: data }) => {
7+
const guild = client.guilds.cache.get(data.guild_id);
8+
9+
if (!guild) return;
10+
11+
const soundboardSounds = new Collection();
12+
13+
for (const soundboardSound of data.soundboard_sounds) {
14+
soundboardSounds.set(soundboardSound.sound_id, guild.soundboardSounds._add(soundboardSound));
15+
}
16+
17+
/**
18+
* Emitted whenever multiple guild soundboard sounds are updated.
19+
* @event Client#guildSoundboardSoundsUpdate
20+
* @param {Collection<Snowflake, SoundboardSound>} soundboardSounds The updated soundboard sounds
21+
* @param {Guild} guild The guild that the soundboard sounds are from
22+
*/
23+
client.emit(Events.GuildSoundboardSoundsUpdate, soundboardSounds, guild);
24+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
const { Events } = require('../../../util/Events.js');
4+
5+
module.exports = (client, { d: data }) => {
6+
const guild = client.guilds.cache.get(data.guild_id);
7+
8+
if (!guild) return;
9+
10+
const soundboardSound = guild.soundboardSounds._add(data);
11+
12+
/**
13+
* Emitted whenever a guild soundboard sound is created.
14+
* @event Client#guildSoundboardSoundCreate
15+
* @param {SoundboardSound} soundboardSound The created guild soundboard sound
16+
*/
17+
client.emit(Events.GuildSoundboardSoundCreate, soundboardSound);
18+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
module.exports = (client, { d: data }) => {
4+
client.actions.GuildSoundboardSoundDelete.handle(data);
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
const { Events } = require('../../../util/Events.js');
4+
5+
module.exports = (client, { d: data }) => {
6+
const guild = client.guilds.cache.get(data.guild_id);
7+
8+
if (!guild) return;
9+
10+
const oldGuildSoundboardSound = guild.soundboardSounds.cache.get(data.sound_id)?._clone() ?? null;
11+
const newGuildSoundboardSound = guild.soundboardSounds._add(data);
12+
13+
/**
14+
* Emitted whenever a guild soundboard sound is updated.
15+
* @event Client#guildSoundboardSoundUpdate
16+
* @param {?SoundboardSound} oldGuildSoundboardSound The guild soundboard sound before the update
17+
* @param {SoundboardSound} newGuildSoundboardSound The guild soundboard sound after the update
18+
*/
19+
client.emit(Events.GuildSoundboardSoundUpdate, oldGuildSoundboardSound, newGuildSoundboardSound);
20+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
const { Collection } = require('@discordjs/collection');
4+
const { Events } = require('../../../util/Events.js');
5+
6+
module.exports = (client, { d: data }) => {
7+
const guild = client.guilds.cache.get(data.guild_id);
8+
9+
if (!guild) return;
10+
11+
const soundboardSounds = new Collection();
12+
13+
for (const soundboardSound of data.soundboard_sounds) {
14+
soundboardSounds.set(soundboardSound.sound_id, guild.soundboardSounds._add(soundboardSound));
15+
}
16+
17+
/**
18+
* Emitted whenever soundboard sounds are received (all soundboard sounds come from the same guild).
19+
* @event Client#soundboardSounds
20+
* @param {Collection<Snowflake, SoundboardSound>} soundboardSounds The sounds received
21+
* @param {Guild} guild The guild that the soundboard sounds are from
22+
*/
23+
client.emit(Events.SoundboardSounds, soundboardSounds, guild);
24+
};

packages/discord.js/src/client/websocket/handlers/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const PacketHandlers = Object.fromEntries([
3232
['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE.js')],
3333
['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD.js')],
3434
['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE.js')],
35+
['GUILD_SOUNDBOARD_SOUNDS_UPDATE', require('./GUILD_SOUNDBOARD_SOUNDS_UPDATE.js')],
36+
['GUILD_SOUNDBOARD_SOUND_CREATE', require('./GUILD_SOUNDBOARD_SOUND_CREATE.js')],
37+
['GUILD_SOUNDBOARD_SOUND_DELETE', require('./GUILD_SOUNDBOARD_SOUND_DELETE.js')],
38+
['GUILD_SOUNDBOARD_SOUND_UPDATE', require('./GUILD_SOUNDBOARD_SOUND_UPDATE.js')],
3539
['GUILD_STICKERS_UPDATE', require('./GUILD_STICKERS_UPDATE.js')],
3640
['GUILD_UPDATE', require('./GUILD_UPDATE.js')],
3741
['INTERACTION_CREATE', require('./INTERACTION_CREATE.js')],
@@ -49,6 +53,7 @@ const PacketHandlers = Object.fromEntries([
4953
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE.js')],
5054
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE.js')],
5155
['READY', require('./READY.js')],
56+
['SOUNDBOARD_SOUNDS', require('./SOUNDBOARD_SOUNDS.js')],
5257
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE.js')],
5358
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE.js')],
5459
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE.js')],

packages/discord.js/src/errors/ErrorCodes.js

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
* @property {'GuildChannelUnowned'} GuildChannelUnowned
6262
* @property {'GuildOwned'} GuildOwned
6363
* @property {'GuildMembersTimeout'} GuildMembersTimeout
64+
* @property {'GuildSoundboardSoundsTimeout'} GuildSoundboardSoundsTimeout
6465
* @property {'GuildUncachedMe'} GuildUncachedMe
6566
* @property {'ChannelNotCached'} ChannelNotCached
6667
* @property {'StageChannelResolve'} StageChannelResolve
@@ -85,6 +86,8 @@
8586
* @property {'EmojiManaged'} EmojiManaged
8687
* @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission
8788
*
89+
90+
* @property {'NotGuildSoundboardSound'} NotGuildSoundboardSound
8891
* @property {'NotGuildSticker'} NotGuildSticker
8992
9093
* @property {'ReactionResolveUser'} ReactionResolveUser
@@ -193,6 +196,7 @@ const keys = [
193196
'GuildChannelUnowned',
194197
'GuildOwned',
195198
'GuildMembersTimeout',
199+
'GuildSoundboardSoundsTimeout',
196200
'GuildUncachedMe',
197201
'ChannelNotCached',
198202
'StageChannelResolve',
@@ -217,6 +221,7 @@ const keys = [
217221
'EmojiManaged',
218222
'MissingManageGuildExpressionsPermission',
219223

224+
'NotGuildSoundboardSound',
220225
'NotGuildSticker',
221226

222227
'ReactionResolveUser',

packages/discord.js/src/errors/Messages.js

+3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const Messages = {
6666
[ErrorCodes.GuildChannelUnowned]: "The fetched channel does not belong to this manager's guild.",
6767
[ErrorCodes.GuildOwned]: 'Guild is owned by the client.',
6868
[ErrorCodes.GuildMembersTimeout]: "Members didn't arrive in time.",
69+
[ErrorCodes.GuildSoundboardSoundsTimeout]: "Soundboard sounds didn't arrive in time.",
6970
[ErrorCodes.GuildUncachedMe]: 'The client user as a member of this guild is uncached.',
7071
[ErrorCodes.ChannelNotCached]: 'Could not find the channel where this message came from in the cache!',
7172
[ErrorCodes.StageChannelResolve]: 'Could not resolve channel to a stage channel.',
@@ -91,6 +92,8 @@ const Messages = {
9192
[ErrorCodes.MissingManageGuildExpressionsPermission]: guild =>
9293
`Client must have Manage Guild Expressions permission in guild ${guild} to see emoji authors.`,
9394

95+
[ErrorCodes.NotGuildSoundboardSound]: action =>
96+
`Soundboard sound is a default (non-guild) soundboard sound and can't be ${action}.`,
9497
[ErrorCodes.NotGuildSticker]: 'Sticker is a standard (non-guild) sticker and has no author.',
9598

9699
[ErrorCodes.ReactionResolveUser]: "Couldn't resolve the user id to remove from the reaction.",

packages/discord.js/src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ exports.GuildMemberManager = require('./managers/GuildMemberManager.js').GuildMe
7777
exports.GuildMemberRoleManager = require('./managers/GuildMemberRoleManager.js').GuildMemberRoleManager;
7878
exports.GuildMessageManager = require('./managers/GuildMessageManager.js').GuildMessageManager;
7979
exports.GuildScheduledEventManager = require('./managers/GuildScheduledEventManager.js').GuildScheduledEventManager;
80+
exports.GuildSoundboardSoundManager = require('./managers/GuildSoundboardSoundManager.js').GuildSoundboardSoundManager;
8081
exports.GuildStickerManager = require('./managers/GuildStickerManager.js').GuildStickerManager;
8182
exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager.js').GuildTextThreadManager;
8283
exports.MessageManager = require('./managers/MessageManager.js').MessageManager;
@@ -205,6 +206,7 @@ exports.RoleSelectMenuBuilder = require('./structures/RoleSelectMenuBuilder.js')
205206
exports.RoleSelectMenuComponent = require('./structures/RoleSelectMenuComponent.js').RoleSelectMenuComponent;
206207
exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteraction.js').RoleSelectMenuInteraction;
207208
exports.SKU = require('./structures/SKU.js').SKU;
209+
exports.SoundboardSound = require('./structures/SoundboardSound.js').SoundboardSound;
208210
exports.StageChannel = require('./structures/StageChannel.js').StageChannel;
209211
exports.StageInstance = require('./structures/StageInstance.js').StageInstance;
210212
exports.Sticker = require('./structures/Sticker.js').Sticker;

packages/discord.js/src/managers/GuildManager.js

+67-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ const process = require('node:process');
44
const { setTimeout, clearTimeout } = require('node:timers');
55
const { Collection } = require('@discordjs/collection');
66
const { makeURLSearchParams } = require('@discordjs/rest');
7-
const { Routes, RouteBases } = require('discord-api-types/v10');
7+
const { GatewayOpcodes, Routes, RouteBases } = require('discord-api-types/v10');
88
const { CachedManager } = require('./CachedManager.js');
9+
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
910
const { ShardClientUtil } = require('../sharding/ShardClientUtil.js');
1011
const { Guild } = require('../structures/Guild.js');
1112
const { GuildChannel } = require('../structures/GuildChannel.js');
@@ -282,6 +283,71 @@ class GuildManager extends CachedManager {
282283
return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection());
283284
}
284285

286+
/**
287+
* @typedef {Object} FetchSoundboardSoundsOptions
288+
* @param {Snowflake[]} guildIds The ids of the guilds to fetch soundboard sounds for
289+
* @param {number} [time=10_000] The timeout for receipt of the soundboard sounds
290+
*/
291+
292+
/**
293+
* Fetches soundboard sounds for the specified guilds.
294+
* @param {FetchSoundboardSoundsOptions} options The options for fetching soundboard sounds
295+
* @returns {Promise<Collection<Snowflake, Collection<string, SoundboardSound>>>}
296+
* @example
297+
* // Fetch soundboard sounds for multiple guilds
298+
* const soundboardSounds = await client.guilds.fetchSoundboardSounds({
299+
* guildIds: ['1234567890', '9876543210'],
300+
* })
301+
*
302+
* console.log(soundboardSounds.get('1234567890'));
303+
*/
304+
async fetchSoundboardSounds({ guildIds, time = 10_000 }) {
305+
const shardCount = await this.client.ws.getShardCount();
306+
const shardIds = Map.groupBy(guildIds, guildId => ShardClientUtil.shardIdForGuildId(guildId, shardCount));
307+
308+
for (const [shardId, shardGuildIds] of shardIds) {
309+
this.client.ws.send(shardId, {
310+
op: GatewayOpcodes.RequestSoundboardSounds,
311+
d: {
312+
guild_ids: shardGuildIds,
313+
},
314+
});
315+
}
316+
317+
return new Promise((resolve, reject) => {
318+
const remainingGuildIds = new Set(guildIds);
319+
320+
const fetchedSoundboardSounds = new Collection();
321+
322+
const handler = (soundboardSounds, guild) => {
323+
if (!remainingGuildIds.has(guild.id)) return;
324+
325+
timeout.refresh();
326+
327+
fetchedSoundboardSounds.set(guild.id, soundboardSounds);
328+
329+
remainingGuildIds.delete(guild.id);
330+
331+
if (remainingGuildIds.size === 0) {
332+
clearTimeout(timeout);
333+
this.client.removeListener(Events.SoundboardSounds, handler);
334+
this.client.decrementMaxListeners();
335+
336+
resolve(fetchedSoundboardSounds);
337+
}
338+
};
339+
340+
const timeout = setTimeout(() => {
341+
this.client.removeListener(Events.SoundboardSounds, handler);
342+
this.client.decrementMaxListeners();
343+
reject(new DiscordjsError(ErrorCodes.GuildSoundboardSoundsTimeout));
344+
}, time).unref();
345+
346+
this.client.incrementMaxListeners();
347+
this.client.on(Events.SoundboardSounds, handler);
348+
});
349+
}
350+
285351
/**
286352
* Options used to set incident actions. Supplying `null` to any option will disable the action.
287353
* @typedef {Object} IncidentActionsEditOptions

0 commit comments

Comments
 (0)