feat: Soundboard API (#267)

* feat(discord): soundboard api

* chore: apply formatting

* fix(gateway): events

* feat(events): soundboard

* chore: apply formatting

* fix: xd

* feat(api): soundboard routes

* chore: apply formatting

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: MARCROCK22 <57925328+MARCROCK22@users.noreply.github.com>
This commit is contained in:
Marcos Susaña 2024-10-01 12:57:44 -04:00 committed by GitHub
parent 9f54a8df9c
commit 44c872de71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 303 additions and 17 deletions

View File

@ -1,6 +1,6 @@
import { CDN_URL } from '../common';
import type { APIRoutes, ApiHandler, CDNRoute } from './index';
import type { HttpMethods, ImageExtension, ImageSize, StickerExtension } from './shared';
import type { HttpMethods, ImageExtension, ImageSize, SoundExtension, StickerExtension } from './shared';
export enum ProxyRequestMethod {
Delete = 'delete',
@ -63,7 +63,7 @@ export const CDNRouter = {
};
export interface BaseCDNUrlOptions {
extension?: ImageExtension | StickerExtension | undefined;
extension?: ImageExtension | StickerExtension | SoundExtension;
size?: ImageSize;
}
@ -75,7 +75,9 @@ export function parseCDNURL(route: string, options: CDNUrlOptions = {}) {
if (options.forceStatic && route.includes('a_')) options.extension = 'png';
if (!options.extension && route.includes('a_')) options.extension = 'gif';
const url = new URL(`${route}.${options.extension || 'png'}`);
options.extension ||= route.includes('soundboard') ? 'ogg' : 'png';
const url = new URL(`${route}.${options.extension}`);
if (options.size) url.searchParams.set('size', `${options.size}`);

View File

@ -1,5 +1,5 @@
import type { BaseCDNUrlOptions, CDNUrlOptions } from '../..';
import type { StickerExtension } from '../shared';
import type { SoundExtension, StickerExtension } from '../shared';
export interface CDNRoute {
embed: {
@ -67,6 +67,9 @@ export interface CDNRoute {
'guild-events'(id: string): {
get(cover: string, options?: BaseCDNUrlOptions): string;
};
'soundboard-sounds': {
get(id: string, options?: { extension: SoundExtension }): string;
};
}
export interface CDNRoute {

View File

@ -9,6 +9,7 @@ import type { StickerRoutes } from './stickers';
import type { UserRoutes } from './users';
import type { VoiceRoutes } from './voice';
import type { WebhookRoutes } from './webhooks';
export type { SoundboardRoutes } from './soundboard';
export * from './cdn';

View File

@ -0,0 +1,36 @@
import type {
RESTGetAPIDefaultsSoundboardSoundsResult,
RESTGetAPIGuildSoundboardSoundsResult,
RESTPatchAPIGuildSoundboardSound,
RESTPatchAPIGuildSoundboardSoundResult,
RESTPostAPIGuildSoundboardSound,
RESTPostAPIGuildSoundboardSoundResult,
RESTPostAPISendSoundboardSound,
} from '../../types';
import type { RestArguments, RestArgumentsNoBody } from '../api';
export interface SoundboardRoutes {
channels(id: string): {
'send-soundboard-sound': {
post(args: RestArguments<RESTPostAPISendSoundboardSound>): Promise<never>;
};
};
'soundboard-default-sounds': {
get(args?: RestArgumentsNoBody): Promise<RESTGetAPIDefaultsSoundboardSoundsResult>;
};
guilds(id: string): {
get(args?: RestArgumentsNoBody): Promise<RESTGetAPIGuildSoundboardSoundsResult>;
'soundboard-sounds': {
post(
args: RestArguments<RESTPostAPIGuildSoundboardSound>,
): Promise<RESTPostAPIGuildSoundboardSoundResult | undefined>;
(
id: string,
): {
get(args?: RestArgumentsNoBody): Promise<RESTPostAPIGuildSoundboardSoundResult>;
patch(args?: RestArguments<RESTPatchAPIGuildSoundboardSound>): Promise<RESTPatchAPIGuildSoundboardSoundResult>;
delete(args?: RestArgumentsNoBody): Promise<never>;
};
};
};
}

View File

@ -2,7 +2,9 @@ export const DefaultUserAgent = 'DiscordBot (https://seyfert.dev, v2.1.0)';
export const ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'] as const;
export const ALLOWED_STICKER_EXTENSIONS = ['png', 'json', 'gif'] as const;
export const ALLOWED_SIZES = [16, 32, 64, 100, 128, 256, 512, 1_024, 2_048, 4_096] as const;
export const ALLOWED_SOUNDS_EXTENSIONS = ['mp3', 'ogg'] as const;
export type ImageExtension = (typeof ALLOWED_EXTENSIONS)[number];
export type StickerExtension = (typeof ALLOWED_STICKER_EXTENSIONS)[number];
export type ImageSize = (typeof ALLOWED_SIZES)[number];
export type SoundExtension = (typeof ALLOWED_SOUNDS_EXTENSIONS)[number];

4
src/cache/index.ts vendored
View File

@ -224,8 +224,8 @@ export class Cache {
return this.hasIntent('GuildMembers');
}
get hasEmojisAndStickersIntent() {
return this.hasIntent('GuildEmojisAndStickers');
get hasGuildExpressionsIntent() {
return this.hasIntent('GuildExpressions');
}
get hasVoiceStatesIntent() {

View File

@ -40,7 +40,7 @@ export class EmojiShorter extends BaseShorter {
body: bodyResolved,
});
await this.client.cache.emojis?.setIfNI('GuildEmojisAndStickers', emoji.id!, guildId, emoji);
await this.client.cache.emojis?.setIfNI('GuildExpressions', emoji.id!, guildId, emoji);
return Transformers.GuildEmoji(this.client, emoji, guildId);
}
@ -70,7 +70,7 @@ export class EmojiShorter extends BaseShorter {
*/
async delete(guildId: string, emojiId: string, reason?: string) {
await this.client.proxy.guilds(guildId).emojis(emojiId).delete({ reason });
await this.client.cache.emojis?.removeIfNI('GuildEmojisAndStickers', emojiId, guildId);
await this.client.cache.emojis?.removeIfNI('GuildExpressions', emojiId, guildId);
}
/**
@ -83,7 +83,7 @@ export class EmojiShorter extends BaseShorter {
*/
async edit(guildId: string, emojiId: string, body: RESTPatchAPIGuildEmojiJSONBody, reason?: string) {
const emoji = await this.client.proxy.guilds(guildId).emojis(emojiId).patch({ body, reason });
await this.client.cache.emojis?.setIfNI('GuildEmojisAndStickers', emoji.id!, guildId, emoji);
await this.client.cache.emojis?.setIfNI('GuildExpressions', emoji.id!, guildId, emoji);
return Transformers.GuildEmoji(this.client, emoji, guildId);
}
}

View File

@ -299,7 +299,7 @@ export class GuildShorter extends BaseShorter {
const sticker = await this.client.proxy
.guilds(guildId)
.stickers.post({ reason, body: json, files: [{ ...fileResolve[0], key: 'file' }], appendToFormData: true });
await this.client.cache.stickers?.setIfNI('GuildEmojisAndStickers', sticker.id, guildId, sticker);
await this.client.cache.stickers?.setIfNI('GuildExpressions', sticker.id, guildId, sticker);
return Transformers.Sticker(this.client, sticker);
},
@ -313,7 +313,7 @@ export class GuildShorter extends BaseShorter {
*/
edit: async (guildId: string, stickerId: string, body: RESTPatchAPIGuildStickerJSONBody, reason?: string) => {
const sticker = await this.client.proxy.guilds(guildId).stickers(stickerId).patch({ body, reason });
await this.client.cache.stickers?.setIfNI('GuildEmojisAndStickers', stickerId, guildId, sticker);
await this.client.cache.stickers?.setIfNI('GuildExpressions', stickerId, guildId, sticker);
return Transformers.Sticker(this.client, sticker);
},
@ -344,7 +344,7 @@ export class GuildShorter extends BaseShorter {
*/
delete: async (guildId: string, stickerId: string, reason?: string) => {
await this.client.proxy.guilds(guildId).stickers(stickerId).delete({ reason });
await this.client.cache.stickers?.removeIfNI('GuildEmojisAndStickers', stickerId, guildId);
await this.client.cache.stickers?.removeIfNI('GuildExpressions', stickerId, guildId);
},
};
}

View File

@ -16,6 +16,7 @@ export * from './typing';
export * from './user';
export * from './voice';
export * from './webhook';
export * from './soundboard';
import type { CamelCase } from '../../common';
import type * as RawEvents from './index';

View File

@ -0,0 +1,39 @@
import { Transformers } from '../../client';
import type { UsingClient } from '../../commands';
import { toCamelCase } from '../../common';
import type {
GatewayGuildSoundboardSoundCreateDispatchData,
GatewayGuildSoundboardSoundDeleteDispatchData,
GatewayGuildSoundboardSoundUpdateDispatchData,
GatewayGuildSoundboardSoundsUpdateDispatchData,
GatewaySoundboardSoundsDispatchData,
} from '../../types';
export const GUILD_SOUNDBOARD_SOUND_CREATE = (
self: UsingClient,
data: GatewayGuildSoundboardSoundCreateDispatchData,
) => {
return data.user ? { ...toCamelCase(data), user: Transformers.User(self, data.user) } : toCamelCase(data);
};
export const GUILD_SOUNDBOARD_SOUND_UPDATE = (
self: UsingClient,
data: GatewayGuildSoundboardSoundUpdateDispatchData,
) => {
return data.user ? { ...toCamelCase(data), user: Transformers.User(self, data.user) } : toCamelCase(data);
};
export const GUILD_SOUNDBOARD_SOUNDS_UPDATE = (
self: UsingClient,
data: GatewayGuildSoundboardSoundsUpdateDispatchData,
) => {
return data.map(d => (d.user ? { ...toCamelCase(d), user: Transformers.User(self, d.user) } : toCamelCase(d)));
};
export const GUILD_SOUNDBOARD_SOUND_DELETE = (_: UsingClient, data: GatewayGuildSoundboardSoundDeleteDispatchData) => {
return toCamelCase(data);
};
export const SOUNDBOARD_SOUNDS = (_: UsingClient, data: GatewaySoundboardSoundsDispatchData) => {
return toCamelCase(data);
};

View File

@ -35,6 +35,7 @@ import type {
GatewayThreadListSync as RawGatewayThreadListSync,
GatewayThreadMembersUpdate as RawGatewayThreadMembersUpdate,
} from './payloads/index';
import type { APISoundBoard } from './payloads/soundboard';
import type { ReactionType } from './rest/index';
import type { AnimationTypes, Nullable } from './utils';
@ -57,7 +58,8 @@ export type GatewaySendPayload =
| GatewayRequestGuildMembers
| GatewayResume
| GatewayUpdatePresence
| GatewayVoiceStateUpdate;
| GatewayVoiceStateUpdate
| GatewayRequestSoundboardSounds;
export type GatewayReceivePayload =
| GatewayDispatchPayload
@ -127,7 +129,12 @@ export type GatewayDispatchPayload =
| GatewayUserUpdateDispatch
| GatewayVoiceServerUpdateDispatch
| GatewayVoiceStateUpdateDispatch
| GatewayWebhooksUpdateDispatch;
| GatewayWebhooksUpdateDispatch
| GatewayGuildSoundboardSoundCreateDispatch
| GatewayGuildSoundboardSoundDeleteDispatch
| GatewayGuildSoundboardSoundUpdateDispatch
| GatewayGuildSoundboardSoundsUpdateDispatch
| GatewaySoundboardSoundsDispatch;
// #region Dispatch Payloads
@ -607,6 +614,14 @@ export interface GatewayGuildCreateDispatchData extends APIGuild {
* https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object
*/
guild_scheduled_events: APIGuildScheduledEvent[];
/**
* Soundboard sounds in the guild
*
* **This field is only sent within the [GUILD_CREATE](https://discord.com/developers/docs/topics/gateway-events#guild-create) event**
*
* https://discord.com/developers/docs/resources/soundboard
*/
soundboard_sounds: APISoundBoard[];
}
/**
@ -968,6 +983,51 @@ export interface GatewayGuildScheduledEventUserRemoveDispatchData {
guild_id: Snowflake;
}
export type GatewayGuildSoundboardSoundCreateDispatchData = APISoundBoard;
export type GatewayGuildSoundboardSoundCreateDispatch = DataPayload<
GatewayDispatchEvents.GuildSoundboardSoundCreate,
GatewayGuildSoundboardSoundCreateDispatchData
>;
export type GatewayGuildSoundboardSoundUpdateDispatchData = APISoundBoard;
export type GatewayGuildSoundboardSoundUpdateDispatch = DataPayload<
GatewayDispatchEvents.GuildSoundboardSoundUpdate,
GatewayGuildSoundboardSoundUpdateDispatchData
>;
export interface GatewayGuildSoundboardSoundDeleteDispatchData {
/** ID of the sound that was deleted */
sound_id: string;
/** ID of the guild the sound was in */
guild_id: string;
}
export type GatewayGuildSoundboardSoundDeleteDispatch = DataPayload<
GatewayDispatchEvents.GuildSoundboardSoundDelete,
GatewayGuildSoundboardSoundDeleteDispatchData
>;
export type GatewayGuildSoundboardSoundsUpdateDispatchData = APISoundBoard[];
export type GatewayGuildSoundboardSoundsUpdateDispatch = DataPayload<
GatewayDispatchEvents.GuildSoundboardSoundsUpdate,
GatewayGuildSoundboardSoundsUpdateDispatchData
>;
export interface GatewaySoundboardSoundsDispatchData {
/** The guild's soundboard sounds */
soundboard_sounds: APISoundBoard[];
/** ID of the guild */
guild_id: string;
}
export type GatewaySoundboardSoundsDispatch = DataPayload<
GatewayDispatchEvents.SoundboardSounds,
GatewaySoundboardSoundsDispatchData
>;
/**
* https://discord.com/developers/docs/topics/gateway-events#integration-create
*/
@ -1862,6 +1922,21 @@ export type GatewayRequestGuildMembersData =
| GatewayRequestGuildMembersDataWithQuery
| GatewayRequestGuildMembersDataWithUserIds;
/**
* https://discord.com/developers/docs/topics/gateway-events#request-soundboard-sounds
*/
export interface GatewayRequestSoundboardSounds {
op: GatewayOpcodes.RequestSoundboardSounds;
d: GatewayRequestSoundboardSoundsData;
}
/**
* https://discord.com/developers/docs/topics/gateway-events#request-soundboard-sounds-request-soundboard-sounds-structure
*/
export interface GatewayRequestSoundboardSoundsData {
/** IDs of the guilds to get soundboard sounds for */
guild_ids: string[];
}
/**
* https://discord.com/developers/docs/topics/gateway-events#update-voice-state
*/

View File

@ -471,6 +471,10 @@ export enum GuildFeature {
* Guild has enabled Membership Screening
*/
MemberVerificationGateEnabled = 'MEMBER_VERIFICATION_GATE_ENABLED',
/**
* Guild has increased custom soundboard sound slots
*/
MoreSoundboard = 'MORE_SOUNDBOARD',
/**
* Guild has enabled monetization
*
@ -514,6 +518,10 @@ export enum GuildFeature {
* Guild has enabled role subscriptions
*/
RoleSubscriptionsEnabled = 'ROLE_SUBSCRIPTIONS_ENABLED',
/**
* Guild has created soundboard sounds
*/
Soundboard = 'SOUNDBOARD',
/**
* Guild has enabled ticketed events
*/

View File

@ -19,6 +19,7 @@ export * from './user';
export * from './voice';
export * from './webhook';
export * from './monetization';
export * from './soundboard';
import type { LocaleString } from '../rest';

View File

@ -0,0 +1,25 @@
/**
* Types extracted from https://discord.com/developers/docs/resources/soundboard
*/
import type { APIUser } from './user';
/** https://discord.com/developers/docs/resources/soundboard#soundboard-sound-object-soundboard-sound-structure */
export interface APISoundBoard {
/** the name of this sound */
name: string;
/** the id of this sound */
sound_id: string;
/** the volume of this sound, from 0 to 1 */
volume: number;
/** the id of this sound's custom emoji */
emoji_id: string | null;
/** the unicode character of this sound's standard emoji */
emoji_name: string | null;
/** the id of the guild this sound is in */
guild_id?: string;
/** whether this sound can be used, may be false due to loss of Server Boosts */
available: boolean;
/** the user who created this sound */
user?: APIUser;
}

View File

@ -19,6 +19,7 @@ export * from './user';
export * from './voice';
export * from './webhook';
export * from './monetization';
export * from './soundboard';
export type DefaultUserAvatarAssets = 0 | 1 | 2 | 3 | 4 | 5;

View File

@ -0,0 +1,70 @@
import type { APISoundBoard } from '../payloads/soundboard';
/**
* https://discord.com/developers/docs/resources/soundboard#send-soundboard-sound
* @fires VoiceChannelEffectSend
* @requires Permissions Speak and UseSoundboard
* @satisfies VoiceState without deaf, self_deaf, mute, or suppress enabled.
*/
export interface RESTPostAPISendSoundboardSound {
/** the id of the soundboard sound to play */
sound_id: string;
/** the id of the guild the soundboard sound is from, required to play sounds from different servers */
source_guild_id?: string;
}
/**
* https://discord.com/developers/docs/resources/soundboard#list-default-soundboard-sounds
*/
export type RESTGetAPIDefaultsSoundboardSoundsResult = Omit<APISoundBoard, 'user' | 'guild_id'>[];
/**
* https://discord.com/developers/docs/resources/soundboard#list-guild-soundboard-sounds
*/
export type RESTGetAPIGuildSoundboardSoundsResult = { items: APISoundBoard[] };
/**
* https://discord.com/developers/docs/resources/soundboard#create-guild-soundboard-sound
*
* Soundboard sounds have a max file size of 512kb and a max duration of 5.2 seconds.
* This endpoint supports the X-Audit-Log-Reason header.
* @requires Permission CreateGuildExpressions
*/
export interface RESTPostAPIGuildSoundboardSound {
/** name of the soundboard sound (2-32 characters) */
name: string;
/** the mp3 or ogg sound data, base64 encoded, similar to image data */
sound: string;
/** the volume of the soundboard sound, from 0 to 1, defaults to 1 */
volume?: number | null;
/** the id of the custom emoji for the soundboard sound */
emoji_id?: string | null;
/** the unicode character of a standard emoji for the soundboard sound */
emoji_name?: string | null;
}
export type RESTPostAPIGuildSoundboardSoundResult = APISoundBoard;
/**
* https://discord.com/developers/docs/resources/soundboard#modify-guild-soundboard-sound
* @fires GuildSoundboardSoundUpdate
*/
export interface RESTPatchAPIGuildSoundboardSound {
/** name of the soundboard sound (2-32 characters) */
name?: string;
/** the volume of the soundboard sound, from 0 to 1, defaults to 1 */
volume?: number | null;
/** the id of the custom emoji for the soundboard sound */
emoji_id?: string | null;
/** the unicode character of a standard emoji for the soundboard sound */
emoji_name?: string | null;
}
export type RESTPatchAPIGuildSoundboardSoundResult = APISoundBoard;
/**
* https://discord.com/developers/docs/resources/soundboard#delete-guild-soundboard-sound
* This endpoint supports the X-Audit-Log-Reason header.
* @fires GuildSoundboardSoundDelete
*/
export type RESTDeleteAPIGuildSoundboardSoundResult = never;

View File

@ -197,6 +197,10 @@ export enum GatewayOpcodes {
* Sent in response to receiving a heartbeat to acknowledge that it has been received
*/
HeartbeatAck = 11,
/**
* Used to request soundboard sounds for a list of guilds. The server will send Soundboard Sounds events for each guild in response.
*/
RequestSoundboardSounds = 31,
}
/**
@ -292,9 +296,8 @@ export enum GatewayIntentBits {
/**
* @deprecated This is the old name for {@apilink GatewayIntentBits#GuildModeration}
*/
GuildBans = GuildModeration,
GuildEmojisAndStickers = 1 << 3,
GuildExpressions = 1 << 3,
GuildIntegrations = 1 << 4,
GuildWebhooks = 1 << 5,
GuildInvites = 1 << 6,
@ -380,6 +383,11 @@ export enum GatewayDispatchEvents {
GuildScheduledEventDelete = 'GUILD_SCHEDULED_EVENT_DELETE',
GuildScheduledEventUserAdd = 'GUILD_SCHEDULED_EVENT_USER_ADD',
GuildScheduledEventUserRemove = 'GUILD_SCHEDULED_EVENT_USER_REMOVE',
GuildSoundboardSoundCreate = 'GUILD_SOUNDBOARD_SOUND_CREATE',
GuildSoundboardSoundUpdate = 'GUILD_SOUNDBOARD_SOUND_UPDATE',
GuildSoundboardSoundDelete = 'GUILD_SOUNDBOARD_SOUND_DELETE',
GuildSoundboardSoundsUpdate = 'GUILD_SOUNDBOARD_SOUNDS_UPDATE',
SoundboardSounds = 'SOUNDBOARD_SOUNDS',
AutoModerationRuleCreate = 'AUTO_MODERATION_RULE_CREATE',
AutoModerationRuleUpdate = 'AUTO_MODERATION_RULE_UPDATE',
AutoModerationRuleDelete = 'AUTO_MODERATION_RULE_DELETE',

View File

@ -29,6 +29,8 @@ import type {
GatewayGuildRoleDeleteDispatchData,
GatewayGuildRoleUpdateDispatchData,
GatewayGuildScheduledEventUserRemoveDispatchData,
GatewayGuildSoundboardSoundDeleteDispatchData,
GatewayGuildSoundboardSoundsUpdateDispatchData,
GatewayGuildStickersUpdateDispatchData,
GatewayIntegrationCreateDispatchData,
GatewayIntegrationDeleteDispatchData,
@ -49,6 +51,7 @@ import type {
GatewayReadyDispatchData,
GatewayRequestGuildMembersDataWithQuery,
GatewayRequestGuildMembersDataWithUserIds,
GatewaySoundboardSoundsDispatchData,
GatewayThreadCreateDispatchData,
GatewayThreadDeleteDispatchData,
GatewayThreadListSyncDispatchData,
@ -63,6 +66,7 @@ import type {
PresenceUpdateStatus,
} from '../types';
import { GatewayDispatchEvents } from '../types';
import type { APISoundBoard } from '../types/payloads/soundboard';
/** https://discord.com/developers/docs/topics/gateway-events#update-presence */
export interface StatusUpdate {
@ -123,6 +127,9 @@ export interface Events {
[GatewayDispatchEvents.GuildEmojisUpdate]: GatewayGuildEmojisUpdateDispatchData;
[GatewayDispatchEvents.GuildStickersUpdate]: GatewayGuildStickersUpdateDispatchData;
[GatewayDispatchEvents.GuildIntegrationsUpdate]: GatewayGuildIntegrationsUpdateDispatchData;
[GatewayDispatchEvents.GuildSoundboardSoundsUpdate]: GatewayGuildSoundboardSoundsUpdateDispatchData;
[GatewayDispatchEvents.GuildSoundboardSoundDelete]: GatewayGuildSoundboardSoundDeleteDispatchData;
[GatewayDispatchEvents.SoundboardSounds]: GatewaySoundboardSoundsDispatchData;
[GatewayDispatchEvents.GuildMemberAdd]: GatewayGuildMemberAddDispatchData;
[GatewayDispatchEvents.GuildMemberRemove]: GatewayGuildMemberRemoveDispatchData;
[GatewayDispatchEvents.GuildMemberUpdate]: GatewayGuildMemberUpdateDispatchData;
@ -225,6 +232,10 @@ export type SubscriptionEvents = RestToKeys<
]
>;
export type SoundboardSoundsEvents = RestToKeys<
[APISoundBoard, GatewayDispatchEvents.GuildSoundboardSoundCreate, GatewayDispatchEvents.GuildSoundboardSoundUpdate]
>;
export type NormalizeEvents = Events &
AutoModetaractionRuleEvents &
ChannelSameEvents &
@ -234,6 +245,9 @@ export type NormalizeEvents = Events &
EntitlementEvents &
PollVoteSameEvents &
StageSameEvents &
SubscriptionEvents & { RAW: GatewayDispatchEvents };
SubscriptionEvents &
SoundboardSoundsEvents & {
RAW: GatewayDispatchEvents;
};
export type GatewayEvents = { [x in keyof NormalizeEvents]: NormalizeEvents[x] };