diff --git a/packages/rest/__tests__/CDN.test.ts b/packages/rest/__tests__/CDN.test.ts index ed2600f4f614..6bc21ecd6341 100644 --- a/packages/rest/__tests__/CDN.test.ts +++ b/packages/rest/__tests__/CDN.test.ts @@ -1,114 +1,119 @@ import { test, expect } from 'vitest'; import { CDN } from '../src/index.js'; -const base = 'https://discord.com'; +const baseCDN = 'https://cdn-discord.com'; +const baseMedia = 'https://media-discord.com'; const id = '123456'; const hash = 'abcdef'; const animatedHash = 'a_bcdef'; const defaultAvatar = 1_234 % 5; -const cdn = new CDN(base); +const cdn = new CDN(baseCDN, baseMedia); test('appAsset default', () => { - expect(cdn.appAsset(id, hash)).toEqual(`${base}/app-assets/${id}/${hash}.webp`); + expect(cdn.appAsset(id, hash)).toEqual(`${baseCDN}/app-assets/${id}/${hash}.webp`); }); test('appIcon default', () => { - expect(cdn.appIcon(id, hash)).toEqual(`${base}/app-icons/${id}/${hash}.webp`); + expect(cdn.appIcon(id, hash)).toEqual(`${baseCDN}/app-icons/${id}/${hash}.webp`); }); test('avatar default', () => { - expect(cdn.avatar(id, hash)).toEqual(`${base}/avatars/${id}/${hash}.webp`); + expect(cdn.avatar(id, hash)).toEqual(`${baseCDN}/avatars/${id}/${hash}.webp`); }); test('avatar dynamic-animated', () => { - expect(cdn.avatar(id, animatedHash)).toEqual(`${base}/avatars/${id}/${animatedHash}.gif`); + expect(cdn.avatar(id, animatedHash)).toEqual(`${baseCDN}/avatars/${id}/${animatedHash}.gif`); }); test('avatar dynamic-not-animated', () => { - expect(cdn.avatar(id, hash)).toEqual(`${base}/avatars/${id}/${hash}.webp`); + expect(cdn.avatar(id, hash)).toEqual(`${baseCDN}/avatars/${id}/${hash}.webp`); }); test('avatar decoration default', () => { - expect(cdn.avatarDecoration(id, hash)).toEqual(`${base}/avatar-decorations/${id}/${hash}.webp`); + expect(cdn.avatarDecoration(id, hash)).toEqual(`${baseCDN}/avatar-decorations/${id}/${hash}.webp`); }); test('avatar decoration preset', () => { - expect(cdn.avatarDecoration(hash)).toEqual(`${base}/avatar-decoration-presets/${hash}.png`); + expect(cdn.avatarDecoration(hash)).toEqual(`${baseCDN}/avatar-decoration-presets/${hash}.png`); }); test('banner default', () => { - expect(cdn.banner(id, hash)).toEqual(`${base}/banners/${id}/${hash}.webp`); + expect(cdn.banner(id, hash)).toEqual(`${baseCDN}/banners/${id}/${hash}.webp`); }); test('channelIcon default', () => { - expect(cdn.channelIcon(id, hash)).toEqual(`${base}/channel-icons/${id}/${hash}.webp`); + expect(cdn.channelIcon(id, hash)).toEqual(`${baseCDN}/channel-icons/${id}/${hash}.webp`); }); test('defaultAvatar default', () => { - expect(cdn.defaultAvatar(defaultAvatar)).toEqual(`${base}/embed/avatars/${defaultAvatar}.png`); + expect(cdn.defaultAvatar(defaultAvatar)).toEqual(`${baseCDN}/embed/avatars/${defaultAvatar}.png`); }); test('discoverySplash default', () => { - expect(cdn.discoverySplash(id, hash)).toEqual(`${base}/discovery-splashes/${id}/${hash}.webp`); + expect(cdn.discoverySplash(id, hash)).toEqual(`${baseCDN}/discovery-splashes/${id}/${hash}.webp`); }); test('emoji default', () => { - expect(cdn.emoji(id)).toEqual(`${base}/emojis/${id}.webp`); + expect(cdn.emoji(id)).toEqual(`${baseCDN}/emojis/${id}.webp`); }); test('emoji gif', () => { - expect(cdn.emoji(id, 'gif')).toEqual(`${base}/emojis/${id}.gif`); + expect(cdn.emoji(id, 'gif')).toEqual(`${baseCDN}/emojis/${id}.gif`); }); test('guildMemberAvatar default', () => { - expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${base}/guilds/${id}/users/${id}/avatars/${hash}.webp`); + expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${baseCDN}/guilds/${id}/users/${id}/avatars/${hash}.webp`); }); test('guildMemberAvatar dynamic-animated', () => { expect(cdn.guildMemberAvatar(id, id, animatedHash)).toEqual( - `${base}/guilds/${id}/users/${id}/avatars/${animatedHash}.gif`, + `${baseCDN}/guilds/${id}/users/${id}/avatars/${animatedHash}.gif`, ); }); test('guildMemberAvatar dynamic-not-animated', () => { - expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${base}/guilds/${id}/users/${id}/avatars/${hash}.webp`); + expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${baseCDN}/guilds/${id}/users/${id}/avatars/${hash}.webp`); }); test('guildScheduledEventCover default', () => { - expect(cdn.guildScheduledEventCover(id, hash)).toEqual(`${base}/guild-events/${id}/${hash}.webp`); + expect(cdn.guildScheduledEventCover(id, hash)).toEqual(`${baseCDN}/guild-events/${id}/${hash}.webp`); }); test('icon default', () => { - expect(cdn.icon(id, hash)).toEqual(`${base}/icons/${id}/${hash}.webp`); + expect(cdn.icon(id, hash)).toEqual(`${baseCDN}/icons/${id}/${hash}.webp`); }); test('icon dynamic-animated', () => { - expect(cdn.icon(id, animatedHash)).toEqual(`${base}/icons/${id}/${animatedHash}.gif`); + expect(cdn.icon(id, animatedHash)).toEqual(`${baseCDN}/icons/${id}/${animatedHash}.gif`); }); test('icon dynamic-not-animated', () => { - expect(cdn.icon(id, hash)).toEqual(`${base}/icons/${id}/${hash}.webp`); + expect(cdn.icon(id, hash)).toEqual(`${baseCDN}/icons/${id}/${hash}.webp`); }); test('role icon default', () => { - expect(cdn.roleIcon(id, hash)).toEqual(`${base}/role-icons/${id}/${hash}.webp`); + expect(cdn.roleIcon(id, hash)).toEqual(`${baseCDN}/role-icons/${id}/${hash}.webp`); }); test('splash default', () => { - expect(cdn.splash(id, hash)).toEqual(`${base}/splashes/${id}/${hash}.webp`); + expect(cdn.splash(id, hash)).toEqual(`${baseCDN}/splashes/${id}/${hash}.webp`); }); test('sticker default', () => { - expect(cdn.sticker(id)).toEqual(`${base}/stickers/${id}.png`); + expect(cdn.sticker(id)).toEqual(`${baseCDN}/stickers/${id}.png`); +}); + +test('sticker GIF', () => { + expect(cdn.sticker(id, 'gif')).toEqual(`${baseMedia}/stickers/${id}.gif`); }); test('stickerPackBanner default', () => { - expect(cdn.stickerPackBanner(id)).toEqual(`${base}/app-assets/710982414301790216/store/${id}.webp`); + expect(cdn.stickerPackBanner(id)).toEqual(`${baseCDN}/app-assets/710982414301790216/store/${id}.webp`); }); test('teamIcon default', () => { - expect(cdn.teamIcon(id, hash)).toEqual(`${base}/team-icons/${id}/${hash}.webp`); + expect(cdn.teamIcon(id, hash)).toEqual(`${baseCDN}/team-icons/${id}/${hash}.webp`); }); test('makeURL throws on invalid size', () => { @@ -122,5 +127,5 @@ test('makeURL throws on invalid extension', () => { }); test('makeURL valid size', () => { - expect(cdn.avatar(id, animatedHash, { size: 512 })).toEqual(`${base}/avatars/${id}/${animatedHash}.gif?size=512`); + expect(cdn.avatar(id, animatedHash, { size: 512 })).toEqual(`${baseCDN}/avatars/${id}/${animatedHash}.gif?size=512`); }); diff --git a/packages/rest/src/lib/CDN.ts b/packages/rest/src/lib/CDN.ts index 7d2ac6540c1e..46cd17080e7d 100644 --- a/packages/rest/src/lib/CDN.ts +++ b/packages/rest/src/lib/CDN.ts @@ -46,6 +46,12 @@ export interface MakeURLOptions { * The allowed extensions that can be used */ allowedExtensions?: readonly string[]; + /** + * The base URL. + * + * @defaultValue `DefaultRestOptions.cdn` + */ + base?: string; /** * The extension to use for the image URL * @@ -62,7 +68,10 @@ export interface MakeURLOptions { * The CDN link builder */ export class CDN { - public constructor(private readonly base: string = DefaultRestOptions.cdn) {} + public constructor( + private readonly cdn: string = DefaultRestOptions.cdn, + private readonly mediaProxy: string = DefaultRestOptions.mediaProxy, + ) {} /** * Generates an app asset URL for a client's asset. @@ -287,10 +296,15 @@ export class CDN { * @param stickerId - The sticker id * @param extension - The extension of the sticker * @privateRemarks - * Stickers cannot have a `.webp` extension, so we default to a `.png` + * Stickers cannot have a `.webp` extension, so we default to a `.png`. + * Sticker GIFs do not use the CDN base URL. */ public sticker(stickerId: string, extension: StickerExtension = 'png'): string { - return this.makeURL(`/stickers/${stickerId}`, { allowedExtensions: ALLOWED_STICKER_EXTENSIONS, extension }); + return this.makeURL(`/stickers/${stickerId}`, { + allowedExtensions: ALLOWED_STICKER_EXTENSIONS, + base: extension === 'gif' ? this.mediaProxy : this.cdn, + extension, + }); } /** @@ -352,7 +366,12 @@ export class CDN { */ private makeURL( route: string, - { allowedExtensions = ALLOWED_EXTENSIONS, extension = 'webp', size }: Readonly = {}, + { + allowedExtensions = ALLOWED_EXTENSIONS, + base = this.cdn, + extension = 'webp', + size, + }: Readonly = {}, ): string { // eslint-disable-next-line no-param-reassign extension = String(extension).toLowerCase(); @@ -365,7 +384,7 @@ export class CDN { throw new RangeError(`Invalid size provided: ${size}\nMust be one of: ${ALLOWED_SIZES.join(', ')}`); } - const url = new URL(`${this.base}${route}.${extension}`); + const url = new URL(`${base}${route}.${extension}`); if (size) { url.searchParams.set('size', String(size)); diff --git a/packages/rest/src/lib/REST.ts b/packages/rest/src/lib/REST.ts index a6258f677713..e9d9b0f5da8c 100644 --- a/packages/rest/src/lib/REST.ts +++ b/packages/rest/src/lib/REST.ts @@ -75,7 +75,7 @@ export class REST extends AsyncEventEmitter { public constructor(options: Partial = {}) { super(); - this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn); + this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn, options.mediaProxy ?? DefaultRestOptions.mediaProxy); this.options = { ...DefaultRestOptions, ...options }; this.globalRemaining = Math.max(1, this.options.globalRequestsPerSecond); this.agent = options.agent ?? null; diff --git a/packages/rest/src/lib/utils/constants.ts b/packages/rest/src/lib/utils/constants.ts index ae56bd437e92..91375b5a6df5 100644 --- a/packages/rest/src/lib/utils/constants.ts +++ b/packages/rest/src/lib/utils/constants.ts @@ -31,6 +31,7 @@ export const DefaultRestOptions = { async makeRequest(...args): Promise { return getDefaultStrategy()(...args); }, + mediaProxy: 'https://media.discordapp.net', } as const satisfies Required; /** diff --git a/packages/rest/src/lib/utils/types.ts b/packages/rest/src/lib/utils/types.ts index 90a0e9f34a93..e45d6614ff3a 100644 --- a/packages/rest/src/lib/utils/types.ts +++ b/packages/rest/src/lib/utils/types.ts @@ -85,6 +85,12 @@ export interface RESTOptions { * For example, to use global fetch, simply provide `makeRequest: fetch` */ makeRequest(url: string, init: RequestInit): Promise; + /** + * The media proxy path + * + * @defaultValue `'https://media.discordapp.net'` + */ + mediaProxy: string; /** * The extra offset to add to rate limits in milliseconds *