Skip to content

feat: add soundboard #10590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/discord.js/src/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const { ClientPresence } = require('../structures/ClientPresence.js');
const { GuildPreview } = require('../structures/GuildPreview.js');
const { GuildTemplate } = require('../structures/GuildTemplate.js');
const { Invite } = require('../structures/Invite.js');
const { SoundboardSound } = require('../structures/SoundboardSound.js');
const { Sticker } = require('../structures/Sticker.js');
const { StickerPack } = require('../structures/StickerPack.js');
const { VoiceRegion } = require('../structures/VoiceRegion.js');
Expand Down Expand Up @@ -537,6 +538,20 @@ class Client extends BaseClient {
return new Collection(data.sticker_packs.map(stickerPack => [stickerPack.id, new StickerPack(this, stickerPack)]));
}

/**
* Obtains the list of default soundboard sounds.
* @returns {Promise<Collection<string, SoundboardSound>>}
* @example
* client.fetchDefaultSoundboardSounds()
* .then(sounds => console.log(`Available soundboard sounds are: ${sounds.map(sound => sound.name).join(', ')}`))
* .catch(console.error);
*/
async fetchDefaultSoundboardSounds() {
const data = await this.rest.get(Routes.soundboardDefaultSounds());

return new Collection(data.map(sound => [sound.sound_id, new SoundboardSound(this, sound)]));
}

/**
* Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds.
* @param {GuildResolvable} guild The guild to fetch the preview for
Expand Down
4 changes: 4 additions & 0 deletions packages/discord.js/src/client/actions/Action.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ class Action {
return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false);
}

getSoundboardSound(data, guild) {
return this.getPayload(data, guild.soundboardSounds, data.sound_id, Partials.SoundboardSound);
}

spreadInjectedData(data) {
return Object.fromEntries(Object.getOwnPropertySymbols(data).map(symbol => [symbol, data[symbol]]));
}
Expand Down
1 change: 1 addition & 0 deletions packages/discord.js/src/client/actions/ActionsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ActionsManager {
this.register(require('./GuildScheduledEventDelete.js').GuildScheduledEventDeleteAction);
this.register(require('./GuildScheduledEventUserAdd.js').GuildScheduledEventUserAddAction);
this.register(require('./GuildScheduledEventUserRemove.js').GuildScheduledEventUserRemoveAction);
this.register(require('./GuildSoundboardSoundDelete.js').GuildSoundboardSoundDeleteAction);
this.register(require('./GuildStickerCreate.js').GuildStickerCreateAction);
this.register(require('./GuildStickerDelete.js').GuildStickerDeleteAction);
this.register(require('./GuildStickerUpdate.js').GuildStickerUpdateAction);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const { Action } = require('./Action.js');
const { Events } = require('../../util/Events.js');

class GuildSoundboardSoundDeleteAction extends Action {
handle(data) {
const guild = this.client.guilds.cache.get(data.guild_id);

if (!guild) return {};

const soundboardSound = this.getSoundboardSound(data, guild);

if (soundboardSound) {
guild.soundboardSounds.cache.delete(soundboardSound.soundId);

/**
* Emitted whenever a soundboard sound is deleted in a guild.
* @event Client#guildSoundboardSoundDelete
* @param {SoundboardSound} soundboardSound The soundboard sound that was deleted
*/
this.client.emit(Events.GuildSoundboardSoundDelete, soundboardSound);
}

return { soundboardSound };
}
}

exports.GuildSoundboardSoundDeleteAction = GuildSoundboardSoundDeleteAction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const { Collection } = require('@discordjs/collection');
const { Events } = require('../../../util/Events.js');

module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);

if (!guild) return;

const soundboardSounds = new Collection();

for (const soundboardSound of data.soundboard_sounds) {
soundboardSounds.set(soundboardSound.sound_id, guild.soundboardSounds._add(soundboardSound));
}

/**
* Emitted whenever multiple guild soundboard sounds are updated.
* @event Client#guildSoundboardSoundsUpdate
* @param {Collection<Snowflake, SoundboardSound>} soundboardSounds The updated soundboard sounds
* @param {Guild} guild The guild that the soundboard sounds are from
*/
client.emit(Events.GuildSoundboardSoundsUpdate, soundboardSounds, guild);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

const { Events } = require('../../../util/Events.js');

module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);

if (!guild) return;

const soundboardSound = guild.soundboardSounds._add(data);

/**
* Emitted whenever a guild soundboard sound is created.
* @event Client#guildSoundboardSoundCreate
* @param {SoundboardSound} soundboardSound The created guild soundboard sound
*/
client.emit(Events.GuildSoundboardSoundCreate, soundboardSound);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = (client, { d: data }) => {
client.actions.GuildSoundboardSoundDelete.handle(data);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

const { Events } = require('../../../util/Events.js');

module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);

if (!guild) return;

const oldGuildSoundboardSound = guild.soundboardSounds.cache.get(data.sound_id)?._clone() ?? null;
const newGuildSoundboardSound = guild.soundboardSounds._add(data);

/**
* Emitted whenever a guild soundboard sound is updated.
* @event Client#guildSoundboardSoundUpdate
* @param {?SoundboardSound} oldGuildSoundboardSound The guild soundboard sound before the update
* @param {SoundboardSound} newGuildSoundboardSound The guild soundboard sound after the update
*/
client.emit(Events.GuildSoundboardSoundUpdate, oldGuildSoundboardSound, newGuildSoundboardSound);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const { Collection } = require('@discordjs/collection');
const { Events } = require('../../../util/Events.js');

module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);

if (!guild) return;

const soundboardSounds = new Collection();

for (const soundboardSound of data.soundboard_sounds) {
soundboardSounds.set(soundboardSound.sound_id, guild.soundboardSounds._add(soundboardSound));
}

/**
* Emitted whenever soundboard sounds are received (all soundboard sounds come from the same guild).
* @event Client#soundboardSounds
* @param {Collection<Snowflake, SoundboardSound>} soundboardSounds The sounds received
* @param {Guild} guild The guild that the soundboard sounds are from
*/
client.emit(Events.SoundboardSounds, soundboardSounds, guild);
};
5 changes: 5 additions & 0 deletions packages/discord.js/src/client/websocket/handlers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const PacketHandlers = Object.fromEntries([
['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE.js')],
['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD.js')],
['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE.js')],
['GUILD_SOUNDBOARD_SOUNDS_UPDATE', require('./GUILD_SOUNDBOARD_SOUNDS_UPDATE.js')],
['GUILD_SOUNDBOARD_SOUND_CREATE', require('./GUILD_SOUNDBOARD_SOUND_CREATE.js')],
['GUILD_SOUNDBOARD_SOUND_DELETE', require('./GUILD_SOUNDBOARD_SOUND_DELETE.js')],
['GUILD_SOUNDBOARD_SOUND_UPDATE', require('./GUILD_SOUNDBOARD_SOUND_UPDATE.js')],
['GUILD_STICKERS_UPDATE', require('./GUILD_STICKERS_UPDATE.js')],
['GUILD_UPDATE', require('./GUILD_UPDATE.js')],
['INTERACTION_CREATE', require('./INTERACTION_CREATE.js')],
Expand All @@ -49,6 +53,7 @@ const PacketHandlers = Object.fromEntries([
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE.js')],
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE.js')],
['READY', require('./READY.js')],
['SOUNDBOARD_SOUNDS', require('./SOUNDBOARD_SOUNDS.js')],
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE.js')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE.js')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE.js')],
Expand Down
5 changes: 5 additions & 0 deletions packages/discord.js/src/errors/ErrorCodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
* @property {'GuildChannelUnowned'} GuildChannelUnowned
* @property {'GuildOwned'} GuildOwned
* @property {'GuildMembersTimeout'} GuildMembersTimeout
* @property {'GuildSoundboardSoundsTimeout'} GuildSoundboardSoundsTimeout
* @property {'GuildUncachedMe'} GuildUncachedMe
* @property {'ChannelNotCached'} ChannelNotCached
* @property {'StageChannelResolve'} StageChannelResolve
Expand All @@ -85,6 +86,8 @@
* @property {'EmojiManaged'} EmojiManaged
* @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission
*

* @property {'NotGuildSoundboardSound'} NotGuildSoundboardSound
* @property {'NotGuildSticker'} NotGuildSticker

* @property {'ReactionResolveUser'} ReactionResolveUser
Expand Down Expand Up @@ -193,6 +196,7 @@ const keys = [
'GuildChannelUnowned',
'GuildOwned',
'GuildMembersTimeout',
'GuildSoundboardSoundsTimeout',
'GuildUncachedMe',
'ChannelNotCached',
'StageChannelResolve',
Expand All @@ -217,6 +221,7 @@ const keys = [
'EmojiManaged',
'MissingManageGuildExpressionsPermission',

'NotGuildSoundboardSound',
'NotGuildSticker',

'ReactionResolveUser',
Expand Down
3 changes: 3 additions & 0 deletions packages/discord.js/src/errors/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const Messages = {
[ErrorCodes.GuildChannelUnowned]: "The fetched channel does not belong to this manager's guild.",
[ErrorCodes.GuildOwned]: 'Guild is owned by the client.',
[ErrorCodes.GuildMembersTimeout]: "Members didn't arrive in time.",
[ErrorCodes.GuildSoundboardSoundsTimeout]: "Soundboard sounds didn't arrive in time.",
[ErrorCodes.GuildUncachedMe]: 'The client user as a member of this guild is uncached.',
[ErrorCodes.ChannelNotCached]: 'Could not find the channel where this message came from in the cache!',
[ErrorCodes.StageChannelResolve]: 'Could not resolve channel to a stage channel.',
Expand All @@ -91,6 +92,8 @@ const Messages = {
[ErrorCodes.MissingManageGuildExpressionsPermission]: guild =>
`Client must have Manage Guild Expressions permission in guild ${guild} to see emoji authors.`,

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

[ErrorCodes.ReactionResolveUser]: "Couldn't resolve the user id to remove from the reaction.",
Expand Down
2 changes: 2 additions & 0 deletions packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ exports.GuildMemberManager = require('./managers/GuildMemberManager.js').GuildMe
exports.GuildMemberRoleManager = require('./managers/GuildMemberRoleManager.js').GuildMemberRoleManager;
exports.GuildMessageManager = require('./managers/GuildMessageManager.js').GuildMessageManager;
exports.GuildScheduledEventManager = require('./managers/GuildScheduledEventManager.js').GuildScheduledEventManager;
exports.GuildSoundboardSoundManager = require('./managers/GuildSoundboardSoundManager.js').GuildSoundboardSoundManager;
exports.GuildStickerManager = require('./managers/GuildStickerManager.js').GuildStickerManager;
exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager.js').GuildTextThreadManager;
exports.MessageManager = require('./managers/MessageManager.js').MessageManager;
Expand Down Expand Up @@ -202,6 +203,7 @@ exports.RoleSelectMenuBuilder = require('./structures/RoleSelectMenuBuilder.js')
exports.RoleSelectMenuComponent = require('./structures/RoleSelectMenuComponent.js').RoleSelectMenuComponent;
exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteraction.js').RoleSelectMenuInteraction;
exports.SKU = require('./structures/SKU.js').SKU;
exports.SoundboardSound = require('./structures/SoundboardSound.js').SoundboardSound;
exports.StageChannel = require('./structures/StageChannel.js').StageChannel;
exports.StageInstance = require('./structures/StageInstance.js').StageInstance;
exports.Sticker = require('./structures/Sticker.js').Sticker;
Expand Down
68 changes: 67 additions & 1 deletion packages/discord.js/src/managers/GuildManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ const process = require('node:process');
const { setTimeout, clearTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes, RouteBases } = require('discord-api-types/v10');
const { GatewayOpcodes, Routes, RouteBases } = require('discord-api-types/v10');
const { CachedManager } = require('./CachedManager.js');
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
const { ShardClientUtil } = require('../sharding/ShardClientUtil.js');
const { Guild } = require('../structures/Guild.js');
const { GuildChannel } = require('../structures/GuildChannel.js');
Expand Down Expand Up @@ -282,6 +283,71 @@ class GuildManager extends CachedManager {
return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection());
}

/**
* @typedef {Object} FetchSoundboardSoundsOptions
* @param {Snowflake[]} guildIds The ids of the guilds to fetch soundboard sounds for
* @param {number} [time=10_000] The timeout for receipt of the soundboard sounds
*/

/**
* Fetches soundboard sounds for the specified guilds.
* @param {FetchSoundboardSoundsOptions} options The options for fetching soundboard sounds
* @returns {Promise<Collection<Snowflake, Collection<string, SoundboardSound>>>}
* @example
* // Fetch soundboard sounds for multiple guilds
* const soundboardSounds = await client.guilds.fetchSoundboardSounds({
* guildIds: ['1234567890', '9876543210'],
* })
*
* console.log(soundboardSounds.get('1234567890'));
*/
async fetchSoundboardSounds({ guildIds, time = 10_000 }) {
const shardCount = await this.client.ws.getShardCount();
const shardIds = Map.groupBy(guildIds, guildId => ShardClientUtil.shardIdForGuildId(guildId, shardCount));

for (const [shardId, shardGuildIds] of shardIds) {
this.client.ws.send(shardId, {
op: GatewayOpcodes.RequestSoundboardSounds,
d: {
guild_ids: shardGuildIds,
},
});
}

return new Promise((resolve, reject) => {
const remainingGuildIds = new Set(guildIds);

const fetchedSoundboardSounds = new Collection();

const handler = (soundboardSounds, guild) => {
if (!remainingGuildIds.has(guild.id)) return;

timeout.refresh();

fetchedSoundboardSounds.set(guild.id, soundboardSounds);

remainingGuildIds.delete(guild.id);

if (remainingGuildIds.size === 0) {
clearTimeout(timeout);
this.client.removeListener(Events.SoundboardSounds, handler);
this.client.decrementMaxListeners();

resolve(fetchedSoundboardSounds);
}
};

const timeout = setTimeout(() => {
this.client.removeListener(Events.SoundboardSounds, handler);
this.client.decrementMaxListeners();
reject(new DiscordjsError(ErrorCodes.GuildSoundboardSoundsTimeout));
}, time).unref();

this.client.incrementMaxListeners();
this.client.on(Events.SoundboardSounds, handler);
});
}

/**
* Options used to set incident actions. Supplying `null` to any option will disable the action.
* @typedef {Object} IncidentActionsEditOptions
Expand Down
Loading