Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support new username system #9512

Merged
merged 23 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/discord.js/src/structures/GuildMember.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Syjalo marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
45 changes: 41 additions & 4 deletions packages/discord.js/src/structures/User.js
Original file line number Diff line number Diff line change
@@ -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}
Expand Down Expand Up @@ -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
Expand All @@ -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
* <info>`'0'`, or a 4-digit stringified number if they're using the legacy username system</info>
Jiralite marked this conversation as resolved.
Show resolved Hide resolved
* @type {?string}
*/
this.discriminator = data.discriminator;
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -188,12 +204,33 @@ class User extends Base {
}

/**
* The Discord "tag" (e.g. `hydrabolt#0001`) for this user
* The tag of this user
* <info>This user's username, or their legacy tag (e.g. `hydrabolt#0001`)
* if they're using the legacy username system</info>
* @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');
Syjalo marked this conversation as resolved.
Show resolved Hide resolved
Syjalo marked this conversation as resolved.
Show resolved Hide resolved
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}
Syjalo marked this conversation as resolved.
Show resolved Hide resolved
* @readonly
*/
get displayName() {
return this.globalName ?? this.username;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/discord.js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserFlagsBitField> | 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;
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions packages/rest/src/lib/CDN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}

/**
Expand Down
11 changes: 10 additions & 1 deletion packages/rest/src/lib/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}