diff --git a/packages/core/README.md b/packages/core/README.md index e950109138fc..b41da95e63fa 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -39,10 +39,10 @@ import { WebSocketManager } from '@discordjs/ws'; import { GatewayDispatchEvents, GatewayIntentBits, InteractionType, MessageFlags, Client } from '@discordjs/core'; // Create REST and WebSocket managers directly -const rest = new REST({ version: '10' }).setToken(token); +const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); const gateway = new WebSocketManager({ - token, + token: process.env.DISCORD_TOKEN, intents: GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent, rest, }); diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 907912860f73..07d15c973f6d 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -226,7 +226,7 @@ class Client extends BaseClient { await this.ws.connect(); return this.token; } catch (error) { - this.destroy(); + await this.destroy(); throw error; } } @@ -242,13 +242,13 @@ class Client extends BaseClient { /** * Logs out, terminates the connection to Discord, and destroys the client. - * @returns {void} + * @returns {Promise} */ - destroy() { + async destroy() { super.destroy(); this.sweepers.destroy(); - this.ws.destroy(); + await this.ws.destroy(); this.token = null; this.rest.setToken(null); } diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index b0e4fb10a2ef..e3a2ec749778 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -320,12 +320,12 @@ class WebSocketManager extends EventEmitter { * Destroys this manager and all its shards. * @private */ - destroy() { + async destroy() { if (this.destroyed) return; // TODO: Make a util for getting a stack this.debug(`Manager was destroyed. Called by:\n${new Error().stack}`); this.destroyed = true; - this._ws?.destroy({ code: CloseCodes.Normal }); + await this._ws?.destroy({ code: CloseCodes.Normal }); } /** 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/src/util/Options.js b/packages/discord.js/src/util/Options.js index 7855d3e2703c..9e31308e7590 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -35,7 +35,7 @@ const { version } = require('../../package.json'); * @property {IntentsResolvable} intents Intents to enable for this connection * @property {number} [waitGuildTimeout=15_000] Time in milliseconds that clients with the * {@link GatewayIntentBits.Guilds} gateway intent should wait for missing guilds to be received before being ready. - * @property {SweeperOptions} [sweepers={}] Options for cache sweeping + * @property {SweeperOptions} [sweepers=this.DefaultSweeperSettings] Options for cache sweeping * @property {WebsocketOptions} [ws] Options for the WebSocket * @property {RESTOptions} [rest] Options for the REST manager * @property {Function} [jsonTransformer] A function used to transform outgoing json data diff --git a/packages/discord.js/test/createGuild.js b/packages/discord.js/test/createGuild.js index 90e529a43182..21ac04ea16b5 100644 --- a/packages/discord.js/test/createGuild.js +++ b/packages/discord.js/test/createGuild.js @@ -26,7 +26,7 @@ client.on('ready', async () => { } catch (error) { console.error(error); } finally { - client.destroy(); + await client.destroy(); } }); diff --git a/packages/discord.js/test/shard.js b/packages/discord.js/test/shard.js index a5dfbe8e1a28..f27b30562a79 100644 --- a/packages/discord.js/test/shard.js +++ b/packages/discord.js/test/shard.js @@ -28,9 +28,9 @@ process.send(123); client.on('ready', () => { console.log('Ready', client.options.shards); if (client.options.shards === 0) { - setTimeout(() => { + setTimeout(async () => { console.log('kek dying'); - client.destroy(); + await client.destroy(); }, 5_000); } }); diff --git a/packages/discord.js/test/templateCreateGuild.js b/packages/discord.js/test/templateCreateGuild.js index 42730c567188..adf0e319b8cf 100644 --- a/packages/discord.js/test/templateCreateGuild.js +++ b/packages/discord.js/test/templateCreateGuild.js @@ -20,7 +20,7 @@ client } catch (error) { console.error(error); } finally { - client.destroy(); + await client.destroy(); } }) .login(token) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 277b6fa794e0..27fd856715c0 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -956,7 +956,7 @@ export class Client extends BaseClient { public users: UserManager; public voice: ClientVoiceManager; public ws: WebSocketManager; - public destroy(): void; + public destroy(): Promise; public fetchGuildPreview(guild: GuildResolvable): Promise; public fetchInvite(invite: InviteResolvable, options?: ClientFetchInviteOptions): Promise; public fetchGuildTemplate(template: GuildTemplateResolvable): Promise; @@ -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; @@ -3326,7 +3329,7 @@ export class WebSocketManager extends EventEmitter { private debug(message: string, shardId?: number): void; private connect(): Promise; private broadcast(packet: unknown): void; - private destroy(): void; + private destroy(): Promise; private handlePacket(packet?: unknown, shard?: WebSocketShard): boolean; private checkShardsReady(): void; private triggerClientReady(): void; 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/REST.ts b/packages/rest/src/lib/REST.ts index abfc06560a6e..4b24ab208639 100644 --- a/packages/rest/src/lib/REST.ts +++ b/packages/rest/src/lib/REST.ts @@ -41,7 +41,7 @@ export interface RESTOptions { /** * The cdn path * - * @defaultValue 'https://cdn.discordapp.com' + * @defaultValue `'https://cdn.discordapp.com'` */ cdn: string; /** 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; +}