diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 68b867a45..336aed50c 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -52,6 +52,7 @@ "@discordjs/builders": "^0.11.0", "@discordjs/collection": "^0.4.0", "@sapphire/async-queue": "^1.1.9", + "@sapphire/snowflake": "^3.0.0", "@types/node-fetch": "^2.5.12", "@types/ws": "^8.2.2", "discord-api-types": "^0.26.1", diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 484bf36ca..c26236ee7 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -525,6 +525,19 @@ class Client extends BaseClient { module.exports = Client; +/** + * A {@link https://developer.twitter.com/en/docs/twitter-ids Twitter snowflake}, + * except the epoch is 2015-01-01T00:00:00.000Z. + * + * If we have a snowflake '266241948824764416' we can represent it as binary: + * ``` + * 64 22 17 12 0 + * 000000111011000111100001101001000101000000 00001 00000 000000000000 + * number of ms since Discord epoch worker pid increment + * ``` + * @typedef {string} Snowflake + */ + /** * Emitted for general warnings. * @event Client#warn diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index cc40bfa3c..d734a34b9 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -25,7 +25,7 @@ exports.MessageFlags = require('./util/MessageFlags'); exports.Options = require('./util/Options'); exports.Permissions = require('./util/Permissions'); exports.RateLimitError = require('./rest/RateLimitError'); -exports.SnowflakeUtil = require('./util/SnowflakeUtil'); +exports.SnowflakeUtil = require('@sapphire/snowflake').DiscordSnowflake; exports.Sweepers = require('./util/Sweepers'); exports.SystemChannelFlags = require('./util/SystemChannelFlags'); exports.ThreadMemberFlags = require('./util/ThreadMemberFlags'); diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index 3476978b5..9b4defa6f 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -3,13 +3,13 @@ const { Buffer } = require('node:buffer'); const { setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const CachedManager = require('./CachedManager'); const { Error, TypeError, RangeError } = require('../errors'); const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel'); const { GuildMember } = require('../structures/GuildMember'); const { Role } = require('../structures/Role'); const { Events, Opcodes } = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Manages API methods for GuildMembers and stores their cache. @@ -414,7 +414,7 @@ class GuildMemberManager extends CachedManager { user: user_ids, query, time = 120e3, - nonce = SnowflakeUtil.generate(), + nonce = DiscordSnowflake.generate().toString(), } = {}) { return new Promise((resolve, reject) => { if (!query && !user_ids) query = ''; diff --git a/packages/discord.js/src/structures/ApplicationCommand.js b/packages/discord.js/src/structures/ApplicationCommand.js index ea4558efc..3ac524e5f 100644 --- a/packages/discord.js/src/structures/ApplicationCommand.js +++ b/packages/discord.js/src/structures/ApplicationCommand.js @@ -1,9 +1,9 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager'); const { ApplicationCommandOptionTypes, ApplicationCommandTypes, ChannelTypes } = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents an application command. @@ -103,7 +103,7 @@ class ApplicationCommand extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/BaseGuild.js b/packages/discord.js/src/structures/BaseGuild.js index a39c44e96..16a09467b 100644 --- a/packages/discord.js/src/structures/BaseGuild.js +++ b/packages/discord.js/src/structures/BaseGuild.js @@ -1,7 +1,7 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * The base class for {@link Guild}, {@link OAuth2Guild} and {@link InviteGuild}. @@ -43,7 +43,7 @@ class BaseGuild extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/Channel.js b/packages/discord.js/src/structures/Channel.js index f6f0fd662..4afa76759 100644 --- a/packages/discord.js/src/structures/Channel.js +++ b/packages/discord.js/src/structures/Channel.js @@ -1,5 +1,6 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); let CategoryChannel; let DMChannel; @@ -10,7 +11,6 @@ let TextChannel; let ThreadChannel; let VoiceChannel; const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents any channel on Discord. @@ -45,7 +45,7 @@ class Channel extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/Emoji.js b/packages/discord.js/src/structures/Emoji.js index 2a900fcc9..0df860caa 100644 --- a/packages/discord.js/src/structures/Emoji.js +++ b/packages/discord.js/src/structures/Emoji.js @@ -1,7 +1,7 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents raw emoji data from the API @@ -62,7 +62,7 @@ class Emoji extends Base { * @readonly */ get createdTimestamp() { - return this.id && SnowflakeUtil.timestampFrom(this.id); + return this.id && DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/GuildAuditLogs.js b/packages/discord.js/src/structures/GuildAuditLogs.js index 39371847b..09b6342c8 100644 --- a/packages/discord.js/src/structures/GuildAuditLogs.js +++ b/packages/discord.js/src/structures/GuildAuditLogs.js @@ -1,6 +1,7 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const { GuildScheduledEvent } = require('./GuildScheduledEvent'); const Integration = require('./Integration'); const Invite = require('./Invite'); @@ -8,7 +9,6 @@ const { StageInstance } = require('./StageInstance'); const { Sticker } = require('./Sticker'); const Webhook = require('./Webhook'); const { OverwriteTypes, PartialTypes } = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); const Util = require('../util/Util'); /** @@ -614,7 +614,7 @@ class GuildAuditLogsEntry { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/GuildPreview.js b/packages/discord.js/src/structures/GuildPreview.js index 4627fafa2..544006061 100644 --- a/packages/discord.js/src/structures/GuildPreview.js +++ b/packages/discord.js/src/structures/GuildPreview.js @@ -1,9 +1,9 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const GuildPreviewEmoji = require('./GuildPreviewEmoji'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents the data about the guild any bot can preview, connected to the specified guild. @@ -110,7 +110,7 @@ class GuildPreview extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/Interaction.js b/packages/discord.js/src/structures/Interaction.js index 4da57f31c..2c7739d9f 100644 --- a/packages/discord.js/src/structures/Interaction.js +++ b/packages/discord.js/src/structures/Interaction.js @@ -1,9 +1,9 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const { InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents an interaction. @@ -82,7 +82,7 @@ class Interaction extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 4e2942025..17b387a37 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -1,6 +1,7 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const BaseMessageComponent = require('./BaseMessageComponent'); const ClientApplication = require('./ClientApplication'); @@ -16,7 +17,6 @@ const ReactionManager = require('../managers/ReactionManager'); const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants'); const MessageFlags = require('../util/MessageFlags'); const Permissions = require('../util/Permissions'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); const Util = require('../util/Util'); /** @@ -53,7 +53,7 @@ class Message extends Base { * The timestamp the message was sent at * @type {number} */ - this.createdTimestamp = SnowflakeUtil.timestampFrom(this.id); + this.createdTimestamp = DiscordSnowflake.timestampFrom(this.id); if ('type' in data) { /** diff --git a/packages/discord.js/src/structures/Role.js b/packages/discord.js/src/structures/Role.js index 9db76433c..1a9b93054 100644 --- a/packages/discord.js/src/structures/Role.js +++ b/packages/discord.js/src/structures/Role.js @@ -1,9 +1,9 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const { Error } = require('../errors'); const Permissions = require('../util/Permissions'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); const Util = require('../util/Util'); /** @@ -128,7 +128,7 @@ class Role extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/StageInstance.js b/packages/discord.js/src/structures/StageInstance.js index 1ad5f4e06..a9a0a97e5 100644 --- a/packages/discord.js/src/structures/StageInstance.js +++ b/packages/discord.js/src/structures/StageInstance.js @@ -1,8 +1,8 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const { PrivacyLevels } = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents a stage instance. @@ -132,7 +132,7 @@ class StageInstance extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/Sticker.js b/packages/discord.js/src/structures/Sticker.js index b1af1fbae..f43663fd6 100644 --- a/packages/discord.js/src/structures/Sticker.js +++ b/packages/discord.js/src/structures/Sticker.js @@ -1,8 +1,8 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const { StickerFormatTypes, StickerTypes } = require('../util/Constants'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents a Sticker. @@ -125,7 +125,7 @@ class Sticker extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/StickerPack.js b/packages/discord.js/src/structures/StickerPack.js index 6fd6a5b74..66df3245b 100644 --- a/packages/discord.js/src/structures/StickerPack.js +++ b/packages/discord.js/src/structures/StickerPack.js @@ -1,9 +1,9 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const { Sticker } = require('./Sticker'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents a pack of standard stickers. @@ -61,7 +61,7 @@ class StickerPack extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/Team.js b/packages/discord.js/src/structures/Team.js index ccb817527..7c3d39c2d 100644 --- a/packages/discord.js/src/structures/Team.js +++ b/packages/discord.js/src/structures/Team.js @@ -1,9 +1,9 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const TeamMember = require('./TeamMember'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); /** * Represents a Client OAuth2 Application Team. @@ -76,7 +76,7 @@ class Team extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/User.js b/packages/discord.js/src/structures/User.js index 7a302e341..38c206795 100644 --- a/packages/discord.js/src/structures/User.js +++ b/packages/discord.js/src/structures/User.js @@ -1,8 +1,8 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); const UserFlags = require('../util/UserFlags'); /** @@ -126,7 +126,7 @@ class User extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/Webhook.js b/packages/discord.js/src/structures/Webhook.js index 022270a87..60d80ef9e 100644 --- a/packages/discord.js/src/structures/Webhook.js +++ b/packages/discord.js/src/structures/Webhook.js @@ -1,11 +1,11 @@ 'use strict'; const process = require('node:process'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const MessagePayload = require('./MessagePayload'); const { Error } = require('../errors'); const { WebhookTypes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const SnowflakeUtil = require('../util/SnowflakeUtil'); let deprecationEmittedForFetchMessage = false; @@ -380,7 +380,7 @@ class Webhook { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/interfaces/Application.js b/packages/discord.js/src/structures/interfaces/Application.js index bc3b09c00..e814c8768 100644 --- a/packages/discord.js/src/structures/interfaces/Application.js +++ b/packages/discord.js/src/structures/interfaces/Application.js @@ -1,7 +1,7 @@ 'use strict'; +const { DiscordSnowflake } = require('@sapphire/snowflake'); const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants'); -const SnowflakeUtil = require('../../util/SnowflakeUtil'); const Base = require('../Base'); const AssetTypes = Object.keys(ClientApplicationAssetTypes); @@ -60,7 +60,7 @@ class Application extends Base { * @readonly */ get createdTimestamp() { - return SnowflakeUtil.timestampFrom(this.id); + return DiscordSnowflake.timestampFrom(this.id); } /** diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index 54bb79559..ba97b553e 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -3,8 +3,8 @@ /* eslint-disable import/order */ const MessageCollector = require('../MessageCollector'); const MessagePayload = require('../MessagePayload'); -const SnowflakeUtil = require('../../util/SnowflakeUtil'); const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const { InteractionTypes } = require('../../util/Constants'); const { TypeError, Error } = require('../../errors'); const InteractionCollector = require('../InteractionCollector'); @@ -294,7 +294,7 @@ class TextBasedChannel { if (Array.isArray(messages) || messages instanceof Collection) { let messageIds = messages instanceof Collection ? [...messages.keys()] : messages.map(m => m.id ?? m); if (filterOld) { - messageIds = messageIds.filter(id => Date.now() - SnowflakeUtil.timestampFrom(id) < 1_209_600_000); + messageIds = messageIds.filter(id => Date.now() - DiscordSnowflake.timestampFrom(id) < 1_209_600_000); } if (messageIds.length === 0) return new Collection(); if (messageIds.length === 1) { diff --git a/packages/discord.js/src/util/SnowflakeUtil.js b/packages/discord.js/src/util/SnowflakeUtil.js deleted file mode 100644 index 3e89efd72..000000000 --- a/packages/discord.js/src/util/SnowflakeUtil.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; - -// Discord epoch (2015-01-01T00:00:00.000Z) -const EPOCH = 1_420_070_400_000; -let INCREMENT = BigInt(0); - -/** - * A container for useful snowflake-related methods. - */ -class SnowflakeUtil extends null { - /** - * A {@link https://developer.twitter.com/en/docs/twitter-ids Twitter snowflake}, - * except the epoch is 2015-01-01T00:00:00.000Z. - * - * If we have a snowflake '266241948824764416' we can represent it as binary: - * ``` - * 64 22 17 12 0 - * 000000111011000111100001101001000101000000 00001 00000 000000000000 - * number of ms since Discord epoch worker pid increment - * ``` - * @typedef {string} Snowflake - */ - - /** - * Generates a Discord snowflake. - * This hardcodes the worker's id as 1 and the process's id as 0. - * @param {number|Date} [timestamp=Date.now()] Timestamp or date of the snowflake to generate - * @returns {Snowflake} The generated snowflake - */ - static generate(timestamp = Date.now()) { - if (timestamp instanceof Date) timestamp = timestamp.getTime(); - if (typeof timestamp !== 'number' || isNaN(timestamp)) { - throw new TypeError( - `"timestamp" argument must be a number (received ${isNaN(timestamp) ? 'NaN' : typeof timestamp})`, - ); - } - if (INCREMENT >= 4095n) INCREMENT = BigInt(0); - - // Assign WorkerId as 1 and ProcessId as 0: - return ((BigInt(timestamp - EPOCH) << 22n) | (1n << 17n) | INCREMENT++).toString(); - } - - /** - * A deconstructed snowflake. - * @typedef {Object} DeconstructedSnowflake - * @property {number} timestamp Timestamp the snowflake was created - * @property {Date} date Date the snowflake was created - * @property {number} workerId The worker's id in the snowflake - * @property {number} processId The process's id in the snowflake - * @property {number} increment Increment in the snowflake - * @property {string} binary Binary representation of the snowflake - */ - - /** - * Deconstructs a Discord snowflake. - * @param {Snowflake} snowflake Snowflake to deconstruct - * @returns {DeconstructedSnowflake} - */ - static deconstruct(snowflake) { - const bigIntSnowflake = BigInt(snowflake); - return { - timestamp: Number(bigIntSnowflake >> 22n) + EPOCH, - get date() { - return new Date(this.timestamp); - }, - workerId: Number((bigIntSnowflake >> 17n) & 0b11111n), - processId: Number((bigIntSnowflake >> 12n) & 0b11111n), - increment: Number(bigIntSnowflake & 0b111111111111n), - binary: bigIntSnowflake.toString(2).padStart(64, '0'), - }; - } - - /** - * Retrieves the timestamp field's value from a Discord snowflake. - * @param {Snowflake} snowflake Snowflake to get the timestamp value from - * @returns {number} - */ - static timestampFrom(snowflake) { - return Number(BigInt(snowflake) >> 22n) + EPOCH; - } - - /** - * Discord's epoch value (2015-01-01T00:00:00.000Z). - * @type {number} - * @readonly - */ - static get EPOCH() { - return EPOCH; - } -} - -module.exports = SnowflakeUtil; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 3dbcad2e8..e50124ee8 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2073,13 +2073,11 @@ export interface FetchRecommendedShardsOptions { multipleOf?: number; } -export class SnowflakeUtil extends null { - private constructor(); - public static deconstruct(snowflake: Snowflake): DeconstructedSnowflake; - public static generate(timestamp?: number | Date): Snowflake; - public static timestampFrom(snowflake: Snowflake): number; - public static readonly EPOCH: number; -} +export { + DiscordSnowflake as SnowflakeUtil, + SnowflakeGenerateOptions, + DeconstructedSnowflake, +} from '@sapphire/snowflake'; export class StageChannel extends BaseGuildVoiceChannel { public topic: string | null; @@ -4234,15 +4232,6 @@ export interface CrosspostedChannel { export type DateResolvable = Date | number | string; -export interface DeconstructedSnowflake { - timestamp: number; - readonly date: Date; - workerId: number; - processId: number; - increment: number; - binary: string; -} - export type DefaultMessageNotificationLevel = keyof typeof DefaultMessageNotificationLevels; export type DynamicImageFormat = AllowedImageFormat | 'gif';