Skip to content

Commit 599ad3e

Browse files
fix: Correct base path for GIF stickers (discordjs#10330)
* fix: correct base path for GIF stickers * test: add sticker GIF --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 7f60a8f commit 599ad3e

File tree

5 files changed

+65
-34
lines changed

5 files changed

+65
-34
lines changed

packages/rest/__tests__/CDN.test.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,119 @@
11
import { test, expect } from 'vitest';
22
import { CDN } from '../src/index.js';
33

4-
const base = 'https://discord.com';
4+
const baseCDN = 'https://cdn-discord.com';
5+
const baseMedia = 'https://media-discord.com';
56
const id = '123456';
67
const hash = 'abcdef';
78
const animatedHash = 'a_bcdef';
89
const defaultAvatar = 1_234 % 5;
910

10-
const cdn = new CDN(base);
11+
const cdn = new CDN(baseCDN, baseMedia);
1112

1213
test('appAsset default', () => {
13-
expect(cdn.appAsset(id, hash)).toEqual(`${base}/app-assets/${id}/${hash}.webp`);
14+
expect(cdn.appAsset(id, hash)).toEqual(`${baseCDN}/app-assets/${id}/${hash}.webp`);
1415
});
1516

1617
test('appIcon default', () => {
17-
expect(cdn.appIcon(id, hash)).toEqual(`${base}/app-icons/${id}/${hash}.webp`);
18+
expect(cdn.appIcon(id, hash)).toEqual(`${baseCDN}/app-icons/${id}/${hash}.webp`);
1819
});
1920

2021
test('avatar default', () => {
21-
expect(cdn.avatar(id, hash)).toEqual(`${base}/avatars/${id}/${hash}.webp`);
22+
expect(cdn.avatar(id, hash)).toEqual(`${baseCDN}/avatars/${id}/${hash}.webp`);
2223
});
2324

2425
test('avatar dynamic-animated', () => {
25-
expect(cdn.avatar(id, animatedHash)).toEqual(`${base}/avatars/${id}/${animatedHash}.gif`);
26+
expect(cdn.avatar(id, animatedHash)).toEqual(`${baseCDN}/avatars/${id}/${animatedHash}.gif`);
2627
});
2728

2829
test('avatar dynamic-not-animated', () => {
29-
expect(cdn.avatar(id, hash)).toEqual(`${base}/avatars/${id}/${hash}.webp`);
30+
expect(cdn.avatar(id, hash)).toEqual(`${baseCDN}/avatars/${id}/${hash}.webp`);
3031
});
3132

3233
test('avatar decoration default', () => {
33-
expect(cdn.avatarDecoration(id, hash)).toEqual(`${base}/avatar-decorations/${id}/${hash}.webp`);
34+
expect(cdn.avatarDecoration(id, hash)).toEqual(`${baseCDN}/avatar-decorations/${id}/${hash}.webp`);
3435
});
3536

3637
test('avatar decoration preset', () => {
37-
expect(cdn.avatarDecoration(hash)).toEqual(`${base}/avatar-decoration-presets/${hash}.png`);
38+
expect(cdn.avatarDecoration(hash)).toEqual(`${baseCDN}/avatar-decoration-presets/${hash}.png`);
3839
});
3940

4041
test('banner default', () => {
41-
expect(cdn.banner(id, hash)).toEqual(`${base}/banners/${id}/${hash}.webp`);
42+
expect(cdn.banner(id, hash)).toEqual(`${baseCDN}/banners/${id}/${hash}.webp`);
4243
});
4344

4445
test('channelIcon default', () => {
45-
expect(cdn.channelIcon(id, hash)).toEqual(`${base}/channel-icons/${id}/${hash}.webp`);
46+
expect(cdn.channelIcon(id, hash)).toEqual(`${baseCDN}/channel-icons/${id}/${hash}.webp`);
4647
});
4748

4849
test('defaultAvatar default', () => {
49-
expect(cdn.defaultAvatar(defaultAvatar)).toEqual(`${base}/embed/avatars/${defaultAvatar}.png`);
50+
expect(cdn.defaultAvatar(defaultAvatar)).toEqual(`${baseCDN}/embed/avatars/${defaultAvatar}.png`);
5051
});
5152

5253
test('discoverySplash default', () => {
53-
expect(cdn.discoverySplash(id, hash)).toEqual(`${base}/discovery-splashes/${id}/${hash}.webp`);
54+
expect(cdn.discoverySplash(id, hash)).toEqual(`${baseCDN}/discovery-splashes/${id}/${hash}.webp`);
5455
});
5556

5657
test('emoji default', () => {
57-
expect(cdn.emoji(id)).toEqual(`${base}/emojis/${id}.webp`);
58+
expect(cdn.emoji(id)).toEqual(`${baseCDN}/emojis/${id}.webp`);
5859
});
5960

6061
test('emoji gif', () => {
61-
expect(cdn.emoji(id, 'gif')).toEqual(`${base}/emojis/${id}.gif`);
62+
expect(cdn.emoji(id, 'gif')).toEqual(`${baseCDN}/emojis/${id}.gif`);
6263
});
6364

6465
test('guildMemberAvatar default', () => {
65-
expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${base}/guilds/${id}/users/${id}/avatars/${hash}.webp`);
66+
expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${baseCDN}/guilds/${id}/users/${id}/avatars/${hash}.webp`);
6667
});
6768

6869
test('guildMemberAvatar dynamic-animated', () => {
6970
expect(cdn.guildMemberAvatar(id, id, animatedHash)).toEqual(
70-
`${base}/guilds/${id}/users/${id}/avatars/${animatedHash}.gif`,
71+
`${baseCDN}/guilds/${id}/users/${id}/avatars/${animatedHash}.gif`,
7172
);
7273
});
7374

7475
test('guildMemberAvatar dynamic-not-animated', () => {
75-
expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${base}/guilds/${id}/users/${id}/avatars/${hash}.webp`);
76+
expect(cdn.guildMemberAvatar(id, id, hash)).toEqual(`${baseCDN}/guilds/${id}/users/${id}/avatars/${hash}.webp`);
7677
});
7778

7879
test('guildScheduledEventCover default', () => {
79-
expect(cdn.guildScheduledEventCover(id, hash)).toEqual(`${base}/guild-events/${id}/${hash}.webp`);
80+
expect(cdn.guildScheduledEventCover(id, hash)).toEqual(`${baseCDN}/guild-events/${id}/${hash}.webp`);
8081
});
8182

8283
test('icon default', () => {
83-
expect(cdn.icon(id, hash)).toEqual(`${base}/icons/${id}/${hash}.webp`);
84+
expect(cdn.icon(id, hash)).toEqual(`${baseCDN}/icons/${id}/${hash}.webp`);
8485
});
8586

8687
test('icon dynamic-animated', () => {
87-
expect(cdn.icon(id, animatedHash)).toEqual(`${base}/icons/${id}/${animatedHash}.gif`);
88+
expect(cdn.icon(id, animatedHash)).toEqual(`${baseCDN}/icons/${id}/${animatedHash}.gif`);
8889
});
8990

9091
test('icon dynamic-not-animated', () => {
91-
expect(cdn.icon(id, hash)).toEqual(`${base}/icons/${id}/${hash}.webp`);
92+
expect(cdn.icon(id, hash)).toEqual(`${baseCDN}/icons/${id}/${hash}.webp`);
9293
});
9394

9495
test('role icon default', () => {
95-
expect(cdn.roleIcon(id, hash)).toEqual(`${base}/role-icons/${id}/${hash}.webp`);
96+
expect(cdn.roleIcon(id, hash)).toEqual(`${baseCDN}/role-icons/${id}/${hash}.webp`);
9697
});
9798

9899
test('splash default', () => {
99-
expect(cdn.splash(id, hash)).toEqual(`${base}/splashes/${id}/${hash}.webp`);
100+
expect(cdn.splash(id, hash)).toEqual(`${baseCDN}/splashes/${id}/${hash}.webp`);
100101
});
101102

102103
test('sticker default', () => {
103-
expect(cdn.sticker(id)).toEqual(`${base}/stickers/${id}.png`);
104+
expect(cdn.sticker(id)).toEqual(`${baseCDN}/stickers/${id}.png`);
105+
});
106+
107+
test('sticker GIF', () => {
108+
expect(cdn.sticker(id, 'gif')).toEqual(`${baseMedia}/stickers/${id}.gif`);
104109
});
105110

106111
test('stickerPackBanner default', () => {
107-
expect(cdn.stickerPackBanner(id)).toEqual(`${base}/app-assets/710982414301790216/store/${id}.webp`);
112+
expect(cdn.stickerPackBanner(id)).toEqual(`${baseCDN}/app-assets/710982414301790216/store/${id}.webp`);
108113
});
109114

110115
test('teamIcon default', () => {
111-
expect(cdn.teamIcon(id, hash)).toEqual(`${base}/team-icons/${id}/${hash}.webp`);
116+
expect(cdn.teamIcon(id, hash)).toEqual(`${baseCDN}/team-icons/${id}/${hash}.webp`);
112117
});
113118

114119
test('makeURL throws on invalid size', () => {
@@ -122,5 +127,5 @@ test('makeURL throws on invalid extension', () => {
122127
});
123128

124129
test('makeURL valid size', () => {
125-
expect(cdn.avatar(id, animatedHash, { size: 512 })).toEqual(`${base}/avatars/${id}/${animatedHash}.gif?size=512`);
130+
expect(cdn.avatar(id, animatedHash, { size: 512 })).toEqual(`${baseCDN}/avatars/${id}/${animatedHash}.gif?size=512`);
126131
});

packages/rest/src/lib/CDN.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export interface MakeURLOptions {
4646
* The allowed extensions that can be used
4747
*/
4848
allowedExtensions?: readonly string[];
49+
/**
50+
* The base URL.
51+
*
52+
* @defaultValue `DefaultRestOptions.cdn`
53+
*/
54+
base?: string;
4955
/**
5056
* The extension to use for the image URL
5157
*
@@ -62,7 +68,10 @@ export interface MakeURLOptions {
6268
* The CDN link builder
6369
*/
6470
export class CDN {
65-
public constructor(private readonly base: string = DefaultRestOptions.cdn) {}
71+
public constructor(
72+
private readonly cdn: string = DefaultRestOptions.cdn,
73+
private readonly mediaProxy: string = DefaultRestOptions.mediaProxy,
74+
) {}
6675

6776
/**
6877
* Generates an app asset URL for a client's asset.
@@ -287,10 +296,15 @@ export class CDN {
287296
* @param stickerId - The sticker id
288297
* @param extension - The extension of the sticker
289298
* @privateRemarks
290-
* Stickers cannot have a `.webp` extension, so we default to a `.png`
299+
* Stickers cannot have a `.webp` extension, so we default to a `.png`.
300+
* Sticker GIFs do not use the CDN base URL.
291301
*/
292302
public sticker(stickerId: string, extension: StickerExtension = 'png'): string {
293-
return this.makeURL(`/stickers/${stickerId}`, { allowedExtensions: ALLOWED_STICKER_EXTENSIONS, extension });
303+
return this.makeURL(`/stickers/${stickerId}`, {
304+
allowedExtensions: ALLOWED_STICKER_EXTENSIONS,
305+
base: extension === 'gif' ? this.mediaProxy : this.cdn,
306+
extension,
307+
});
294308
}
295309

296310
/**
@@ -352,7 +366,12 @@ export class CDN {
352366
*/
353367
private makeURL(
354368
route: string,
355-
{ allowedExtensions = ALLOWED_EXTENSIONS, extension = 'webp', size }: Readonly<MakeURLOptions> = {},
369+
{
370+
allowedExtensions = ALLOWED_EXTENSIONS,
371+
base = this.cdn,
372+
extension = 'webp',
373+
size,
374+
}: Readonly<MakeURLOptions> = {},
356375
): string {
357376
// eslint-disable-next-line no-param-reassign
358377
extension = String(extension).toLowerCase();
@@ -365,7 +384,7 @@ export class CDN {
365384
throw new RangeError(`Invalid size provided: ${size}\nMust be one of: ${ALLOWED_SIZES.join(', ')}`);
366385
}
367386

368-
const url = new URL(`${this.base}${route}.${extension}`);
387+
const url = new URL(`${base}${route}.${extension}`);
369388

370389
if (size) {
371390
url.searchParams.set('size', String(size));

packages/rest/src/lib/REST.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class REST extends AsyncEventEmitter<RestEvents> {
7575

7676
public constructor(options: Partial<RESTOptions> = {}) {
7777
super();
78-
this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn);
78+
this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn, options.mediaProxy ?? DefaultRestOptions.mediaProxy);
7979
this.options = { ...DefaultRestOptions, ...options };
8080
this.globalRemaining = Math.max(1, this.options.globalRequestsPerSecond);
8181
this.agent = options.agent ?? null;

packages/rest/src/lib/utils/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const DefaultRestOptions = {
3131
async makeRequest(...args): Promise<ResponseLike> {
3232
return getDefaultStrategy()(...args);
3333
},
34+
mediaProxy: 'https://media.discordapp.net',
3435
} as const satisfies Required<RESTOptions>;
3536

3637
/**

packages/rest/src/lib/utils/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ export interface RESTOptions {
8585
* For example, to use global fetch, simply provide `makeRequest: fetch`
8686
*/
8787
makeRequest(url: string, init: RequestInit): Promise<ResponseLike>;
88+
/**
89+
* The media proxy path
90+
*
91+
* @defaultValue `'https://media.discordapp.net'`
92+
*/
93+
mediaProxy: string;
8894
/**
8995
* The extra offset to add to rate limits in milliseconds
9096
*

0 commit comments

Comments
 (0)