From e801cae5c8cf8f9f7805ee228b73217eabe90fe1 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Sun, 25 May 2025 14:04:32 +0100 Subject: [PATCH 1/3] feat: support animated WebP --- packages/discord.js/src/client/Client.js | 5 +++ .../src/structures/BaseGuildEmoji.js | 2 +- packages/discord.js/src/structures/Emoji.js | 13 ++++++-- packages/discord.js/typings/index.d.ts | 6 ++-- packages/rest/__tests__/CDN.test.ts | 22 +++---------- packages/rest/src/lib/CDN.ts | 33 +++++++++++++++++-- 6 files changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 6dff855a079e..73ee69cefa1f 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -779,6 +779,11 @@ exports.Client = Client; * @see {@link https://discord.js.org/docs/packages/rest/stable/ImageURLOptions:Interface} */ +/** + * @external EmojiURLOptions + * @see {@link https://discord.js.org/docs/packages/rest/stable/EmojiURLOptions:TypeAlias} + */ + /** * @external BaseImageURLOptions * @see {@link https://discord.js.org/docs/packages/rest/stable/BaseImageURLOptions:Interface} diff --git a/packages/discord.js/src/structures/BaseGuildEmoji.js b/packages/discord.js/src/structures/BaseGuildEmoji.js index bb4e8d005e2e..485ff76fcff3 100644 --- a/packages/discord.js/src/structures/BaseGuildEmoji.js +++ b/packages/discord.js/src/structures/BaseGuildEmoji.js @@ -58,7 +58,7 @@ class BaseGuildEmoji extends Emoji { * @method imageURL * @memberof BaseGuildEmoji * @instance - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {EmojiURLOptions} [options={}] Options for the emoji URL * @returns {string} */ diff --git a/packages/discord.js/src/structures/Emoji.js b/packages/discord.js/src/structures/Emoji.js index 8ce73fb045a0..46e737b47c6c 100644 --- a/packages/discord.js/src/structures/Emoji.js +++ b/packages/discord.js/src/structures/Emoji.js @@ -42,11 +42,20 @@ class Emoji extends Base { /** * Returns a URL for the emoji or `null` if this is not a custom emoji. - * @param {ImageURLOptions} [options={}] Options for the image URL + * @param {EmojiURLOptions} [options={}] Options for the emoji URL * @returns {?string} */ imageURL(options = {}) { - return this.id && this.client.rest.cdn.emoji(this.id, this.animated, options); + if (!this.id) return null; + + // Return a dynamic extension depending on whether the emoji is animated. + const resolvedOptions = { extension: options.extension, size: options.size }; + + if (!options.extension || options.extension === 'webp') { + resolvedOptions.animated = options.animated ?? (this.animated || undefined); + } + + return this.client.rest.cdn.emoji(this.id, resolvedOptions); } /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index d22d659306cb..4636362b3388 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -1,6 +1,6 @@ import { ApplicationCommandOptionAllowedChannelTypes, MessageActionRowComponentBuilder } from '@discordjs/builders'; import { Collection, ReadonlyCollection } from '@discordjs/collection'; -import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; +import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions, EmojiURLOptions } from '@discordjs/rest'; import { Awaitable, JSONEncodable } from '@discordjs/util'; import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws'; import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; @@ -590,7 +590,7 @@ export abstract class BaseGuild extends Base { export class BaseGuildEmoji extends Emoji { protected constructor(client: Client, data: APIEmoji, guild: Guild | GuildPreview); - public imageURL(options?: ImageURLOptions): string; + public imageURL(options?: EmojiURLOptions): string; public get url(): string; public available: boolean | null; public get createdAt(): Date; @@ -1267,7 +1267,7 @@ export class Emoji extends Base { public id: Snowflake | null; public name: string | null; public get identifier(): string; - public imageURL(options?: ImageURLOptions): string | null; + public imageURL(options?: EmojiURLOptions): string | null; public get url(): string | null; public toJSON(): unknown; public toString(): string; diff --git a/packages/rest/__tests__/CDN.test.ts b/packages/rest/__tests__/CDN.test.ts index acad9f5b647c..ee271f2a08e3 100644 --- a/packages/rest/__tests__/CDN.test.ts +++ b/packages/rest/__tests__/CDN.test.ts @@ -50,28 +50,16 @@ test('discoverySplash default', () => { expect(cdn.discoverySplash(id, hash)).toEqual(`${baseCDN}/discovery-splashes/${id}/${hash}.webp`); }); -test('emoji static', () => { - expect(cdn.emoji(id, false)).toEqual(`${baseCDN}/emojis/${id}.webp`); -}); - -test('emoji static with JPG extension', () => { - expect(cdn.emoji(id, false, { extension: 'jpg' })).toEqual(`${baseCDN}/emojis/${id}.jpg`); -}); - -test('emoji static with JPG extension with force static', () => { - expect(cdn.emoji(id, false, { extension: 'jpg', forceStatic: true })).toEqual(`${baseCDN}/emojis/${id}.jpg`); +test('emoji', () => { + expect(cdn.emoji(id)).toEqual(`${baseCDN}/emojis/${id}.webp`); }); test('emoji animated', () => { - expect(cdn.emoji(id, true)).toEqual(`${baseCDN}/emojis/${id}.gif`); -}); - -test('emoji animated with JPG extension', () => { - expect(cdn.emoji(id, true, { extension: 'jpg' })).toEqual(`${baseCDN}/emojis/${id}.gif`); + expect(cdn.emoji(id, { animated: true })).toEqual(`${baseCDN}/emojis/${id}.webp?animated=true`); }); -test('emoji animated with JPG extension with force static', () => { - expect(cdn.emoji(id, true, { extension: 'jpg', forceStatic: true })).toEqual(`${baseCDN}/emojis/${id}.jpg`); +test('emoji with GIF format', () => { + expect(cdn.emoji(id, { extension: 'gif' })).toEqual(`${baseCDN}/emojis/${id}.gif`); }); test('guildMemberAvatar default', () => { diff --git a/packages/rest/src/lib/CDN.ts b/packages/rest/src/lib/CDN.ts index 9b59b3afe8b4..357aa746b52f 100644 --- a/packages/rest/src/lib/CDN.ts +++ b/packages/rest/src/lib/CDN.ts @@ -26,6 +26,25 @@ export interface BaseImageURLOptions { size?: ImageSize; } +interface EmojiURLOptionsWebp extends BaseImageURLOptions { + /** + * Whether to use the `animated` query parameter. + * + * @remarks An animated custom emoji with the WebP format utilises this query paramter to be animated. + */ + animated?: boolean; + extension?: 'webp'; +} + +interface EmojiURLOptionsNotWebP extends BaseImageURLOptions { + extension: Exclude; +} + +/** + * The options used for emoji URLs. + */ +export type EmojiURLOptions = EmojiURLOptionsNotWebP | EmojiURLOptionsWebp; + /** * The options used for image URLs with animated content */ @@ -44,6 +63,10 @@ export interface MakeURLOptions { * The allowed extensions that can be used */ allowedExtensions?: readonly string[]; + /** + * Whether to use the `animated` query parameter + */ + animated?: boolean; /** * The base URL. * @@ -162,11 +185,10 @@ export class CDN { * Generates an emoji's URL. * * @param emojiId - The emoji id - * @param animated - Whether the emoji is animated * @param options - Optional options for the emoji */ - public emoji(emojiId: string, animated: boolean, options?: Readonly): string { - return this.dynamicMakeURL(`/emojis/${emojiId}`, animated ? 'a_' : '', options); + public emoji(emojiId: string, options?: Readonly): string { + return this.makeURL(`/emojis/${emojiId}`, options); } /** @@ -326,6 +348,7 @@ export class CDN { base = this.cdn, extension = 'webp', size, + animated, }: Readonly = {}, ): string { // eslint-disable-next-line no-param-reassign @@ -345,6 +368,10 @@ export class CDN { url.searchParams.set('size', String(size)); } + if (animated !== undefined) { + url.searchParams.set('animated', String(animated)); + } + return url.toString(); } } From 2150819e261112b73eaf8484f83b7c7db42d1049 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Sun, 25 May 2025 14:35:45 +0100 Subject: [PATCH 2/3] refactor: change the rest --- packages/rest/__tests__/CDN.test.ts | 12 ++++---- packages/rest/src/lib/CDN.ts | 46 +++++++++++++++++------------ 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/rest/__tests__/CDN.test.ts b/packages/rest/__tests__/CDN.test.ts index ee271f2a08e3..691d1f28da67 100644 --- a/packages/rest/__tests__/CDN.test.ts +++ b/packages/rest/__tests__/CDN.test.ts @@ -23,7 +23,7 @@ test('avatar default', () => { }); test('avatar dynamic-animated', () => { - expect(cdn.avatar(id, animatedHash)).toEqual(`${baseCDN}/avatars/${id}/${animatedHash}.gif`); + expect(cdn.avatar(id, animatedHash)).toEqual(`${baseCDN}/avatars/${id}/${animatedHash}.webp?animated=true`); }); test('avatar dynamic-not-animated', () => { @@ -68,7 +68,7 @@ test('guildMemberAvatar default', () => { test('guildMemberAvatar dynamic-animated', () => { expect(cdn.guildMemberAvatar(id, id, animatedHash)).toEqual( - `${baseCDN}/guilds/${id}/users/${id}/avatars/${animatedHash}.gif`, + `${baseCDN}/guilds/${id}/users/${id}/avatars/${animatedHash}.webp?animated=true`, ); }); @@ -82,7 +82,7 @@ test('guildMemberBanner default', () => { test('guildMemberBanner dynamic-animated', () => { expect(cdn.guildMemberBanner(id, id, animatedHash)).toEqual( - `${baseCDN}/guilds/${id}/users/${id}/banners/${animatedHash}.gif`, + `${baseCDN}/guilds/${id}/users/${id}/banners/${animatedHash}.webp?animated=true`, ); }); @@ -99,7 +99,7 @@ test('icon default', () => { }); test('icon dynamic-animated', () => { - expect(cdn.icon(id, animatedHash)).toEqual(`${baseCDN}/icons/${id}/${animatedHash}.gif`); + expect(cdn.icon(id, animatedHash)).toEqual(`${baseCDN}/icons/${id}/${animatedHash}.webp?animated=true`); }); test('icon dynamic-not-animated', () => { @@ -145,5 +145,7 @@ test('makeURL throws on invalid extension', () => { }); test('makeURL valid size', () => { - expect(cdn.avatar(id, animatedHash, { size: 512 })).toEqual(`${baseCDN}/avatars/${id}/${animatedHash}.gif?size=512`); + expect(cdn.avatar(id, animatedHash, { size: 512 })).toEqual( + `${baseCDN}/avatars/${id}/${animatedHash}.webp?animated=true&size=512`, + ); }); diff --git a/packages/rest/src/lib/CDN.ts b/packages/rest/src/lib/CDN.ts index 357aa746b52f..273043c5fe73 100644 --- a/packages/rest/src/lib/CDN.ts +++ b/packages/rest/src/lib/CDN.ts @@ -11,54 +11,62 @@ import { } from './utils/constants.js'; /** - * The options used for image URLs + * The options used for image URLs. */ export interface BaseImageURLOptions { /** - * The extension to use for the image URL + * The extension to use for the image URL. * * @defaultValue `'webp'` */ extension?: ImageExtension; /** - * The size specified in the image URL + * The size specified in the image URL. */ size?: ImageSize; } -interface EmojiURLOptionsWebp extends BaseImageURLOptions { +export interface EmojiURLOptionsWebp extends BaseImageURLOptions { /** * Whether to use the `animated` query parameter. - * - * @remarks An animated custom emoji with the WebP format utilises this query paramter to be animated. */ animated?: boolean; extension?: 'webp'; } -interface EmojiURLOptionsNotWebP extends BaseImageURLOptions { +export interface EmojiURLOptionsNotWebp extends BaseImageURLOptions { extension: Exclude; } /** * The options used for emoji URLs. */ -export type EmojiURLOptions = EmojiURLOptionsNotWebP | EmojiURLOptionsWebp; +export type EmojiURLOptions = EmojiURLOptionsNotWebp | EmojiURLOptionsWebp; -/** - * The options used for image URLs with animated content - */ -export interface ImageURLOptions extends BaseImageURLOptions { +export interface BaseAnimatedImageURLOptions extends BaseImageURLOptions { /** - * Whether or not to prefer the static version of an image asset. + * Whether to prefer the static asset. */ forceStatic?: boolean; } +export interface ImageURLOptionsWebp extends BaseAnimatedImageURLOptions { + extension?: 'webp'; +} + +export interface ImageURLOptionsNotWebp extends BaseAnimatedImageURLOptions { + extension: Exclude; +} + +/** + * The options used for image URLs that may be animated. + */ +export type ImageURLOptions = ImageURLOptionsNotWebp | ImageURLOptionsWebp; + /** * The options to use when making a CDN URL */ -export interface MakeURLOptions { +interface MakeURLOptions { /** * The allowed extensions that can be used */ @@ -332,7 +340,7 @@ export class CDN { hash: string, { forceStatic = false, ...options }: Readonly = {}, ): string { - return this.makeURL(route, !forceStatic && hash.startsWith('a_') ? { ...options, extension: 'gif' } : options); + return this.makeURL(route, !forceStatic && hash.startsWith('a_') ? { ...options, animated: true } : options); } /** @@ -364,14 +372,14 @@ export class CDN { const url = new URL(`${base}${route}.${extension}`); - if (size) { - url.searchParams.set('size', String(size)); - } - if (animated !== undefined) { url.searchParams.set('animated', String(animated)); } + if (size) { + url.searchParams.set('size', String(size)); + } + return url.toString(); } } From 05ed86f9ebcd8f9060f3bd2f03ba48dc3a629bc9 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Sun, 25 May 2025 18:46:18 +0100 Subject: [PATCH 3/3] fix: remove redundant code --- packages/rest/src/lib/CDN.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/rest/src/lib/CDN.ts b/packages/rest/src/lib/CDN.ts index 273043c5fe73..319d2bf2d934 100644 --- a/packages/rest/src/lib/CDN.ts +++ b/packages/rest/src/lib/CDN.ts @@ -43,26 +43,16 @@ export interface EmojiURLOptionsNotWebp extends BaseImageURLOptions { */ export type EmojiURLOptions = EmojiURLOptionsNotWebp | EmojiURLOptionsWebp; -export interface BaseAnimatedImageURLOptions extends BaseImageURLOptions { +/** + * The options used for image URLs that may be animated. + */ +export interface ImageURLOptions extends BaseImageURLOptions { /** * Whether to prefer the static asset. */ forceStatic?: boolean; } -export interface ImageURLOptionsWebp extends BaseAnimatedImageURLOptions { - extension?: 'webp'; -} - -export interface ImageURLOptionsNotWebp extends BaseAnimatedImageURLOptions { - extension: Exclude; -} - -/** - * The options used for image URLs that may be animated. - */ -export type ImageURLOptions = ImageURLOptionsNotWebp | ImageURLOptionsWebp; - /** * The options to use when making a CDN URL */