diff --git a/packages/discord.js/src/structures/GuildMember.js b/packages/discord.js/src/structures/GuildMember.js index 3e71824f3f12..8806b508b555 100644 --- a/packages/discord.js/src/structures/GuildMember.js +++ b/packages/discord.js/src/structures/GuildMember.js @@ -238,12 +238,12 @@ class GuildMember extends Base { } /** - * The nickname of this member, or their username if they don't have one + * The nickname of this member, or their user display name if they don't have one * @type {?string} * @readonly */ get displayName() { - return this.nickname ?? this.user.username; + return this.nickname ?? this.user.displayName; } /** diff --git a/packages/discord.js/src/structures/User.js b/packages/discord.js/src/structures/User.js index c784e4e95eb4..092cc3c6ffcc 100644 --- a/packages/discord.js/src/structures/User.js +++ b/packages/discord.js/src/structures/User.js @@ -1,11 +1,15 @@ 'use strict'; +const process = require('node:process'); const { userMention } = require('@discordjs/builders'); +const { calculateUserDefaultAvatarIndex } = require('@discordjs/rest'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const UserFlagsBitField = require('../util/UserFlagsBitField'); +let tagDeprecationEmitted = false; + /** * Represents a user on Discord. * @implements {TextBasedChannel} @@ -41,6 +45,16 @@ class User extends Base { this.username ??= null; } + if ('global_name' in data) { + /** + * The global name of this user + * @type {?string} + */ + this.globalName = data.global_name; + } else { + this.globalName ??= null; + } + if ('bot' in data) { /** * Whether or not the user is a bot @@ -53,7 +67,8 @@ class User extends Base { if ('discriminator' in data) { /** - * A discriminator based on username for the user + * The discriminator of this user + * `'0'`, or a 4-digit stringified number if they're using the legacy username system * @type {?string} */ this.discriminator = data.discriminator; @@ -154,7 +169,8 @@ class User extends Base { * @readonly */ get defaultAvatarURL() { - return this.client.rest.cdn.defaultAvatar(this.discriminator % 5); + const index = this.discriminator === '0' ? calculateUserDefaultAvatarIndex(this.id) : this.discriminator % 5; + return this.client.rest.cdn.defaultAvatar(index); } /** @@ -188,12 +204,33 @@ class User extends Base { } /** - * The Discord "tag" (e.g. `hydrabolt#0001`) for this user + * The tag of this user + * This user's username, or their legacy tag (e.g. `hydrabolt#0001`) + * if they're using the legacy username system * @type {?string} * @readonly + * @deprecated Use {@link User#username} instead. */ get tag() { - return typeof this.username === 'string' ? `${this.username}#${this.discriminator}` : null; + if (!tagDeprecationEmitted) { + process.emitWarning('User#tag is deprecated. Use User#username instead.', 'DeprecationWarning'); + tagDeprecationEmitted = true; + } + + return typeof this.username === 'string' + ? this.discriminator === '0' + ? this.username + : `${this.username}#${this.discriminator}` + : null; + } + + /** + * The global name of this user, or their username if they don't have one + * @type {?string} + * @readonly + */ + get displayName() { + return this.globalName ?? this.username; } /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index c96a54f37db8..c8e8e4b13e8f 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3060,13 +3060,16 @@ export class User extends PartialTextBasedChannel(Base) { public get createdAt(): Date; public get createdTimestamp(): number; public discriminator: string; + public get displayName(): string; public get defaultAvatarURL(): string; public get dmChannel(): DMChannel | null; public flags: Readonly | null; + public globalName: string | null; public get hexAccentColor(): HexColorString | null | undefined; public id: Snowflake; public get partial(): false; public system: boolean; + /** @deprecated Use {@link User#username} instead. */ public get tag(): string; public username: string; public avatarURL(options?: ImageURLOptions): string | null; diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 586580e61fc4..b6af7b7ef7df 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -5,7 +5,7 @@ export * from './lib/errors/RateLimitError.js'; export * from './lib/RequestManager.js'; export * from './lib/REST.js'; export * from './lib/utils/constants.js'; -export { makeURLSearchParams, parseResponse } from './lib/utils/utils.js'; +export { calculateUserDefaultAvatarIndex, makeURLSearchParams, parseResponse } from './lib/utils/utils.js'; /** * The {@link https://github.com/discordjs/discord.js/blob/main/packages/rest/#readme | @discordjs/rest} version diff --git a/packages/rest/src/lib/CDN.ts b/packages/rest/src/lib/CDN.ts index eddde427e469..a42966bd8829 100644 --- a/packages/rest/src/lib/CDN.ts +++ b/packages/rest/src/lib/CDN.ts @@ -119,12 +119,15 @@ export class CDN { } /** - * Generates the default avatar URL for a discriminator. + * Generates a default avatar URL * - * @param discriminator - The discriminator modulo 5 + * @param index - The default avatar index + * @remarks + * To calculate the index for a user do `(userId >> 22) % 6`, + * or `discriminator % 5` if they're using the legacy username system. */ - public defaultAvatar(discriminator: number): string { - return this.makeURL(`/embed/avatars/${discriminator}`, { extension: 'png' }); + public defaultAvatar(index: number): string { + return this.makeURL(`/embed/avatars/${index}`, { extension: 'png' }); } /** diff --git a/packages/rest/src/lib/utils/utils.ts b/packages/rest/src/lib/utils/utils.ts index 0489b02d289d..07d63e542d86 100644 --- a/packages/rest/src/lib/utils/utils.ts +++ b/packages/rest/src/lib/utils/utils.ts @@ -1,5 +1,5 @@ import { URLSearchParams } from 'node:url'; -import type { RESTPatchAPIChannelJSONBody } from 'discord-api-types/v10'; +import type { RESTPatchAPIChannelJSONBody, Snowflake } from 'discord-api-types/v10'; import type { RateLimitData, ResponseLike } from '../REST.js'; import { type RequestManager, RequestMethod } from '../RequestManager.js'; import { RateLimitError } from '../errors/RateLimitError.js'; @@ -112,3 +112,12 @@ export async function onRateLimit(manager: RequestManager, rateLimitData: RateLi throw new RateLimitError(rateLimitData); } } + +/** + * Calculates the default avatar index for a given user id. + * + * @param userId - The user id to calculate the default avatar index for + */ +export function calculateUserDefaultAvatarIndex(userId: Snowflake) { + return Number(BigInt(userId) >> 22n) % 6; +}