feat!: Support animated WebP (#10911)

* feat: support animated WebP

* refactor: change the rest

* fix: remove redundant code
This commit is contained in:
Jiralite
2025-05-27 10:18:30 +01:00
committed by GitHub
parent 78d512c347
commit 2c35084ecd
6 changed files with 67 additions and 38 deletions

View File

@@ -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}

View File

@@ -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}
*/

View File

@@ -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);
}
/**

View File

@@ -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<true>, 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;

View File

@@ -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', () => {
@@ -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`);
expect(cdn.emoji(id, { animated: true })).toEqual(`${baseCDN}/emojis/${id}.webp?animated=true`);
});
test('emoji animated with JPG extension', () => {
expect(cdn.emoji(id, true, { extension: 'jpg' })).toEqual(`${baseCDN}/emojis/${id}.gif`);
});
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', () => {
@@ -80,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`,
);
});
@@ -94,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`,
);
});
@@ -111,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', () => {
@@ -157,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`,
);
});

View File

@@ -11,27 +11,44 @@ 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;
}
export interface EmojiURLOptionsWebp extends BaseImageURLOptions {
/**
* Whether to use the `animated` query parameter.
*/
animated?: boolean;
extension?: 'webp';
}
export interface EmojiURLOptionsNotWebp extends BaseImageURLOptions {
extension: Exclude<ImageExtension, 'webp'>;
}
/**
* The options used for image URLs with animated content
* The options used for emoji URLs.
*/
export type EmojiURLOptions = EmojiURLOptionsNotWebp | EmojiURLOptionsWebp;
/**
* The options used for image URLs that may be animated.
*/
export interface ImageURLOptions extends BaseImageURLOptions {
/**
* Whether or not to prefer the static version of an image asset.
* Whether to prefer the static asset.
*/
forceStatic?: boolean;
}
@@ -39,11 +56,15 @@ export interface ImageURLOptions extends BaseImageURLOptions {
/**
* The options to use when making a CDN URL
*/
export interface MakeURLOptions {
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 +183,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<ImageURLOptions>): string {
return this.dynamicMakeURL(`/emojis/${emojiId}`, animated ? 'a_' : '', options);
public emoji(emojiId: string, options?: Readonly<EmojiURLOptions>): string {
return this.makeURL(`/emojis/${emojiId}`, options);
}
/**
@@ -310,7 +330,7 @@ export class CDN {
hash: string,
{ forceStatic = false, ...options }: Readonly<ImageURLOptions> = {},
): 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);
}
/**
@@ -326,6 +346,7 @@ export class CDN {
base = this.cdn,
extension = 'webp',
size,
animated,
}: Readonly<MakeURLOptions> = {},
): string {
// eslint-disable-next-line no-param-reassign
@@ -341,6 +362,10 @@ export class CDN {
const url = new URL(`${base}${route}.${extension}`);
if (animated !== undefined) {
url.searchParams.set('animated', String(animated));
}
if (size) {
url.searchParams.set('size', String(size));
}