From 3e1fe0cec177a5b7d567a66fc4e00528e6b308c3 Mon Sep 17 00:00:00 2001 From: bsian03 Date: Sat, 19 Oct 2024 19:29:27 +0100 Subject: [PATCH 1/7] feat(Soundboard): Add Soundboard Sound functionality Patch adapted to Dysnomia-specific changes, moving a couple things in between. Ref: https://github.com/abalabahaha/eris/commit/b69783e90a1d15e43bd1f276160b1c00bfd7f520 Co-authored-by: Snazzah Co-authored-by: TTtie --- esm.mjs | 1 + index.d.ts | 105 ++++++++++++--- index.js | 1 + lib/Client.js | 97 +++++++++++++- lib/Constants.js | 32 +++-- lib/gateway/Shard.js | 214 +++++++++++++++++++++++++----- lib/rest/Endpoints.js | 5 + lib/structures/Guild.js | 69 ++++++++++ lib/structures/SoundboardSound.js | 105 +++++++++++++++ lib/structures/VoiceChannel.js | 11 ++ 10 files changed, 576 insertions(+), 64 deletions(-) create mode 100644 lib/structures/SoundboardSound.js diff --git a/esm.mjs b/esm.mjs index 718e0faf..6565c81e 100644 --- a/esm.mjs +++ b/esm.mjs @@ -44,6 +44,7 @@ export const { SequentialBucket, Shard, SharedStream, + SoundboardSound, StageChannel, StageInstance, TextChannel, diff --git a/index.d.ts b/index.d.ts index c29192e8..2aa8719c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -114,6 +114,7 @@ declare namespace Dysnomia { type OnboardingPromptTypes = Constants["OnboardingPromptTypes"][keyof Constants["OnboardingPromptTypes"]]; type PossiblyUncachedGuild = Guild | Uncached; type PossiblyUncachedGuildScheduledEvent = GuildScheduledEvent | Uncached; + type PossiblyUncachedGuildSoundboardSound = SoundboardSound | { id: string; guild: PossiblyUncachedGuild }; type PremiumTier = Constants["PremiumTiers"][keyof Constants["PremiumTiers"]]; type VerificationLevel = Constants["VerificationLevels"][keyof Constants["VerificationLevels"]]; type SystemChannelFlags = Constants["SystemChannelFlags"][keyof Constants["SystemChannelFlags"]]; @@ -667,6 +668,13 @@ declare namespace Dysnomia { scheduledStartTime: number; status: GuildScheduledEventStatus; } + interface OldGuildSoundboardSound { + available: boolean; + emojiID: string | null; + emojiName: string | null; + name: string; + volume: number; + } interface OldGuildTextChannel extends OldGuildChannel { nsfw: boolean; rateLimitPerUser: number; @@ -784,6 +792,10 @@ declare namespace Dysnomia { guildScheduledEventUpdate: [event: GuildScheduledEvent, oldEvent: OldGuildScheduledEvent | null]; guildScheduledEventUserAdd: [event: PossiblyUncachedGuildScheduledEvent, user: User | Uncached]; guildScheduledEventUserRemove: [event: PossiblyUncachedGuildScheduledEvent, user: User | Uncached]; + guildSoundboardSoundCreate: [sound: SoundboardSound]; + guildSoundboardSoundDelete: [sound: PossiblyUncachedGuildSoundboardSound]; + guildSoundboardSoundUpdate: [sound: SoundboardSound, oldSound: OldGuildSoundboardSound | null]; + guildSoundboardSoundsUpdate: [guild: PossiblyUncachedGuild, sounds: SoundboardSound[], oldSounds: (OldGuildSoundboardSound | null)[]]; guildStickersUpdate: [guild: PossiblyUncachedGuild, stickers: Sticker[], oldStickers: Sticker[] | null]; guildUnavailable: [guild: UnavailableGuild]; guildUpdate: [guild: Guild, oldGuild: OldGuild]; @@ -809,6 +821,7 @@ declare namespace Dysnomia { rawWS: [packet: RawPacket, id: number]; ready: []; shardPreReady: [id: number]; + soundboardSounds: [guild: PossiblyUncachedGuild, sounds: SoundboardSound[]]; stageInstanceCreate: [stageInstance: StageInstance]; stageInstanceDelete: [stageInstance: StageInstance]; stageInstanceUpdate: [stageInstance: StageInstance, oldStageInstance: OldStageInstance | null]; @@ -909,11 +922,16 @@ declare namespace Dysnomia { url: string; } interface RequestMembersPromise { - members: Member; + members: Member[]; received: number; res: (value: Member[]) => void; timeout: NodeJS.Timeout; } + interface RequestSoundboardSoundsPromise { + res: (value: Record) => void; + soundboardSounds: Record; + timeout: NodeJS.Timeout; + } // Guild interface AddGuildMemberOptions { @@ -1128,6 +1146,23 @@ declare namespace Dysnomia { user: User; member?: Member; } + interface GuildSoundboardSoundBase { + emojiID?: string | null; + emojiName?: string | null; + name?: string; + volume?: number | null; + } + interface GuildSoundboardSoundCreate extends GuildSoundboardSoundBase { + name: string; + sound: string; + } + interface GuildSoundboardSoundEdit extends GuildSoundboardSoundBase { + reason?: string; + } + interface GuildSoundboardSoundSend { + soundID: string; + sourceGuildID?: string; + } interface GuildTemplateOptions { name?: string; description?: string | null; @@ -1409,6 +1444,10 @@ declare namespace Dysnomia { username: string; public_flags?: number; } + interface RequestGuildSoundboardSoundsOptions { + guildIDs: string[]; + timeout?: number; + } // Message interface ActionRow { @@ -2152,7 +2191,7 @@ declare namespace Dysnomia { GROUP_DM: 3; GUILD_CATEGORY: 4; GUILD_ANNOUNCEMENT: 5; - + // (undocumented types skipped) ANNOUNCEMENT_THREAD: 10; PUBLIC_THREAD: 11; PRIVATE_THREAD: 12; @@ -2209,20 +2248,20 @@ declare namespace Dysnomia { GALLERY_VIEW: 2; }; GatewayOPCodes: { - DISPATCH: 0; - HEARTBEAT: 1; - IDENTIFY: 2; - PRESENCE_UPDATE: 3; - VOICE_STATE_UPDATE: 4; - VOICE_SERVER_PING: 5; - RESUME: 6; - RECONNECT: 7; - REQUEST_GUILD_MEMBERS: 8; - INVALID_SESSION: 9; - HELLO: 10; - HEARTBEAT_ACK: 11; - SYNC_GUILD: 12; - SYNC_CALL: 13; + DISPATCH: 0; + HEARTBEAT: 1; + IDENTIFY: 2; + PRESENCE_UPDATE: 3; + VOICE_STATE_UPDATE: 4; + VOICE_SERVER_PING: 5; + RESUME: 6; + RECONNECT: 7; + REQUEST_GUILD_MEMBERS: 8; + INVALID_SESSION: 9; + HELLO: 10; + HEARTBEAT_ACK: 11; + // (undocumented op codes skipped) + REQUEST_SOUNDBOARD_SOUNDS: 31; }; GuildFeatures: [ "ANIMATED_BANNER", @@ -2284,6 +2323,8 @@ declare namespace Dysnomia { guilds: 1; guildMembers: 2; guildModeration: 4; + guildExpressions: 8; + /** @deprecated */ guildEmojisAndStickers: 8; guildIntegrations: 16; guildWebhooks: 32; @@ -2888,6 +2929,7 @@ declare namespace Dysnomia { createGuildEmoji(guildID: string, options: EmojiOptions, reason?: string): Promise; createGuildFromTemplate(code: string, name: string, icon?: string): Promise; createGuildScheduledEvent(guildID: string, event: GuildScheduledEventOptions, reason?: string): Promise>; + createGuildSoundboardSound(guildID: string, sound: GuildSoundboardSoundCreate, reason?: string): Promise; createGuildSticker(guildID: string, options: CreateStickerOptions, reason?: string): Promise; createGuildTemplate(guildID: string, name: string, description?: string | null): Promise; createInteractionResponse(interactionID: string, interactionToken: string, options: T, file?: FileContent | FileContent[]): Promise : void>; @@ -2909,6 +2951,7 @@ declare namespace Dysnomia { deleteGuildEmoji(guildID: string, emojiID: string, reason?: string): Promise; deleteGuildIntegration(guildID: string, integrationID: string): Promise; deleteGuildScheduledEvent(guildID: string, eventID: string): Promise; + deleteGuildSoundboardSound(guildID: string, soundID: string, reason?: string): Promise; deleteGuildSticker(guildID: string, stickerID: string, reason?: string): Promise; deleteGuildTemplate(guildID: string, code: string): Promise; deleteInvite(inviteID: string, reason?: string): Promise; @@ -2954,6 +2997,7 @@ declare namespace Dysnomia { editGuildMFALevel(guildID: string, options: EditGuildMFALevelOptions): Promise; editGuildOnboarding(guildID: string, options: EditGuildOnboardingOptions): Promise; editGuildScheduledEvent(guildID: string, eventID: string, event: GuildScheduledEventEditOptions, reason?: string): Promise>; + editGuildSoundboardSound(guildID: string, soundID: string, options: GuildSoundboardSoundEdit): Promise; editGuildSticker(guildID: string, stickerID: string, options?: EditStickerOptions, reason?: string): Promise; editGuildTemplate(guildID: string, code: string, options: GuildTemplateOptions): Promise; editGuildVoiceState(guildID: string, options: VoiceStateOptions, userID?: string): Promise; @@ -3017,6 +3061,8 @@ declare namespace Dysnomia { getGuildPreview(guildID: string): Promise; getGuildScheduledEvents(guildID: string, options?: GetGuildScheduledEventOptions): Promise; getGuildScheduledEventUsers(guildID: string, eventID: string, options?: GetGuildScheduledEventUsersOptions): Promise; + getGuildSoundboardSound(guildID: string, soundID: string): Promise; + getGuildSoundboardSounds(guildID: string): Promise; getGuildTemplate(code: string): Promise; getGuildTemplates(guildID: string): Promise; getGuildVanity(guildID: string): Promise; @@ -3054,6 +3100,7 @@ declare namespace Dysnomia { getRoleConnectionMetadata(): Promise; getSelf(): Promise; getSKUs(): Promise; + getSoundboardSounds(): Promise[]>; getStageInstance(channelID: string): Promise; getStickerPack(packID: string): Promise; getStickerPacks(): Promise<{ sticker_packs: StickerPack[] }>; @@ -3081,6 +3128,7 @@ declare namespace Dysnomia { removeMessageReactions(channelID: string, messageID: string): Promise; searchGuildMembers(guildID: string, query: string, limit?: number): Promise; sendChannelTyping(channelID: string): Promise; + sendSoundboardSound(channelID: string, options: GuildSoundboardSoundSend): Promise; syncGuildIntegration(guildID: string, integrationID: string): Promise; syncGuildTemplate(guildID: string, code: string): Promise; unbanGuildMember(guildID: string, userID: string, reason?: string): Promise; @@ -3259,6 +3307,7 @@ declare namespace Dysnomia { rulesChannelID: string | null; safetyAlertsChannelID: string | null; shard: Shard; + soundboardSounds: Collection; splash: string | null; splashURL: string | null; stageInstances: Collection; @@ -3294,6 +3343,7 @@ declare namespace Dysnomia { createRole(options: RoleOptions, reason?: string): Promise; createRole(options: Role, reason?: string): Promise; createScheduledEvent(event: GuildScheduledEventOptions, reason?: string): Promise>; + createSoundboardSound(sound: GuildSoundboardSoundCreate, reason?: string): Promise; createSticker(options: CreateStickerOptions, reason?: string): Promise; createTemplate(name: string, description?: string | null): Promise; delete(): Promise; @@ -3303,6 +3353,7 @@ declare namespace Dysnomia { deleteIntegration(integrationID: string): Promise; deleteRole(roleID: string): Promise; deleteScheduledEvent(eventID: string): Promise; + deleteSoundboardSound(soundID: string, reason?: string): Promise; deleteSticker(stickerID: string, reason?: string): Promise; deleteTemplate(code: string): Promise; dynamicBannerURL(format?: ImageFormat, size?: number): string | null; @@ -3327,6 +3378,7 @@ declare namespace Dysnomia { editWelcomeScreen(options: WelcomeScreenOptions): Promise; editWidget(options: Partial & { reason?: string }): Promise; fetchMembers(options?: FetchMembersOptions): Promise; + fetchSoundboardSounds(options?: Omit): Promise; getActiveThreads(): Promise; getAuditLog(options?: GetGuildAuditLogOptions): Promise; getAutoModerationRule(guildID: string, ruleID: string): Promise; @@ -3353,6 +3405,8 @@ declare namespace Dysnomia { getRESTVoiceState(userID?: string): Promise; getScheduledEvents(options?: GetGuildScheduledEventOptions): Promise; getScheduledEventUsers(eventID: string, options?: GetGuildScheduledEventUsersOptions): Promise; + getSoundboardSound(soundID: string): Promise; + getSoundboardSounds(): Promise; getTemplates(): Promise; getVanity(): Promise; getVoiceRegions(): Promise; @@ -3857,7 +3911,8 @@ declare namespace Dysnomia { presenceUpdateBucket: Bucket; ready: boolean; reconnectInterval: number; - requestMembersPromise: { [s: string]: RequestMembersPromise }; + requestMembersPromise: Record; + requestSoundboardSoundsPromise: Record; resumeURL: string | null; seq: number; sessionID: string | null; @@ -3885,6 +3940,7 @@ declare namespace Dysnomia { once(event: string, listener: (...args: any[]) => void): this; onPacket(packet: RawPacket): void; requestGuildMembers(guildID: string, options?: FetchMembersOptions): Promise; + requestGuildSoundboardSounds(options: RequestGuildSoundboardSoundsOptions): Promise>; reset(): void; restartGuildCreateTimeout(): void; resume(): void; @@ -3938,6 +3994,20 @@ declare namespace Dysnomia { on(event: string, listener: (...args: any[]) => void): this; } + export class SoundboardSound extends Base { + available: G extends false ? true : boolean; + emojiID: G extends false ? null : string | null; + emojiName: G extends false ? string : string | null; + guild: G extends false ? never : PossiblyUncachedGuild; + name: string; + user: G extends false ? never : User | undefined; + volume: number; + constructor(data: BaseData, client: Client); + delete(reason?: string): Promise; + edit(options: GuildSoundboardSoundEdit): Promise; + send(channelID: string): Promise; + } + export class StageChannel extends TextVoiceChannel { topic?: string; type: Constants["ChannelTypes"]["GUILD_STAGE_VOICE"]; @@ -4119,6 +4189,7 @@ declare namespace Dysnomia { getInvites(): Promise<(Invite<"withMetadata", VoiceChannel>)[]>; join(options?: JoinVoiceChannelOptions): Promise; leave(): void; + sendSoundboardSound(options: GuildSoundboardSoundSend): Promise; } export class VoiceConnection extends EventEmitter implements SimpleJSON { diff --git a/index.js b/index.js index 981a006a..69e4de2f 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,7 @@ Dysnomia.Role = require("./lib/structures/Role"); Dysnomia.SequentialBucket = require("./lib/util/SequentialBucket"); Dysnomia.Shard = require("./lib/gateway/Shard"); Dysnomia.SharedStream = require("./lib/voice/SharedStream"); +Dysnomia.SoundboardSound = require("./lib/structures/SoundboardSound"); Dysnomia.StageChannel = require("./lib/structures/StageChannel"); Dysnomia.StageInstance = require("./lib/structures/StageInstance"); Dysnomia.TextChannel = require("./lib/structures/TextChannel"); diff --git a/lib/Client.js b/lib/Client.js index ff78dd61..8f8cf534 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -1,11 +1,13 @@ "use strict"; const ApplicationCommand = require("./structures/ApplicationCommand"); +const AutoModerationRule = require("./structures/AutoModerationRule"); const Base = require("./structures/Base"); const Channel = require("./structures/Channel"); const Collection = require("./util/Collection"); const Constants = require("./Constants"); const Endpoints = require("./rest/Endpoints"); +const Entitlement = require("./structures/Entitlement"); const ExtendedUser = require("./structures/ExtendedUser"); const Guild = require("./structures/Guild"); const GuildAuditLogEntry = require("./structures/GuildAuditLogEntry"); @@ -21,15 +23,15 @@ const PrivateChannel = require("./structures/PrivateChannel"); const RequestHandler = require("./rest/RequestHandler"); const Role = require("./structures/Role"); const ShardManager = require("./gateway/ShardManager"); +const SoundboardSound = require("./structures/SoundboardSound"); const StageInstance = require("./structures/StageInstance"); const ThreadMember = require("./structures/ThreadMember"); const UnavailableGuild = require("./structures/UnavailableGuild"); const User = require("./structures/User"); const VoiceConnectionManager = require("./voice/VoiceConnectionManager"); -const AutoModerationRule = require("./structures/AutoModerationRule"); -const emitDeprecation = require("./util/emitDeprecation"); const VoiceState = require("./structures/VoiceState"); -const Entitlement = require("./structures/Entitlement"); + +const emitDeprecation = require("./util/emitDeprecation"); let EventEmitter; try { @@ -747,6 +749,25 @@ class Client extends EventEmitter { }).then((data) => new GuildScheduledEvent(data, this)); } + /** + * Create a guild soundboard sound + * @param {String} guildID The guild ID where the sound will be created + * @param {Object} sound The sound to be created + * @param {String?} [sound.emojiID] The ID of the relating custom emoji (mutually exclusive with sound.emojiName) + * @param {String?} [sound.emojiName] The name of the relating default emoji (mutually exclusive with sound.emojiID) + * @param {String} sound.name The name of the soundboard sound (2-32 characters) + * @param {String} sound.sound The base 64 encoded mp3/ogg sound data + * @param {Number?} [sound.volume=1] The volume of the soundboard sound, between 0 and 1 + * @param {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + createGuildSoundboardSound(guildID, sound, reason) { + sound.emoji_id = sound.emojiID; + sound.emoji_name = sound.emojiName; + sound.reason = reason; + return this.requestHandler.request("POST", Endpoints.SOUNDBOARD_SOUNDS_GUILD(guildID), true, sound).then((sound) => new SoundboardSound(sound, this)); + } + /** * Create a guild sticker * @param {String} guildID The guild to create a sticker in @@ -1136,6 +1157,17 @@ class Client extends EventEmitter { return this.requestHandler.request("DELETE", Endpoints.GUILD_SCHEDULED_EVENT(guildID, eventID), true); } + /** + * Delete a guild soundboard sound + * @param {String} guildID The ID of the guild + * @param {String} soundID The ID of the soundboard sound + * @param {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + deleteGuildSoundboardSound(guildID, soundID, reason) { + return this.requestHandler.request("DELETE", Endpoints.SOUNDBOARD_SOUND_GUILD(guildID, soundID), true, {reason}); + } + /** * Delete a guild sticker * @param {String} guildID The ID of the guild @@ -1719,6 +1751,24 @@ class Client extends EventEmitter { }).then((data) => new GuildScheduledEvent(data, this)); } + /** + * Edit a guild soundboard sound + * @param {String} guildID The guild ID where the sound will be edited + * @param {String} soundID The ID of the soundboard sound + * @param {Object} options The properties to edit + * @param {String?} [options.emojiID] The ID of the relating custom emoji (mutually exclusive with options.emojiName) + * @param {String?} [options.emojiName] The name of the relating default emoji (mutually exclusive with options.emojiID) + * @param {String} [options.name] The name of the soundboard sound (2-32 characters) + * @param {Number?} [options.volume] The volume of the soundboard sound, between 0 and 1 + * @param {String} [options.reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + editGuildSoundboardSound(guildID, soundID, options) { + options.emoji_id = options.emojiID; + options.emoji_name = options.emojiName; + return this.requestHandler.request("PATCH", Endpoints.SOUNDBOARD_SOUND_GUILD(guildID, soundID), true, options).then((sound) => new SoundboardSound(sound, this)); + } + /** * Edit a guild sticker * @param {String} stickerID The ID of the sticker @@ -2539,6 +2589,25 @@ class Client extends EventEmitter { })); } + /** + * Get a guild soundboard sound. Not to be confused with getGuildSoundboardSounds, which gets all soundboard sounds in a guild + * @param {String} guildID The guild ID where the sound was created + * @param {String} soundID The ID of the soundboard sound + * @returns {Promise} + */ + getGuildSoundboardSound(guildID, soundID) { + return this.requestHandler.request("GET", Endpoints.SOUNDBOARD_SOUND_GUILD(guildID, soundID), true).then((sound) => new SoundboardSound(sound, this)); + } + + /** + * Get a guild's soundboard sounds. Not to be confused with getGuildSoundboardSound, which gets a specified soundboard sound + * @param {String} guildID The ID of the guild + * @returns {Promise>} + */ + getGuildSoundboardSounds(guildID) { + return this.requestHandler.request("GET", Endpoints.SOUNDBOARD_SOUNDS_GUILD(guildID), true).then((sounds) => sounds.map((sound) => new SoundboardSound(sound, this))); + } + /** * Get a guild template * @param {String} code The template code @@ -3009,6 +3078,14 @@ class Client extends EventEmitter { return this.requestHandler.request("GET", Endpoints.SKUS(this.application.id), true); } + /** + * Get the default soundboard sounds + * @returns {Promise>} + */ + getSoundboardSounds() { + return this.requestHandler.request("GET", Endpoints.SOUNDBOARD_SOUNDS_DEFAULT, true).then((sounds) => sounds.map((sound) => new SoundboardSound(sound, this))); + } + /** * Get the stage instance associated with a stage channel * @param {String} channelID The stage channel ID @@ -3359,6 +3436,20 @@ class Client extends EventEmitter { return this.requestHandler.request("POST", Endpoints.CHANNEL_TYPING(channelID), true); } + /** + * Send a soundboard sound to a connected voice channel + * @param {String} channelID The ID of the connected voice channel + * @param {Object} options The soundboard sound options + * @param {String} options.soundID The ID of the soundboard sound + * @param {String} [options.sourceGuildID] The ID of the guild where the soundboard sound was created, if not in the same guild + * @returns {Promise} + */ + sendSoundboardSound(channelID, options) { + options.sound_id = options.soundID; + options.source_guild_id = options.sourceGuildID; + return this.requestHandler.request("POST", Endpoints.SOUNDBOARD_SOUNDS_SEND(channelID), true, options); + } + /** * Force a guild template to sync * @param {String} guildID The ID of the guild diff --git a/lib/Constants.js b/lib/Constants.js index 4ee4577a..91ce5199 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -212,7 +212,7 @@ module.exports.ChannelTypes = { GROUP_DM: 3, GUILD_CATEGORY: 4, GUILD_ANNOUNCEMENT: 5, - // (undocumented types) + // (undocumented types skipped) ANNOUNCEMENT_THREAD: 10, PUBLIC_THREAD: 11, PRIVATE_THREAD: 12, @@ -272,18 +272,20 @@ module.exports.ForumLayoutTypes = { }; module.exports.GatewayOPCodes = { - DISPATCH: 0, - HEARTBEAT: 1, - IDENTIFY: 2, - PRESENCE_UPDATE: 3, - VOICE_STATE_UPDATE: 4, - VOICE_SERVER_PING: 5, - RESUME: 6, - RECONNECT: 7, - REQUEST_GUILD_MEMBERS: 8, - INVALID_SESSION: 9, - HELLO: 10, - HEARTBEAT_ACK: 11 + DISPATCH: 0, + HEARTBEAT: 1, + IDENTIFY: 2, + PRESENCE_UPDATE: 3, + VOICE_STATE_UPDATE: 4, + VOICE_SERVER_PING: 5, + RESUME: 6, + RECONNECT: 7, + REQUEST_GUILD_MEMBERS: 8, + INVALID_SESSION: 9, + HELLO: 10, + HEARTBEAT_ACK: 11, + // (undocumented op codes skipped) + REQUEST_SOUNDBOARD_SOUNDS: 31 }; module.exports.GuildFeatures = [ @@ -351,6 +353,8 @@ const Intents = { guilds: 1 << 0, guildMembers: 1 << 1, guildModeration: 1 << 2, + guildExpressions: 1 << 3, + /** @deprecated */ guildEmojisAndStickers: 1 << 3, guildIntegrations: 1 << 4, guildWebhooks: 1 << 5, @@ -374,7 +378,7 @@ const Intents = { Intents.allNonPrivileged = Intents.guilds | Intents.guildModeration - | Intents.guildEmojisAndStickers + | Intents.guildExpressions | Intents.guildIntegrations | Intents.guildWebhooks | Intents.guildInvites diff --git a/lib/gateway/Shard.js b/lib/gateway/Shard.js index c4ffbbbb..f0aa6937 100644 --- a/lib/gateway/Shard.js +++ b/lib/gateway/Shard.js @@ -1,26 +1,26 @@ "use strict"; const util = require("node:util"); +const AutoModerationRule = require("../structures/AutoModerationRule"); const Base = require("../structures/Base"); const Bucket = require("../util/Bucket"); const Channel = require("../structures/Channel"); +const Constants = require("../Constants"); +const Entitlement = require("../structures/Entitlement"); +const ExtendedUser = require("../structures/ExtendedUser"); +const ForumChannel = require("../structures/ForumChannel"); +const GuildAuditLogEntry = require("../structures/GuildAuditLogEntry"); const GuildChannel = require("../structures/GuildChannel"); +const GuildScheduledEvent = require("../structures/GuildScheduledEvent"); +const GuildIntegration = require("../structures/GuildIntegration"); +const Invite = require("../structures/Invite"); +const Interaction = require("../structures/Interaction"); const Message = require("../structures/Message"); const PrivateChannel = require("../structures/PrivateChannel"); -const {GATEWAY_VERSION, GatewayOPCodes, ChannelTypes} = require("../Constants"); -const ExtendedUser = require("../structures/ExtendedUser"); const User = require("../structures/User"); -const Invite = require("../structures/Invite"); -const Interaction = require("../structures/Interaction"); -const Constants = require("../Constants"); -const ThreadChannel = require("../structures/ThreadChannel"); +const SoundboardSound = require("../structures/SoundboardSound"); const StageInstance = require("../structures/StageInstance"); -const GuildScheduledEvent = require("../structures/GuildScheduledEvent"); -const GuildAuditLogEntry = require("../structures/GuildAuditLogEntry"); -const AutoModerationRule = require("../structures/AutoModerationRule"); -const GuildIntegration = require("../structures/GuildIntegration"); -const ForumChannel = require("../structures/ForumChannel"); -const Entitlement = require("../structures/Entitlement"); +const ThreadChannel = require("../structures/ThreadChannel"); const WebSocket = typeof window !== "undefined" ? require("../util/BrowserWebSocket") : require("ws"); @@ -292,7 +292,7 @@ class Shard extends EventEmitter { * @type {Number} */ this.lastHeartbeatSent = Date.now(); - this.sendWS(GatewayOPCodes.HEARTBEAT, this.seq, true); + this.sendWS(Constants.GatewayOPCodes.HEARTBEAT, this.seq, true); } identify() { @@ -313,7 +313,7 @@ class Shard extends EventEmitter { this.status = "identifying"; const identify = { token: this.#token, - v: GATEWAY_VERSION, + v: Constants.GATEWAY_VERSION, compress: !!this.client.shards.options.compress, large_threshold: this.client.shards.options.largeThreshold, intents: this.client.shards.options.intents, @@ -329,7 +329,7 @@ class Shard extends EventEmitter { if(this.presence.status) { identify.presence = this.presence; } - this.sendWS(GatewayOPCodes.IDENTIFY, identify); + this.sendWS(Constants.GatewayOPCodes.IDENTIFY, identify); } initializeWS() { @@ -391,17 +391,17 @@ class Shard extends EventEmitter { } switch(packet.op) { - case GatewayOPCodes.DISPATCH: { + case Constants.GatewayOPCodes.DISPATCH: { if(!this.client.shards.options.disableEvents[packet.t]) { this.wsEvent(packet); } break; } - case GatewayOPCodes.HEARTBEAT: { + case Constants.GatewayOPCodes.HEARTBEAT: { this.heartbeat(); break; } - case GatewayOPCodes.INVALID_SESSION: { + case Constants.GatewayOPCodes.INVALID_SESSION: { this.seq = 0; this.sessionID = null; this.resumeURL = null; @@ -409,14 +409,14 @@ class Shard extends EventEmitter { this.identify(); break; } - case GatewayOPCodes.RECONNECT: { + case Constants.GatewayOPCodes.RECONNECT: { this.emit("debug", "Reconnecting due to server request", this.id); this.disconnect({ reconnect: "auto" }); break; } - case GatewayOPCodes.HELLO: { + case Constants.GatewayOPCodes.HELLO: { if(packet.d.heartbeat_interval > 0) { if(this.heartbeatInterval) { clearInterval(this.heartbeatInterval); @@ -451,7 +451,7 @@ class Shard extends EventEmitter { this.emit("hello", packet.d._trace, this.id); break; } - case GatewayOPCodes.HEARTBEAT_ACK: { + case Constants.GatewayOPCodes.HEARTBEAT_ACK: { this.lastHeartbeatAck = true; /** * Last time Discord acknowledged a heartbeat, null if shard has not sent heartbeat yet @@ -493,7 +493,7 @@ class Shard extends EventEmitter { if(opts.user_ids?.length > 100) { throw new Error("Cannot request more than 100 users by their ID"); } - this.sendWS(GatewayOPCodes.REQUEST_GUILD_MEMBERS, opts); + this.sendWS(Constants.GatewayOPCodes.REQUEST_GUILD_MEMBERS, opts); return new Promise((res) => this.requestMembersPromise[opts.nonce] = { res: res, received: 0, @@ -505,6 +505,26 @@ class Shard extends EventEmitter { }); } + requestGuildSoundboardSounds(options) { + const opts = { + guild_ids: options.guildIDs + }; + const soundboardSounds = options.guildIDs.reduce((obj, key) => { + obj[key] = undefined; + return obj; + }, {}); + const nonce = Date.now().toString() + Math.random().toString(36); + this.sendWS(Constants.GatewayOPCodes.REQUEST_SOUNDBOARD_SOUNDS, opts); + return new Promise((res) => this.requestSoundboardSoundsPromise[nonce] = { + res: res, + soundboardSounds: soundboardSounds, + timeout: setTimeout(() => { + res(this.requestSoundboardSoundsPromise[nonce].soundboardSounds); + delete this.requestSoundboardSoundsPromise[nonce]; + }, (options && options.timeout) || this.client.options.requestTimeout) + }); + } + reset() { this.connecting = false; this.ready = false; @@ -519,6 +539,7 @@ class Shard extends EventEmitter { } } this.requestMembersPromise = {}; + this.requestSoundboardSoundsPromise = {}; this.getAllUsersCount = {}; this.latency = Infinity; this.lastHeartbeatAck = true; @@ -548,7 +569,7 @@ class Shard extends EventEmitter { resume() { this.status = "resuming"; - this.sendWS(GatewayOPCodes.RESUME, { + this.sendWS(Constants.GatewayOPCodes.RESUME, { token: this.#token, session_id: this.sessionID, seq: this.seq @@ -556,7 +577,7 @@ class Shard extends EventEmitter { } sendStatusUpdate() { - this.sendWS(GatewayOPCodes.PRESENCE_UPDATE, { + this.sendWS(Constants.GatewayOPCodes.PRESENCE_UPDATE, { activities: this.presence.activities, afk: !!this.presence.afk, since: this.presence.status === "idle" ? Date.now() : null, @@ -578,7 +599,7 @@ class Shard extends EventEmitter { this.emit("debug", JSON.stringify({op: op, d: _data}), this.id); } }; - if(op === GatewayOPCodes.PRESENCE_UPDATE) { + if(op === Constants.GatewayOPCodes.PRESENCE_UPDATE) { ++waitFor; this.presenceUpdateBucket.queue(func, priority); } @@ -770,7 +791,7 @@ class Shard extends EventEmitter { packet.d.member.id = packet.d.user_id; member = guild.members.add(packet.d.member, guild); - const channel = guild.channels.find((channel) => (channel.type === ChannelTypes.GUILD_VOICE || channel.type === ChannelTypes.GUILD_STAGE_VOICE) && channel.voiceMembers.get(packet.d.id)); + const channel = guild.channels.find((channel) => (channel.type === Constants.ChannelTypes.GUILD_VOICE || channel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE) && channel.voiceMembers.get(packet.d.id)); if(channel) { channel.voiceMembers.remove(packet.d); this.emit("debug", "VOICE_STATE_UPDATE member null but in channel: " + packet.d.id, this.id); @@ -790,12 +811,12 @@ class Shard extends EventEmitter { let oldChannel, newChannel; if(oldChannelID) { oldChannel = guild.channels.get(oldChannelID); - if(oldChannel && oldChannel.type !== ChannelTypes.GUILD_VOICE && oldChannel.type !== ChannelTypes.GUILD_STAGE_VOICE) { + if(oldChannel && oldChannel.type !== Constants.ChannelTypes.GUILD_VOICE && oldChannel.type !== Constants.ChannelTypes.GUILD_STAGE_VOICE) { this.emit("warn", "Old channel not a recognized voice channel: " + oldChannelID, this.id); oldChannel = null; } } - if(packet.d.channel_id && (newChannel = guild.channels.get(packet.d.channel_id)) && (newChannel.type === ChannelTypes.GUILD_VOICE || newChannel.type === ChannelTypes.GUILD_STAGE_VOICE)) { // Welcome to Discord, where one can "join" text channels + if(packet.d.channel_id && (newChannel = guild.channels.get(packet.d.channel_id)) && (newChannel.type === Constants.ChannelTypes.GUILD_VOICE || newChannel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE)) { // Welcome to Discord, where one can "join" text channels if(oldChannel) { /** * Fired when a guild member switches voice channels @@ -1666,7 +1687,7 @@ class Shard extends EventEmitter { break; } case "CHANNEL_DELETE": { - if(packet.d.type === ChannelTypes.DM || packet.d.type === undefined) { + if(packet.d.type === Constants.ChannelTypes.DM || packet.d.type === undefined) { if(this.id === 0) { const channel = this.client.privateChannels.remove(packet.d); if(channel) { @@ -1690,7 +1711,7 @@ class Shard extends EventEmitter { if(!channel) { break; } - if(channel.type === ChannelTypes.GUILD_VOICE || channel.type === ChannelTypes.GUILD_STAGE_VOICE) { + if(channel.type === Constants.ChannelTypes.GUILD_VOICE || channel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE) { channel.voiceMembers.forEach((member) => { channel.voiceMembers.remove(member); this.emit("voiceChannelLeave", member, channel); @@ -2423,6 +2444,139 @@ class Shard extends EventEmitter { this.emit("entitlementDelete", new Entitlement(packet.d, this.client)); break; } + case "GUILD_SOUNDBOARD_SOUND_CREATE": { + packet.d.id = packet.d.sound_id; + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("guildSoundboardSoundCreate", new SoundboardSound(packet.d, this.client)); + break; + } + /** + * Fired when a guild soundboard sound is created + * @event Client#guildSoundboardSoundCreate + * @prop {SoundboardSound} sound The created soundboard sound + */ + this.emit("guildSoundboardSoundCreate", guild.soundboardSounds.add(packet.d, this.client)); + break; + } + case "GUILD_SOUNDBOARD_SOUND_DELETE": { + packet.d.id = packet.d.sound_id; + const guild = this.client.guilds.get(packet.d.guild_id); + /** + * Fired when a guild soundboard sound is deleted + * @event Client#guildSoundboardSoundDelete + * @prop {SoundboardSound} sound The deleted soundboard sound. If the soundboard sound isn't cached, this will be an object with `id` and `guild` keys. If the guild isn't cached, it will be an object with an `id` key. No other properties are guaranteed + */ + this.emit("guildSoundboardSoundDelete", (guild && guild.soundboardSounds.remove(packet.d)) || { + id: packet.d.id, + guild: guild || { + id: packet.d.guild_id + } + }); + break; + } + case "GUILD_SOUNDBOARD_SOUND_UPDATE": { + packet.d.id = packet.d.sound_id; + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("guildSoundboardSoundUpdate", new SoundboardSound(packet.d, this.client), null); + break; + } + + const sound = guild.soundboardSounds.get(packet.d.id); + let oldSound = null; + if(sound) { + oldSound = { + name: sound.name, + volume: sound.volume, + emojiID: sound.emojiID, + emojiName: sound.emojiName, + available: sound.available + }; + } + + /** + * Fired when a guild soundboard sound is updated + * @event Client#guildSoundboardSoundUpdate + * @prop {SoundboardSound} sound The updated soundboard sound + * @prop {Object} oldSound The old soundboard sound data, or null if not cached + * @prop {Boolean} oldSound.available Whether the soundboard sound was available or not + * @prop {String?} oldSound.emojiID The ID of the relating custom emoji + * @prop {String?} oldSound.emojiName The name of the relating default emoji + * @prop {String} oldSound.name The name of the soundboard sound + * @prop {Number} oldSound.volume The volume of the soundboard sound, between 0 and 1 + */ + this.emit("guildSoundboardSoundUpdate", guild.soundboardSounds.update(packet.d, this.client), oldSound); + break; + } + case "GUILD_SOUNDBOARD_SOUNDS_UPDATE": { + packet.d.soundboard_sounds = packet.d.soundboard_sounds.map((sound) => { + sound.id = sound.sound_id; + return sound; + }); + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("guildSoundboardSoundsUpdate", {id: packet.d.guild_id}, packet.d.soundboard_sounds.map((sound) => new SoundboardSound(sound, this.client)), null); + break; + } + + const oldSounds = packet.d.soundboard_sounds.map((_sound) => { + const sound = guild.soundboardSounds.get(_sound.id); + if(!sound) { + return null; + } + return { + name: sound.name, + volume: sound.volume, + emojiID: sound.emojiID, + emojiName: sound.emojiName, + available: sound.available + }; + }); + + /** + * Fired when multiple guild soundboard sounds are updated + * @event Client#guildSoundboardSoundsUpdate + * @prop {Guild} guild The guild. If the guild is uncached, this is an object with an ID key. No other property is guaranteed + * @prop {Array} sounds The updated soundboard sounds + * @prop {Array} oldSounds The old soundboard sounds data, or null if not cached + * @prop {Boolean} oldSounds[].available Whether the soundboard sound was available or not + * @prop {String?} oldSounds[].emojiID The ID of the relating custom emoji + * @prop {String?} oldSounds[].emojiName The name of the relating default emoji + * @prop {String} oldSounds[].name The name of the soundboard sound + * @prop {Number} oldSounds[].volume The volume of the soundboard sound, between 0 and 1 + */ + this.emit("guildSoundboardSoundsUpdate", guild, packet.d.soundboard_sounds.map((sound) => guild.soundboardSounds.update(sound, this.client)), oldSounds); + break; + } + case "SOUNDBOARD_SOUNDS": { + const guild = this.client.guilds.get(packet.d.guild_id); + const sounds = packet.d.soundboard_sounds.map((sound) => { + sound.id = sound.sound_id; + const s = guild ? guild.soundboardSounds.update(sound, this.client) : new SoundboardSound(sound, this.client); + return s; + }); + + /** + * Fired when Discord sends a guild's soundboard sounds. Sent in response to `fetchSoundboardSounds()` + * @prop {Guild} guild The ID of the guild. If the guild is uncached, this will be an object with an `id` key. No other properties are guaranteed + * @prop {Array} sounds The guild's soundboard sounds + */ + this.emit("soundboardSounds", guild || {id: packet.d.guild_id}, sounds); + this.lastHeartbeatAck = true; + + for(const nonce in this.requestSoundboardSoundsPromise) { + if(packet.d.guild_id in this.requestSoundboardSoundsPromise[nonce].soundboardSounds) { + this.requestSoundboardSoundsPromise[nonce].soundboardSounds[packet.d.guild_id] = sounds; + if(Object.values(this.requestSoundboardSoundsPromise[nonce].soundboardSounds).every((v) => v !== undefined)) { + clearTimeout(this.requestSoundboardSoundsPromise[nonce].timeout); + this.requestSoundboardSoundsPromise[nonce].res(this.requestSoundboardSoundsPromise[nonce].soundboardSounds); + delete this.requestSoundboardSoundsPromise[nonce]; + } + } + } + break; + } default: { /** * Fired when the shard encounters an unknown packet diff --git a/lib/rest/Endpoints.js b/lib/rest/Endpoints.js index 0150d82e..b21f14bc 100644 --- a/lib/rest/Endpoints.js +++ b/lib/rest/Endpoints.js @@ -88,6 +88,10 @@ module.exports.INVITE = (inviteID) module.exports.OAUTH2_APPLICATION = "/oauth2/applications/@me"; module.exports.ROLE_CONNECTION_METADATA = (applicationID) => `/applications/${applicationID}/role-connections/metadata`; module.exports.SKUS = (applicationID) => `/applications/${applicationID}/skus`; +module.exports.SOUNDBOARD_SOUND_GUILD = (guildID, soundID) => `/guilds/${guildID}/soundboard-sounds/${soundID}`; +module.exports.SOUNDBOARD_SOUNDS_DEFAULT = "/soundboard-default-sounds"; +module.exports.SOUNDBOARD_SOUNDS_GUILD = (guildID) => `/guilds/${guildID}/soundboard-sounds`; +module.exports.SOUNDBOARD_SOUNDS_SEND = (channelID) => `/channels/${channelID}/send-soundboard-sound`; module.exports.STAGE_INSTANCE = (channelID) => `/stage-instances/${channelID}`; module.exports.STAGE_INSTANCES = "/stage-instances"; module.exports.STICKER = (stickerID) => `/stickers/${stickerID}`; @@ -124,6 +128,7 @@ module.exports.GUILD_ICON = (guildID, guildIcon) module.exports.GUILD_SCHEDULED_EVENT_COVER = (eventID, eventIcon) => `/guild-events/${eventID}/${eventIcon}`; module.exports.GUILD_SPLASH = (guildID, guildSplash) => `/splashes/${guildID}/${guildSplash}`; module.exports.ROLE_ICON = (roleID, roleIcon) => `/role-icons/${roleID}/${roleIcon}`; +module.exports.SOUNDBOARD_SOUNDS = (soundID) => `soundboard-sounds/${soundID}`; module.exports.TEAM_ICON = (teamID, teamIcon) => `/team-icons/${teamID}/${teamIcon}`; module.exports.USER_AVATAR = (userID, userAvatar) => `/avatars/${userID}/${userAvatar}`; module.exports.USER_AVATAR_DECORATION_PRESET = (userDecoration) => `/avatar-decoration-presets/${userDecoration}`, diff --git a/lib/structures/Guild.js b/lib/structures/Guild.js index d8ea315c..583bc7f4 100644 --- a/lib/structures/Guild.js +++ b/lib/structures/Guild.js @@ -13,6 +13,7 @@ const GuildScheduledEvent = require("./GuildScheduledEvent"); const {Permissions} = require("../Constants"); const StageInstance = require("./StageInstance"); const ThreadChannel = require("./ThreadChannel"); +const SoundboardSound = require("./SoundboardSound"); /** * Represents a guild @@ -40,6 +41,11 @@ class Guild extends Base { * @type {Collection} */ roles = new Collection(Role); + /** + * Collection of Soundboard Sounds in the guild + * @type {Collection} + */ + soundboardSounds = new Collection(SoundboardSound); /** * Collection of stage instances in the guild * @type {Collection} @@ -165,6 +171,13 @@ class Guild extends Base { } } + if(data.soundboard_sounds) { + for(const sound of data.soundboard_sounds) { + sound.id = sound.sound_id; + this.soundboardSounds.add(sound, client); + } + } + if(data.presences) { for(const presence of data.presences) { if(!this.members.get(presence.user.id)) { @@ -205,6 +218,7 @@ class Guild extends Base { this.events.add(event, client); } } + this.update(data); } @@ -662,6 +676,21 @@ class Guild extends Base { return this.#client.createGuildScheduledEvent.call(this.#client, this.id, event, reason); } + /** + * Create a guild soundboard sound + * @param {Object} sound The sound to be created + * @param {String?} [sound.emojiID] The ID of the relating custom emoji (mutually exclusive with sound.emojiName) + * @param {String?} [sound.emojiName] The name of the relating default emoji (mutually exclusive with sound.emojiID) + * @param {String} sound.name The name of the soundboard sound (2-32 characters) + * @param {String} sound.sound The base 64 encoded mp3/ogg sound data + * @param {Number?} [sound.volume=1] The volume of the soundboard sound, between 0 and 1 + * @param {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + createSoundboardSound(sound, reason) { + return this.#client.createGuildSoundboardSound.call(this.#client, this.id, sound, reason); + } + /** * Create a guild sticker * @param {Object} options Sticker options @@ -753,6 +782,16 @@ class Guild extends Base { return this.#client.deleteGuildScheduledEvent.call(this.#client, this.id, eventID); } + /** + * Delete a guild soundboard sound + * @param {String} soundID The ID of the soundboard sound + * @param {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + deleteSoundboardSound(soundID, reason) { + return this.#client.deleteGuildSoundboardSound.call(this.#client, this.id, soundID, reason); + } + /** * Delete a guild sticker * @param {String} stickerID The ID of the sticker @@ -1072,6 +1111,19 @@ class Guild extends Base { return this.shard.requestGuildMembers(this.id, options); } + /** + * Request the guild's soundboard sounds through the gateway connection + * @param {Object} options Options for fetching the soundboard sounds + * @param {Number} [options.timeout] The number of milliseconds to wait before resolving early. Defaults to the `requestTimeout` client option + * @returns {Promise>} Resolves with the fetched soundboard sounds + */ + fetchSoundboardSounds(options) { + return this.shard.requestGuildSoundboardSounds({ + guildIDs: [this.id], + ...options + }).then((s) => s[this.id]); + } + /** * Get all active threads in this guild * @returns {Promise} An object containing an array of `threads` and an array of `members` @@ -1328,6 +1380,23 @@ class Guild extends Base { return this.#client.getGuildScheduledEventUsers.call(this.#client, this.id, eventID, options); } + /** + * Get a guild soundboard sound. Not to be confused with getSoundboardSounds, which gets all soundboard sounds in this guild + * @param {String} soundID The ID of the soundboard sound + * @returns {Promise} + */ + getSoundboardSound(soundID) { + return this.#client.getGuildSoundboardSound.call(this.#client, this.id, soundID); + } + + /** + * Get the guild's soundboard sounds. Not to be confused with getSoundboardSound, which gets a specified soundboard sound + * @returns {Promise>} + */ + getSoundboardSounds() { + return this.#client.getGuildSoundboardSounds.call(this.#client, this.id); + } + /** * Get the guild's templates * @returns {Promise>} diff --git a/lib/structures/SoundboardSound.js b/lib/structures/SoundboardSound.js new file mode 100644 index 00000000..2ee3bca3 --- /dev/null +++ b/lib/structures/SoundboardSound.js @@ -0,0 +1,105 @@ +"use strict"; + +const Base = require("./Base"); + +/** + * Represents a Soundboard Sound + * @extends Base + */ +class SoundboardSound extends Base { + /** + * The ID of the soundboard sound + * @member {String} SoundboardSound#id + */ + #client; + constructor(data, client) { + super(data.id); + this.#client = client; + if(data.guild_id !== undefined) { + /** + * The guild where the soundboard sound was created in (not present for default soundboard sounds). If the guild is uncached, this will be an object with an `id` key. No other keys are guaranteed + * @type {Guild?} + */ + this.guild = client.guilds.get(data.guild_id) || {id: data.guild_id}; + } + } + + update(data) { + if(data.name !== undefined) { + /** + * The name of the soundboard sound + * @type {String} + */ + this.name = data.name; + } + if(data.volume !== undefined) { + /** + * The volume of the soundboard sound, between 0 and 1 + * @type {Number} + */ + this.volume = data.volume; + } + if(data.emoji_id !== undefined) { + /** + * The ID of the relating custom emoji (will always be null for default soundboard sounds) + * @type {String?} + */ + this.emojiID = data.emoji_id; + } + if(data.emoji_name !== undefined) { + /** + * The name of the relating default emoji + * @type {String?} + */ + this.emojiName = data.emoji_name; + } + if(data.available !== undefined) { + /** + * Whether the soundboard sound is available or not (will always be true for default soundboard sounds) + * @type {Boolean} + */ + this.available = data.available; + } + if(data.user !== undefined) { + /** + * The user that created the soundboard sound (not present for default soundboard sounds, or if the bot doesn't have either create/editGuildExpressions permissions) + * @type {User?} + */ + this.user = this.#client.users.update(data.user, this.#client); + } + } + + /** + * Delete the soundboard sound (not available for default soundboard sounds) + * @param {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + delete(reason) { + return this.#client.deleteGuildSoundboardSound.call(this.#client, this.guild.id, this.id, reason); + } + + /** + * Edit the soundboard sound (not available for default soundboard sounds) + * @param {Object} options The properties to edit + * @param {String?} [options.emojiID] The ID of the relating custom emoji (mutually exclusive with options.emojiName) + * @param {String?} [options.emojiName] The name of the relating default emoji (mutually exclusive with options.emojiID) + * @param {String} [options.name] The name of the soundboard sound (2-32 characters) + * @param {Number?} [options.volume] The volume of the soundboard sound, between 0 and 1 + * @param {String} [options.reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + edit(options) { + return this.#client.editGuildSoundboardSound.call(this.#client, this.guild.id, this.id, options); + } + + /** + * Send the soundboard sound to a connected voice channel + * @param {String} channelID The ID of the connected voice channel + * @returns {Promise} + */ + send(channelID) { + return this.#client.sendSoundboardSound.call(this.#client, channelID, {soundID: this.id, sourceGuildID: this.guild.id}); + } +} + +module.exports = SoundboardSound; diff --git a/lib/structures/VoiceChannel.js b/lib/structures/VoiceChannel.js index 7e692d01..2b413ccb 100644 --- a/lib/structures/VoiceChannel.js +++ b/lib/structures/VoiceChannel.js @@ -106,6 +106,17 @@ class VoiceChannel extends GuildChannel { return this.#client.leaveVoiceChannel.call(this.#client, this.id); } + /** + * Send a soundboard sound to the voice channel + * @param {Object} options The soundboard sound options + * @param {String} options.soundID The ID of the soundboard sound + * @param {String} [options.sourceGuildID] The ID of the guild where the soundboard sound was created, if not in the same guild + * @returns {Promise} + */ + sendSoundboardSound(options) { + return this.#client.sendSoundboardSound.call(this.#client, this.id, options); + } + toJSON(props = []) { return super.toJSON([ "bitrate", From 1681360eaba27dadae032111c675f97e6a7bdaac Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 26 Oct 2024 15:56:17 +0000 Subject: [PATCH 2/7] fix missing leading slash in endpoint --- lib/rest/Endpoints.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rest/Endpoints.js b/lib/rest/Endpoints.js index b21f14bc..e22763b8 100644 --- a/lib/rest/Endpoints.js +++ b/lib/rest/Endpoints.js @@ -128,7 +128,7 @@ module.exports.GUILD_ICON = (guildID, guildIcon) module.exports.GUILD_SCHEDULED_EVENT_COVER = (eventID, eventIcon) => `/guild-events/${eventID}/${eventIcon}`; module.exports.GUILD_SPLASH = (guildID, guildSplash) => `/splashes/${guildID}/${guildSplash}`; module.exports.ROLE_ICON = (roleID, roleIcon) => `/role-icons/${roleID}/${roleIcon}`; -module.exports.SOUNDBOARD_SOUNDS = (soundID) => `soundboard-sounds/${soundID}`; +module.exports.SOUNDBOARD_SOUNDS = (soundID) => `/soundboard-sounds/${soundID}`; module.exports.TEAM_ICON = (teamID, teamIcon) => `/team-icons/${teamID}/${teamIcon}`; module.exports.USER_AVATAR = (userID, userAvatar) => `/avatars/${userID}/${userAvatar}`; module.exports.USER_AVATAR_DECORATION_PRESET = (userDecoration) => `/avatar-decoration-presets/${userDecoration}`, From f5333639830cb4760b0d35f198c63a87137238f5 Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 26 Oct 2024 16:03:17 +0000 Subject: [PATCH 3/7] style: separate SoundboardSound#id from #client declaration --- lib/structures/SoundboardSound.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/structures/SoundboardSound.js b/lib/structures/SoundboardSound.js index 2ee3bca3..a53c78c0 100644 --- a/lib/structures/SoundboardSound.js +++ b/lib/structures/SoundboardSound.js @@ -11,6 +11,7 @@ class SoundboardSound extends Base { * The ID of the soundboard sound * @member {String} SoundboardSound#id */ + #client; constructor(data, client) { super(data.id); From cf75d2127efccbc1a986f8195d9e1f7c3277d9ef Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 26 Oct 2024 16:10:23 +0000 Subject: [PATCH 4/7] fix(Guild): make fetchSoundboardSounds options param optional --- lib/structures/Guild.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/structures/Guild.js b/lib/structures/Guild.js index 583bc7f4..466b1411 100644 --- a/lib/structures/Guild.js +++ b/lib/structures/Guild.js @@ -1113,11 +1113,11 @@ class Guild extends Base { /** * Request the guild's soundboard sounds through the gateway connection - * @param {Object} options Options for fetching the soundboard sounds + * @param {Object} [options] Options for fetching the soundboard sounds * @param {Number} [options.timeout] The number of milliseconds to wait before resolving early. Defaults to the `requestTimeout` client option * @returns {Promise>} Resolves with the fetched soundboard sounds */ - fetchSoundboardSounds(options) { + fetchSoundboardSounds(options = {}) { return this.shard.requestGuildSoundboardSounds({ guildIDs: [this.id], ...options From b03f5f29b2e79221a0b9613ed6b8c0ac6b348a9b Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 26 Oct 2024 16:18:38 +0000 Subject: [PATCH 5/7] fix(Client): correct mapping in `getGuildSoundboardSounds` --- lib/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Client.js b/lib/Client.js index 8f8cf534..6a4c3c47 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -2605,7 +2605,7 @@ class Client extends EventEmitter { * @returns {Promise>} */ getGuildSoundboardSounds(guildID) { - return this.requestHandler.request("GET", Endpoints.SOUNDBOARD_SOUNDS_GUILD(guildID), true).then((sounds) => sounds.map((sound) => new SoundboardSound(sound, this))); + return this.requestHandler.request("GET", Endpoints.SOUNDBOARD_SOUNDS_GUILD(guildID), true).then((data) => data.items.map((sound) => new SoundboardSound(sound, this))); } /** From c8aed66476bbe992ded0315d56c8f53ccd484a33 Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 26 Oct 2024 16:48:49 +0000 Subject: [PATCH 6/7] style: use optional chaining --- lib/gateway/Shard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gateway/Shard.js b/lib/gateway/Shard.js index f0aa6937..1761a7c2 100644 --- a/lib/gateway/Shard.js +++ b/lib/gateway/Shard.js @@ -2467,7 +2467,7 @@ class Shard extends EventEmitter { * @event Client#guildSoundboardSoundDelete * @prop {SoundboardSound} sound The deleted soundboard sound. If the soundboard sound isn't cached, this will be an object with `id` and `guild` keys. If the guild isn't cached, it will be an object with an `id` key. No other properties are guaranteed */ - this.emit("guildSoundboardSoundDelete", (guild && guild.soundboardSounds.remove(packet.d)) || { + this.emit("guildSoundboardSoundDelete", guild?.soundboardSounds.remove(packet.d) || { id: packet.d.id, guild: guild || { id: packet.d.guild_id From fe82cbe321269b04376c55e1c666e08d0a6a7a1e Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 26 Oct 2024 16:53:11 +0000 Subject: [PATCH 7/7] more optional chaining! --- lib/gateway/Shard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gateway/Shard.js b/lib/gateway/Shard.js index 1761a7c2..8bacef14 100644 --- a/lib/gateway/Shard.js +++ b/lib/gateway/Shard.js @@ -521,7 +521,7 @@ class Shard extends EventEmitter { timeout: setTimeout(() => { res(this.requestSoundboardSoundsPromise[nonce].soundboardSounds); delete this.requestSoundboardSoundsPromise[nonce]; - }, (options && options.timeout) || this.client.options.requestTimeout) + }, options?.timeout || this.client.options.requestTimeout) }); }