diff --git a/packages/bot/src/desiredProperties.ts b/packages/bot/src/desiredProperties.ts index 2b3b53bbc..4fe0d5a9c 100644 --- a/packages/bot/src/desiredProperties.ts +++ b/packages/bot/src/desiredProperties.ts @@ -213,6 +213,11 @@ export interface TransformersDesiredPropertiesMetadata extends DesiredProperties system: ['toggles'] mfaEnabled: ['toggles'] verified: ['toggles'] + avatarUrl: ['avatar', 'id'] + displayName: ['username', 'globalName'] + defaultAvatarUrl: ['id', 'discriminator'] + displayAvatarUrl: ['avatar', 'id', 'discriminator'] + createdTimestamp: ['id'] } alwaysPresents: [] } diff --git a/packages/bot/src/transformers/types.ts b/packages/bot/src/transformers/types.ts index f11b229ed..165fa4b26 100644 --- a/packages/bot/src/transformers/types.ts +++ b/packages/bot/src/transformers/types.ts @@ -71,7 +71,7 @@ import type { VideoQualityModes, WebhookTypes, } from '@discordeno/types' -import type { Collection } from '@discordeno/utils' +import type { Collection, ImageOptions } from '@discordeno/utils' import type { Bot } from '../bot.js' import type { InteractionResolvedDataChannel, InteractionResolvedDataMember } from '../commandOptionsParser.js' import type { DesiredPropertiesBehavior, TransformersDesiredProperties } from '../desiredProperties.js' @@ -1762,6 +1762,10 @@ export interface User { username: string /** The user's display name, if it is set. For bots, this is the application name */ globalName?: string + /** The user's display name based on `globalName` and `username` */ + displayName: string + /** Get the timestamp in milliseconds of user's creation date */ + createdTimestamp: number /** The user's chosen language option */ locale?: string /** The flags on a user's account */ @@ -1798,6 +1802,20 @@ export interface User { collectibles?: Collectibles /** The user's primary guild */ primaryGuild?: UserPrimaryGuild + /** Get user's default avatar in formatted url */ + defaultAvatarUrl: string + /** + * Get user's avatar in formatted url + * @param options Image format options + * @returns User's avatar in formatted url + */ + avatarUrl: (options?: ImageOptions) => string | undefined + /** + * Get user's display avatar in formatted url + * @param options Image format options + * @returns User's display avatar in formatted url + */ + displayAvatarUrl: (options?: ImageOptions) => string } export interface Collectibles { diff --git a/packages/bot/src/transformers/user.ts b/packages/bot/src/transformers/user.ts index 00e0dc041..3118a0447 100644 --- a/packages/bot/src/transformers/user.ts +++ b/packages/bot/src/transformers/user.ts @@ -1,5 +1,5 @@ import type { DiscordCollectibles, DiscordNameplate, DiscordUser, DiscordUserPrimaryGuild } from '@discordeno/types' -import { iconHashToBigInt } from '@discordeno/utils' +import { avatarUrl, defaultAvatarUrl, displayAvatarUrl, iconHashToBigInt, snowflakeToTimestamp } from '@discordeno/utils' import type { Bot } from '../bot.js' import type { DesiredPropertiesBehavior, SetupDesiredProps, TransformersDesiredProperties } from '../desiredProperties.js' import { ToggleBitfield } from './toggles/ToggleBitfield.js' @@ -10,6 +10,22 @@ export const baseUser: User = { // This allows typescript to still check for type errors on functions below ...(undefined as unknown as User), + avatarUrl(options) { + if (!this.avatar) return + return avatarUrl(this.id, this.avatar, options) + }, + displayAvatarUrl(options) { + return displayAvatarUrl(this.id, this.discriminator, this.avatar, options) + }, + get defaultAvatarUrl() { + return defaultAvatarUrl(this.id, this.discriminator) + }, + get displayName() { + return this.globalName ?? this.username + }, + get createdTimestamp() { + return snowflakeToTimestamp(this.id) + }, get tag() { const isLegacy = this.discriminator !== '0' && this.discriminator !== '0000' return isLegacy ? `${this.username}#${this.discriminator}` : this.username diff --git a/packages/utils/src/images.ts b/packages/utils/src/images.ts index 418a20ab4..d4f0f65a4 100644 --- a/packages/utils/src/images.ts +++ b/packages/utils/src/images.ts @@ -1,6 +1,11 @@ import { type BigString, type GetGuildWidgetImageQuery, type ImageFormat, type ImageSize, StickerFormatTypes } from '@discordeno/types' import { iconBigintToHash } from './hash.js' +export interface ImageOptions { + size?: ImageSize + format?: ImageFormat +} + /** Help format an image url. */ export function formatImageUrl(url: string, size: ImageSize = 128, format?: ImageFormat): string { return `${url}.${format ?? (url.includes('/a_') ? 'gif' : 'webp')}?size=${size}` @@ -25,26 +30,43 @@ export function emojiUrl(emojiId: BigString, animated = false, format: ImageForm * Builds a URL to a user's avatar stored in the Discord CDN. * * @param userId - The ID of the user to get the avatar of. - * @param discriminator - The user's discriminator. (4-digit tag after the hashtag.) + * @param avatar - The user's avatar hash. * @param options - The parameters for the building of the URL. - * @returns The link to the resource. + * @returns The user avatar as a URL. */ -export function avatarUrl( - userId: BigString, - discriminator: string, - options?: { - avatar: BigString | undefined - size?: ImageSize - format?: ImageFormat - }, -): string { - return options?.avatar - ? formatImageUrl( - `https://cdn.discordapp.com/avatars/${userId}/${typeof options.avatar === 'string' ? options.avatar : iconBigintToHash(options.avatar)}`, - options?.size ?? 128, - options?.format, - ) - : `https://cdn.discordapp.com/embed/avatars/${discriminator === '0' ? (BigInt(userId) >> BigInt(22)) % BigInt(6) : Number(discriminator) % 5}.png` +export function avatarUrl(userId: BigString, avatar: BigString, options?: ImageOptions): string { + return formatImageUrl( + `https://cdn.discordapp.com/avatars/${userId}/${typeof avatar === 'string' ? avatar : iconBigintToHash(avatar)}`, + options?.size ?? 128, + options?.format, + ) +} + +/** + * Builds a URL to a user's default avatar stored in the Discord CDN. + * + * @param userId - The ID of the user to get the avatar of. + * @param discriminator - The user's discriminator. (4-digit tag after the hashtag.) + * @returns The user default avatar as an URL. + */ +export function defaultAvatarUrl(userId: BigString, discriminator: string) { + const isLegacy = discriminator === '0' || discriminator === '0000' + const index = isLegacy ? (BigInt(userId) >> 22n) % 6n : Number(discriminator) % 5 + + return `https://cdn.discordapp.com/embed/avatars/${index}.png` +} + +/** + * Builds a URL to a user's display avatar stored in the Discord CDN. + * + * @param userId - The ID of the user to get the avatar of. + * @param discriminator - The user's discriminator. (4-digit tag after the hashtag.) + * @param avatar - The user's avatar hash. + * @param options - The parameters for the building of the URL. + * @returns The user display avatar as an URL. + */ +export function displayAvatarUrl(userId: BigString, discriminator: string, avatar: BigString | undefined, options?: ImageOptions): string { + return avatar ? avatarUrl(userId, avatar, options) : defaultAvatarUrl(userId, discriminator) } export function avatarDecorationUrl(avatarDecoration: BigString): string { @@ -60,14 +82,7 @@ export function avatarDecorationUrl(avatarDecoration: BigString): string { * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if no banner has not been set. */ -export function bannerUrl( - userId: BigString, - options?: { - banner?: BigString - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function bannerUrl(userId: BigString, options?: ImageOptions & { banner?: BigString }): string | undefined { return options?.banner ? formatImageUrl( `https://cdn.discordapp.com/banners/${userId}/${typeof options.banner === 'string' ? options.banner : iconBigintToHash(options.banner)}`, @@ -84,14 +99,7 @@ export function bannerUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if no banner has been set. */ -export function guildBannerUrl( - guildId: BigString, - options: { - banner?: BigString - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function guildBannerUrl(guildId: BigString, options: ImageOptions & { banner?: BigString }): string | undefined { return options.banner ? formatImageUrl( `https://cdn.discordapp.com/banners/${guildId}/${typeof options.banner === 'string' ? options.banner : iconBigintToHash(options.banner)}`, @@ -109,14 +117,7 @@ export function guildBannerUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if no banner has been set. */ -export function guildIconUrl( - guildId: BigString, - imageHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function guildIconUrl(guildId: BigString, imageHash: BigString | undefined, options?: ImageOptions): string | undefined { return imageHash ? formatImageUrl( `https://cdn.discordapp.com/icons/${guildId}/${typeof imageHash === 'string' ? imageHash : iconBigintToHash(imageHash)}`, @@ -134,14 +135,7 @@ export function guildIconUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if the guild does not have a splash image set. */ -export function guildSplashUrl( - guildId: BigString, - imageHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function guildSplashUrl(guildId: BigString, imageHash: BigString | undefined, options?: ImageOptions): string | undefined { return imageHash ? formatImageUrl( `https://cdn.discordapp.com/splashes/${guildId}/${typeof imageHash === 'string' ? imageHash : iconBigintToHash(imageHash)}`, @@ -159,14 +153,7 @@ export function guildSplashUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if the guild does not have a splash image set. */ -export function guildDiscoverySplashUrl( - guildId: BigString, - imageHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function guildDiscoverySplashUrl(guildId: BigString, imageHash: BigString | undefined, options?: ImageOptions): string | undefined { return imageHash ? formatImageUrl( `https://cdn.discordapp.com/discovery-splashes/${guildId}/${typeof imageHash === 'string' ? imageHash : iconBigintToHash(imageHash)}`, @@ -183,14 +170,7 @@ export function guildDiscoverySplashUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined`. */ -export function guildScheduledEventCoverUrl( - eventId: BigString, - options: { - cover?: BigString - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function guildScheduledEventCoverUrl(eventId: BigString, options: ImageOptions & { cover?: BigString }): string | undefined { return options.cover ? formatImageUrl( `https://cdn.discordapp.com/guild-events/${eventId}/${typeof options.cover === 'string' ? options.cover : iconBigintToHash(options.cover)}`, @@ -225,15 +205,7 @@ export function getWidgetImageUrl(guildId: BigString, options?: GetGuildWidgetIm * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if no banner has been set. */ -export function memberAvatarUrl( - guildId: BigString, - userId: BigString, - options?: { - avatar?: BigString - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function memberAvatarUrl(guildId: BigString, userId: BigString, options?: ImageOptions & { avatar?: BigString }): string | undefined { return options?.avatar ? formatImageUrl( `https://cdn.discordapp.com/guilds/${guildId}/users/${userId}/avatars/${ @@ -253,15 +225,7 @@ export function memberAvatarUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if no banner has been set. */ -export function memberBannerUrl( - guildId: BigString, - userId: BigString, - options?: { - banner?: BigString - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function memberBannerUrl(guildId: BigString, userId: BigString, options?: ImageOptions & { banner?: BigString }): string | undefined { return options?.banner ? formatImageUrl( `https://cdn.discordapp.com/guilds/${guildId}/users/${userId}/banners/${ @@ -281,14 +245,7 @@ export function memberBannerUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` */ -export function applicationIconUrl( - applicationId: BigString, - iconHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function applicationIconUrl(applicationId: BigString, iconHash: BigString | undefined, options?: ImageOptions): string | undefined { return iconHash ? formatImageUrl( `https://cdn.discordapp.com/app-icons/${applicationId}/${typeof iconHash === 'string' ? iconHash : iconBigintToHash(iconHash)}`, @@ -306,14 +263,7 @@ export function applicationIconUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined`. */ -export function applicationCoverUrl( - applicationId: BigString, - coverHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function applicationCoverUrl(applicationId: BigString, coverHash: BigString | undefined, options?: ImageOptions): string | undefined { return coverHash ? formatImageUrl( `https://cdn.discordapp.com/app-icons/${applicationId}/${typeof coverHash === 'string' ? coverHash : iconBigintToHash(coverHash)}`, @@ -331,14 +281,7 @@ export function applicationCoverUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined`. */ -export function applicationAssetUrl( - applicationId: BigString, - assetId: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function applicationAssetUrl(applicationId: BigString, assetId: BigString | undefined, options?: ImageOptions): string | undefined { return assetId ? formatImageUrl( `https://cdn.discordapp.com/app-icons/${applicationId}/${typeof assetId === 'string' ? assetId : iconBigintToHash(assetId)}`, @@ -355,13 +298,7 @@ export function applicationAssetUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined`. */ -export function stickerPackBannerUrl( - bannerAssetId: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function stickerPackBannerUrl(bannerAssetId: BigString | undefined, options?: ImageOptions): string | undefined { return bannerAssetId ? formatImageUrl( `https://cdn.discordapp.com/app-assets/710982414301790216/store/${ @@ -380,14 +317,7 @@ export function stickerPackBannerUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined`. */ -export function stickerUrl( - stickerId: BigString | number, - options?: { - size?: ImageSize - format?: ImageFormat - type?: StickerFormatTypes - }, -): string | undefined { +export function stickerUrl(stickerId: BigString | number, options?: ImageOptions & { type?: StickerFormatTypes }): string | undefined { if (!stickerId) return const url = @@ -406,14 +336,7 @@ export function stickerUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined`. */ -export function teamIconUrl( - teamId: BigString, - iconHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function teamIconUrl(teamId: BigString, iconHash: BigString | undefined, options?: ImageOptions): string | undefined { return iconHash ? formatImageUrl( `https://cdn.discordapp.com/team-icons/${teamId}/store/${typeof iconHash === 'string' ? iconHash : iconBigintToHash(iconHash)}`, @@ -431,14 +354,7 @@ export function teamIconUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined`. */ -export function roleIconUrl( - roleId: BigString, - iconHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function roleIconUrl(roleId: BigString, iconHash: BigString | undefined, options?: ImageOptions): string | undefined { return iconHash ? formatImageUrl( `https://cdn.discordapp.com/role-icons/${roleId}/${typeof iconHash === 'string' ? iconHash : iconBigintToHash(iconHash)}`, @@ -456,14 +372,7 @@ export function roleIconUrl( * @param options - The parameters for the building of the URL. * @returns The link to the resource or `undefined` if no badge has been set. */ -export function guildTagBadgeUrl( - guildId: BigString, - badgeHash: BigString | undefined, - options?: { - size?: ImageSize - format?: ImageFormat - }, -): string | undefined { +export function guildTagBadgeUrl(guildId: BigString, badgeHash: BigString | undefined, options?: ImageOptions): string | undefined { if (badgeHash === undefined) return undefined return formatImageUrl(`https://cdn.discordapp.com/guild-tag-badges/${guildId}/${badgeHash}`, options?.size ?? 128, options?.format) diff --git a/packages/utils/tests/images.spec.ts b/packages/utils/tests/images.spec.ts index c6aec31e5..156cdde87 100644 --- a/packages/utils/tests/images.spec.ts +++ b/packages/utils/tests/images.spec.ts @@ -1,6 +1,16 @@ import { expect } from 'chai' import { describe, it } from 'mocha' -import { avatarUrl, emojiUrl, formatImageUrl, getWidgetImageUrl, guildBannerUrl, guildIconUrl, guildSplashUrl } from '../src/images.js' +import { + avatarUrl, + defaultAvatarUrl, + displayAvatarUrl, + emojiUrl, + formatImageUrl, + getWidgetImageUrl, + guildBannerUrl, + guildIconUrl, + guildSplashUrl, +} from '../src/images.js' describe('images.ts', () => { describe('formatImageUrl function', () => { @@ -55,27 +65,39 @@ describe('images.ts', () => { describe('avatarUrl function', () => { it('will return the url for given avatar icon hash', () => { - expect( - avatarUrl('207324334904049664', '9130', { - avatar: 'db26a6fb924c985f66b79364cf5797b7', - }), - ).to.equal('https://cdn.discordapp.com/avatars/207324334904049664/db26a6fb924c985f66b79364cf5797b7.webp?size=128') + expect(avatarUrl('207324334904049664', 'db26a6fb924c985f66b79364cf5797b7')).to.equal( + 'https://cdn.discordapp.com/avatars/207324334904049664/db26a6fb924c985f66b79364cf5797b7.webp?size=128', + ) }) it('will return the url for given avatar icon bigint', () => { - expect( - avatarUrl('207324334904049664', '9130', { - avatar: 4034407661299384404326332419647968090039n, - }), - ).to.equal('https://cdn.discordapp.com/avatars/207324334904049664/db26a6fb924c985f66b79364cf5797b7.webp?size=128') + expect(avatarUrl('207324334904049664', 4034407661299384404326332419647968090039n)).to.equal( + 'https://cdn.discordapp.com/avatars/207324334904049664/db26a6fb924c985f66b79364cf5797b7.webp?size=128', + ) + }) + }) + + describe('defaultAvatarUrl function', () => { + it('will return the url for default avatar', () => { + expect(defaultAvatarUrl('207324334904049664', '9130')).to.equal('https://cdn.discordapp.com/embed/avatars/0.png') + }) + }) + + describe('displayAvatarUrl function', () => { + it('will return the url for given avatar icon hash', () => { + expect(displayAvatarUrl('207324334904049664', '9130', 'db26a6fb924c985f66b79364cf5797b7')).to.equal( + 'https://cdn.discordapp.com/avatars/207324334904049664/db26a6fb924c985f66b79364cf5797b7.webp?size=128', + ) + }) + + it('will return the url for given avatar icon bigint', () => { + expect(displayAvatarUrl('207324334904049664', '9130', 4034407661299384404326332419647968090039n)).to.equal( + 'https://cdn.discordapp.com/avatars/207324334904049664/db26a6fb924c985f66b79364cf5797b7.webp?size=128', + ) }) it('will return the url for default avatar', () => { - expect( - avatarUrl('207324334904049664', '9130', { - avatar: undefined, - }), - ).to.equal('https://cdn.discordapp.com/embed/avatars/0.png') + expect(displayAvatarUrl('207324334904049664', '9130', undefined)).to.equal('https://cdn.discordapp.com/embed/avatars/0.png') }) })