Skip to content
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

Add Soundboard support #201

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions lib/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default class Client<E extends ClientEvents = ClientEvents> extends Typed
privateChannels: 0,
roles: 0,
scheduledEvents: 0,
soundboardSounds: 0,
stageInstances: 0,
stickers: 0,
unavailableGuilds: 0,
Expand Down Expand Up @@ -106,6 +107,7 @@ export default class Client<E extends ClientEvents = ClientEvents> extends Typed
privateChannels: options?.collectionLimits?.privateChannels ?? 25,
roles: this.util._setLimit(options?.collectionLimits?.roles, Infinity),
scheduledEvents: this.util._setLimit(options?.collectionLimits?.scheduledEvents, Infinity),
soundboardSounds: this.util._setLimit(options?.collectionLimits?.soundboardSounds, Infinity),
stageInstances: this.util._setLimit(options?.collectionLimits?.stageInstances, Infinity),
stickers: this.util._setLimit(options?.collectionLimits?.stickers, Infinity),
unavailableGuilds: options?.collectionLimits?.unavailableGuilds ?? Infinity,
Expand Down
36 changes: 24 additions & 12 deletions lib/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export const GuildFeatures = [
"MONETIZATION_ENABLED",
"MORE_EMOJI",
"MORE_EMOJIS",
"MORE_SOUNDBOARD",
"MORE_STICKERS",
"NEW_THREAD_PERMISSIONS",
"NEWS",
Expand Down Expand Up @@ -1128,7 +1129,9 @@ export enum Intents {
GUILDS = 1 << 0,
GUILD_MEMBERS = 1 << 1,
GUILD_MODERATION = 1 << 2,
/** @deprecated */
GUILD_EMOJIS_AND_STICKERS = 1 << 3,
GUILD_EXPRESSIONS = 1 << 3,
GUILD_INTEGRATIONS = 1 << 4,
GUILD_WEBHOOKS = 1 << 5,
GUILD_INVITES = 1 << 6,
Expand All @@ -1155,7 +1158,7 @@ export type PrivilegedIntentNames = "GUILD_MEMBERS" | "GUILD_PRESENCES" | "MESSA
export const NonPrivilegedIntents = [
Intents.GUILDS,
Intents.GUILD_MODERATION,
Intents.GUILD_EMOJIS_AND_STICKERS,
Intents.GUILD_EXPRESSIONS,
Intents.GUILD_INTEGRATIONS,
Intents.GUILD_WEBHOOKS,
Intents.GUILD_INVITES,
Expand Down Expand Up @@ -1188,17 +1191,18 @@ export const PrivilegedIntentMapping = [
] as Array<[intent: Intents, allowed: Array<ApplicationFlags>]>;

export enum GatewayOPCodes {
DISPATCH = 0,
HEARTBEAT = 1,
IDENTIFY = 2,
PRESENCE_UPDATE = 3,
VOICE_STATE_UPDATE = 4,
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,
RESUME = 6,
RECONNECT = 7,
REQUEST_GUILD_MEMBERS = 8,
INVALID_SESSION = 9,
HELLO = 10,
HEARTBEAT_ACK = 11,
REQUEST_SOUNDBOARD_SOUNDS = 31,
}

export enum GatewayCloseCodes {
Expand Down Expand Up @@ -1513,6 +1517,7 @@ export enum JSONErrorCodes {
UNKNOWN_WEBHOOK = 10_015,
UNKNOWN_WEBHOOK_SERVICE = 10_016,
UNKNOWN_SESSION = 10_020,
UNKNOWN_ASSET = 10_021,
UNKNOWN_BAN = 10_026,
UNKNOWN_SKU = 10_027,
UNKNOWN_STORE_LISTING = 10_028,
Expand All @@ -1537,6 +1542,7 @@ export enum JSONErrorCodes {
UNKNOWN_GUILD_SCHEDULED_EVENT = 10_070,
UNKNOWN_GUILD_SCHEDULED_EVENT_USER = 10_071,
UNKNOWN_TAG = 10_087,
UNKNOWN_SOUND = 10_097,
BOT_DISALLOWED = 20_001,
BOTS_CANNOT_USE_THIS_ENDPOINT = 20_001,
BOT_REQUIRED = 20_002,
Expand Down Expand Up @@ -1584,6 +1590,7 @@ export enum JSONErrorCodes {
TOO_MANY_STICKERS = 30_039,
TOO_MANY_PRUNE_REQUESTS = 30_040,
TOO_MANY_GUILD_WIDGET_SETTINGS_UPDATES = 30_042,
TOO_MANY_SOUNDBOARD_SOUNDS = 30_045,
MAXIMUM_NUMBER_OR_EDITS_TO_MESSAGES_OLDER_THAN_1_HOUR = 30_046,
TOO_MANY_PINNED_THREADS = 30_047,
TOO_MANY_FORUM_TAGS = 30_048,
Expand Down Expand Up @@ -1681,12 +1688,16 @@ export enum JSONErrorCodes {
INVALID_ACTIVITY_LAUNCH_PREMIUM_TIER = 50_107,
INVALID_ACTIVITY_LAUNCH_CONCURRENT_ACTIVITIES = 50_108,
INVALID_JSON = 50_109,
INVALID_PROVIDED_FILE = 50_110,
INVALID_PROVIDED_FILE_TYPE = 50_123,
INVALID_PROVIDED_FILE_DURATION = 50_124,
OWNER_CANNOT_BE_PENDING_MEMBER = 50_131,
OWNERSHIP_CANNOT_BE_TRANSFERRED_TO_BOT = 50_132,
INVALID_FILE_ASSET_SIZE_RESIZE_GIF = 50_138,
CANNOT_MIX_SUBSCRIPTION_AND_NON_SUBSCRIPTION_ROLES = 50_144,
CANNOT_CONVERT_BETWEEN_PREMIUM_AND_NORMAL_EMOJI = 50_145,
UPLOADED_FILE_NOT_FOUND = 50_146,
INVALID_SPECIFIED_EMOJI = 50_151,
INVALID_ACTIVITY_LAUNCH_AFK_CHANNEL = 50_148,
VOICE_MESSAGES_DO_NOT_SUPPORT_ADDITIONAL_CONTENT = 50_159,
VOICE_MESSAGES_MUST_HAVE_A_SINGLE_AUDIO_ATTACHMENT = 50_160,
Expand All @@ -1697,6 +1708,7 @@ export enum JSONErrorCodes {
INVALID_ACTIVITY_LAUNCH_AGE_GATED = 50_165,
CANNOT_SEND_VOICE_MESSAGES_IN_CHANNEL = 50_173,
USER_MUST_FIRST_BE_VERIFIED = 50_178,
PROVIDED_FILE_HAS_INVALID_DURATION = 50_192,
INVALID_SKU_ATTACHMENT_NO_ARCHIVES = 50_186,
NO_PERMISSION_TO_SEND_STICKER = 50_600,
MFA_ENABLED = 60_001,
Expand Down
24 changes: 23 additions & 1 deletion lib/gateway/Shard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import type {
UpdateVoiceStateOptions,
SendStatuses,
BotActivity,
ShardStatus
ShardStatus,
RequestSoundboardSoundsOptions
} from "../types/gateway";
import type Member from "../structures/Member";
import Base from "../structures/Base";
Expand All @@ -23,6 +24,7 @@ import type Guild from "../structures/Guild";
import type { ShardEvents } from "../types/events";
import { GatewayError, DependencyError } from "../util/Errors";
import ClientApplication from "../structures/ClientApplication";
import type Soundboard from "../structures/Soundboard";
import WebSocket, { type Data } from "ws";
import { randomBytes } from "node:crypto";
import { inspect } from "node:util";
Expand All @@ -44,6 +46,7 @@ export default class Shard extends TypedEmitter<ShardEvents> {
private _guildCreateTimeout: NodeJS.Timeout | null;
private _heartbeatInterval: NodeJS.Timeout | null;
private _requestMembersPromise: Record<string, { members: Array<Member>; received: number; timeout: NodeJS.Timeout; reject(reason?: unknown): void; resolve(value: unknown): void; }>;
private _requestSoundboardSoundsPromise: Record<string, { guildID: string; soundboardSounds: Array<Soundboard>; timeout: NodeJS.Timeout; reject(reason?: unknown): void; resolve(value: unknown): void; }>;
client!: Client;
connectAttempts: number;
connecting: boolean;
Expand Down Expand Up @@ -108,6 +111,7 @@ export default class Shard extends TypedEmitter<ShardEvents> {
this.ready = false;
this.reconnectInterval = 1000;
this._requestMembersPromise = {};
this._requestSoundboardSoundsPromise = {};
this.resumeURL = null;
this.sequence = 0;
this.sessionID = null;
Expand Down Expand Up @@ -713,6 +717,24 @@ export default class Shard extends TypedEmitter<ShardEvents> {
});
}

async requestSoundboardSounds(guildID: string, options?: RequestSoundboardSoundsOptions): Promise<Array<Soundboard>> {
const opts = {
guild_ids: [guildID],
nonce: randomBytes(16).toString("hex")
};
this.send(GatewayOPCodes.REQUEST_SOUNDBOARD_SOUNDS, opts);
return new Promise((resolve, reject) => this._requestSoundboardSoundsPromise[opts.nonce] = {
guildID,
soundboardSounds: [],
timeout: setTimeout(() => {
resolve(this._requestSoundboardSoundsPromise[opts.nonce].soundboardSounds);
delete this._requestSoundboardSoundsPromise[opts.nonce];
}, options?.timeout ?? this.client.rest.options.requestTimeout),
resolve,
reject
});
}

reset(): void {
this.connecting = false;
this.ready = false;
Expand Down
44 changes: 44 additions & 0 deletions lib/gateway/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import Integration from "../structures/Integration";
import VoiceState from "../structures/VoiceState";
import AuditLogEntry from "../structures/AuditLogEntry";
import type User from "../structures/User";
import Soundboard from "../structures/Soundboard";

export async function APPLICATION_COMMAND_PERMISSIONS_UPDATE(data: DispatchEventMap["APPLICATION_COMMAND_PERMISSIONS_UPDATE"], shard: Shard): Promise<void> {
shard.client.emit("applicationCommandPermissionsUpdate", shard.client.guilds.get(data.guild_id) ?? { id: data.guild_id }, {
Expand Down Expand Up @@ -382,6 +383,33 @@ export async function GUILD_SCHEDULED_EVENT_USER_REMOVE(data: DispatchEventMap["
shard.client.emit("guildScheduledEventUserRemove", event ?? { id: data.guild_scheduled_event_id }, user ?? { id: data.user_id });
}

export async function GUILD_SOUNDBOARD_SOUND_CREATE(data: DispatchEventMap["GUILD_SOUNDBOARD_SOUND_CREATE"], shard: Shard): Promise<void> {
const guild = shard.client.guilds.get(data.guild_id);
const soundboardSound = guild?.soundboardSounds.update(data) ?? new Soundboard(data, shard.client);
shard.client.emit("guildSoundboardSoundCreate", soundboardSound);
}

export async function GUILD_SOUNDBOARD_SOUND_DELETE(data: DispatchEventMap["GUILD_SOUNDBOARD_SOUND_DELETE"], shard: Shard): Promise<void> {
const guild = shard.client.guilds.get(data.guild_id);
const soundboardSound = guild?.soundboardSounds.get(data.sound_id);
guild?.soundboardSounds.delete(data.sound_id);
shard.client.emit("guildSoundboardSoundDelete", soundboardSound ?? { id: data.sound_id });
}

export async function GUILD_SOUNDBOARD_SOUND_UPDATE(data: DispatchEventMap["GUILD_SOUNDBOARD_SOUND_UPDATE"], shard: Shard): Promise<void> {
const guild = shard.client.guilds.get(data.guild_id);
const oldSoundboardSound = guild?.soundboardSounds.get(data.sound_id)?.toJSON() ?? null;
const soundboardSound = guild?.soundboardSounds.update(data) ?? new Soundboard(data, shard.client);
shard.client.emit("guildSoundboardSoundUpdate", soundboardSound, oldSoundboardSound);
}

export async function GUILD_SOUNDBOARD_SOUNDS_UPDATE(data: DispatchEventMap["GUILD_SOUNDBOARD_SOUNDS_UPDATE"], shard: Shard): Promise<void> {
const guild = shard.client.guilds.get(data.guild_id);
const oldSoundboardSounds = data.soundboard_sounds.map(rawSound => guild?.soundboardSounds.get(rawSound.sound_id)?.toJSON() ?? null);
const newSoundboardSounds = data.soundboard_sounds.map(sound => guild?.soundboardSounds.update(sound) ?? new Soundboard(sound, shard.client));
shard.client.emit("guildSoundboardSoundsUpdate", newSoundboardSounds, oldSoundboardSounds, data.guild_id);
}

export async function GUILD_STICKERS_UPDATE(data: DispatchEventMap["GUILD_STICKERS_UPDATE"], shard: Shard): Promise<void> {
const guild = shard.client.guilds.get(data.guild_id);
const oldStickers = guild?.stickers ? guild.stickers.toArray() : null;
Expand Down Expand Up @@ -722,6 +750,22 @@ export async function RESUMED(data: DispatchEventMap["RESUMED"], shard: Shard):
shard["_resume"]();
}

export async function SOUNDBOARD_SOUNDS(data: DispatchEventMap["SOUNDBOARD_SOUNDS"], shard: Shard): Promise<void> {
const guild = shard.client.guilds.get(data.guild_id);
const soundboardSounds = data.soundboard_sounds.map(soundboardSound => guild?.soundboardSounds.update(soundboardSound) ?? new Soundboard(soundboardSound, shard.client));
for (const nonce in shard["_requestSoundboardSoundsPromise"]) {
if (data.guild_id === shard["_requestSoundboardSoundsPromise"][nonce].guildID) {
shard["_requestSoundboardSoundsPromise"][nonce].soundboardSounds.push(...soundboardSounds);
clearTimeout(shard["_requestSoundboardSoundsPromise"][nonce].timeout);
shard["_requestSoundboardSoundsPromise"][nonce].resolve(shard["_requestSoundboardSoundsPromise"][nonce].soundboardSounds);
delete shard["_requestSoundboardSoundsPromise"][nonce];
}
}

shard.client.emit("soundboardSounds", data.guild_id, soundboardSounds);
shard.lastHeartbeatAck = true;
}

export async function STAGE_INSTANCE_CREATE(data: DispatchEventMap["STAGE_INSTANCE_CREATE"], shard: Shard): Promise<void> {
const guild = shard.client.guilds.get(data.guild_id);
const stateInstance = guild?.stageInstances.update(data) ?? new StageInstance(data, shard.client);
Expand Down
18 changes: 17 additions & 1 deletion lib/routes/Channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ import type {
AnyGuildChannel,
GetChannelMessagesIteratorOptions,
MessagesIterator,
GetPollAnswerUsersOptions
GetPollAnswerUsersOptions,
SendSoundboardSoundOptions
} from "../types/channels";
import * as Routes from "../util/Routes";
import type Message from "../structures/Message";
Expand Down Expand Up @@ -1166,6 +1167,21 @@ export default class Channels {
});
}

/**
* Send a sound from the soundboard to the channel where the user is connected.
* @param channelID The ID of the channel to send the soundboard sound to.
* @param soundID The ID of the soundboard sound to send.
* @param sourceGuildID The ID of the guild the soundboard sound is from.
* @caching This method **does not** cache its result.
*/
async sendSoundboardSound(channelID: string, options: SendSoundboardSoundOptions): Promise<void> {
await this._manager.authRequest<null>({
method: "POST",
path: Routes.SEND_SOUNDBOARD_SOUND(channelID),
json: { sound_id: options.soundID, source_guild_id: options.sourceGuildID }
});
}

/**
* Show a typing indicator in a channel. How long users see this varies from client to client.
* @param channelID The ID of the channel to show the typing indicator in.
Expand Down
Loading
Loading