feat(user)!: add more getters for user transformer (#4661)

* feat: enhance user tag getter

* feat(user): add `0000` condition and better jsdoc for tag

* chore(user): use this keyword instead of destructuring assignment

* fix(user): correct isLegacy logic

* feat(images): add ImageOptions

* refactor(images)!: split avatarUrl into more specific functions and add displayAvatarUrl

* feat(user)!: add more useful getters for user

* fix: update tests for images

* chore: using n suffix for bigint instead of converting to bigint

Co-authored-by: Fleny <Fleny113@outlook.com>

* fix: update dependencies for avatarUrl

Co-authored-by: Fleny <Fleny113@outlook.com>

* fix(user): correct the avatarUrl second parameter to avatar hash

Co-authored-by: Fleny <Fleny113@outlook.com>

* chore: undo unnecessary // from auto comment hotkey

Co-authored-by: Fleny <Fleny113@outlook.com>

---------

Co-authored-by: Fleny <Fleny113@outlook.com>
This commit is contained in:
Louis Johnson
2025-12-31 23:46:11 +07:00
committed by GitHub
parent 875a075f93
commit 288904fef9
5 changed files with 135 additions and 165 deletions

View File

@@ -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: []
}

View File

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

View File

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

View File

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

View File

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