diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index c17d4944e..c6bf38717 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -32,7 +32,7 @@ class ClientDataResolver { * * A Message object (resolves to the message author) * * A Guild object (owner of the guild) * * A GuildMember object - * @typedef {User|string|Message|Guild|GuildMember} UserResolvable + * @typedef {User|Snowflake|Message|Guild|GuildMember} UserResolvable */ /** @@ -52,7 +52,7 @@ class ClientDataResolver { /** * Resolves a UserResolvable to a user ID string * @param {UserResolvable} user The UserResolvable to identify - * @returns {?string} + * @returns {?Snowflake} */ resolveUserID(user) { if (user instanceof User || user instanceof GuildMember) return user.id; @@ -66,7 +66,7 @@ class ClientDataResolver { * Data that resolves to give a Guild object. This can be: * * A Guild object * * A Guild ID - * @typedef {Guild|string} GuildResolvable + * @typedef {Guild|Snowflake} GuildResolvable */ /** @@ -107,7 +107,7 @@ class ClientDataResolver { * * A Message object (the channel the message was sent in) * * A Guild object (the #general channel) * * A channel ID - * @typedef {Channel|Guild|Message|string} ChannelResolvable + * @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable */ /** @@ -126,7 +126,7 @@ class ClientDataResolver { /** * Resolves a ChannelResolvable to a Channel object * @param {ChannelResolvable} channel The channel resolvable to resolve - * @returns {?string} + * @returns {?Snowflake} */ resolveChannelID(channel) { if (channel instanceof Channel) return channel.id; diff --git a/src/index.js b/src/index.js index 85326bfc1..07cfbf39b 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ module.exports = { splitMessage: require('./util/SplitMessage'), escapeMarkdown: require('./util/EscapeMarkdown'), fetchRecommendedShards: require('./util/FetchRecommendedShards'), + Snowflake: require('./util/Snowflake'), Channel: require('./structures/Channel'), ClientOAuth2Application: require('./structures/ClientOAuth2Application'), diff --git a/src/structures/Channel.js b/src/structures/Channel.js index b37b14b41..6b42b2cd6 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -27,7 +27,7 @@ class Channel { setup(data) { /** * The unique ID of the channel - * @type {string} + * @type {Snowflake} */ this.id = data.id; } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 67e19f9c2..22140bee8 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -26,21 +26,21 @@ class ClientUser extends User { /** * A Collection of friends for the logged in user. * This is only filled when using a user account. - * @type {Collection} + * @type {Collection} */ this.friends = new Collection(); /** * A Collection of blocked users for the logged in user. * This is only filled when using a user account. - * @type {Collection} + * @type {Collection} */ this.blocked = new Collection(); /** * A Collection of notes for the logged in user. * This is only filled when using a user account. - * @type {Collection} + * @type {Collection} */ this.notes = new Collection(); @@ -229,7 +229,7 @@ class ClientUser extends User { * @param {number} [options.limit=25] Maximum number of mentions to retrieve * @param {boolean} [options.roles=true] Whether to include role mentions * @param {boolean} [options.everyone=true] Whether to include everyone/here mentions - * @param {Guild|string} [options.guild] Limit the search to a specific guild + * @param {Guild|Snowflake} [options.guild] Limit the search to a specific guild * @returns {Promise} */ fetchMentions(options = { limit: 25, roles: true, everyone: true, guild: null }) { diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 6cd39f661..519092c28 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -26,7 +26,7 @@ class Emoji { setup(data) { /** * The ID of the emoji - * @type {string} + * @type {Snowflake} */ this.id = data.id; @@ -71,7 +71,7 @@ class Emoji { /** * A collection of roles this emoji is active for (empty if all), mapped by role ID. - * @type {Collection} + * @type {Collection} * @readonly */ get roles() { diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index d6b5338d4..cf6739fd3 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -61,7 +61,7 @@ class GroupDMChannel extends Channel { if (!this.recipients) { /** * A collection of the recipients of this DM, mapped by their ID. - * @type {Collection} + * @type {Collection} */ this.recipients = new Collection(); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index e3c50b00e..8b5cb2d3c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -25,25 +25,25 @@ class Guild { /** * A collection of members that are in this guild. The key is the member's ID, the value is the member. - * @type {Collection} + * @type {Collection} */ this.members = new Collection(); /** * A collection of channels that are in this guild. The key is the channel's ID, the value is the channel. - * @type {Collection} + * @type {Collection} */ this.channels = new Collection(); /** * A collection of roles that are in this guild. The key is the role's ID, the value is the role. - * @type {Collection} + * @type {Collection} */ this.roles = new Collection(); /** * A collection of presences in this guild - * @type {Collection} + * @type {Collection} */ this.presences = new Collection(); @@ -57,7 +57,7 @@ class Guild { /** * The Unique ID of the Guild, useful for comparisons. - * @type {string} + * @type {Snowflake} */ this.id = data.id; } else { @@ -116,13 +116,13 @@ class Guild { /** * The ID of the application that created this guild (if applicable) - * @type {?string} + * @type {?Snowflake} */ this.applicationID = data.application_id; /** * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji. - * @type {Collection} + * @type {Collection} */ this.emojis = new Collection(); for (const emoji of data.emojis) this.emojis.set(emoji.id, new Emoji(this, emoji)); @@ -169,7 +169,7 @@ class Guild { if (data.owner_id) { /** * The user ID of this guild's owner. - * @type {string} + * @type {Snowflake} */ this.ownerID = data.owner_id; } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index a93078972..ef5687c43 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -37,7 +37,7 @@ class GuildChannel extends Channel { /** * A map of permission overwrites in this channel for roles and users. - * @type {Collection} + * @type {Collection} */ this.permissionOverwrites = new Collection(); if (data.permission_overwrites) { diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 60a498a3e..c3cb86cdb 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -36,7 +36,7 @@ class GuildMember { /** * The ID of the last message sent by the member in their guild, if one was sent. - * @type {?string} + * @type {?Snowflake} */ this.lastMessageID = null; } @@ -68,13 +68,13 @@ class GuildMember { /** * The voice session ID of this member, if any - * @type {?string} + * @type {?Snowflake} */ this.voiceSessionID = data.session_id; /** * The voice channel ID of this member, if any - * @type {?string} + * @type {?Snowflake} */ this.voiceChannelID = data.channel_id; @@ -120,7 +120,7 @@ class GuildMember { /** * A list of roles that are applied to this GuildMember, mapped by the role ID. - * @type {Collection} + * @type {Collection} * @readonly */ get roles() { @@ -175,7 +175,7 @@ class GuildMember { /** * The ID of this user - * @type {string} + * @type {Snowflake} * @readonly */ get id() { diff --git a/src/structures/Message.js b/src/structures/Message.js index cf4c481f0..7d6708f10 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -33,7 +33,7 @@ class Message { setup(data) { // eslint-disable-line complexity /** * The ID of the message (unique in the channel it was sent) - * @type {string} + * @type {Snowflake} */ this.id = data.id; @@ -75,7 +75,7 @@ class Message { this.tts = data.tts; /** - * A random number used for checking message delivery + * A random number or string used for checking message delivery * @type {string} */ this.nonce = data.nonce; @@ -94,7 +94,7 @@ class Message { /** * A collection of attachments in the message - e.g. Pictures - mapped by their ID. - * @type {Collection} + * @type {Collection} */ this.attachments = new Collection(); for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment)); @@ -114,9 +114,9 @@ class Message { /** * An object containing a further users, roles or channels collections * @type {Object} - * @property {Collection} mentions.users Mentioned users, maps their ID to the user object. - * @property {Collection} mentions.roles Mentioned roles, maps their ID to the role object. - * @property {Collection} mentions.channels Mentioned channels, + * @property {Collection} mentions.users Mentioned users, maps their ID to the user object. + * @property {Collection} mentions.roles Mentioned roles, maps their ID to the role object. + * @property {Collection} mentions.channels Mentioned channels, * maps their ID to the channel object. * @property {boolean} mentions.everyone Whether or not @everyone was mentioned. */ @@ -156,7 +156,7 @@ class Message { /** * A collection of reactions to this message, mapped by the reaction "id". - * @type {Collection} + * @type {Collection} */ this.reactions = new Collection(); @@ -169,7 +169,7 @@ class Message { /** * ID of the webhook that sent the message, if applicable - * @type {?string} + * @type {?Snowflake} */ this.webhookID = data.webhook_id || null; diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 29dfb524e..6bd1030f9 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -23,7 +23,7 @@ class MessageAttachment { setup(data) { /** * The ID of this attachment - * @type {string} + * @type {Snowflake} */ this.id = data.id; diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index f84ecbda3..54e9b1539 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -61,7 +61,7 @@ class MessageCollector extends EventEmitter { /** * A collection of collected messages, mapped by message ID. - * @type {Collection} + * @type {Collection} */ this.collected = new Collection(); diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 30c555f9d..eada0d7a5 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -27,7 +27,7 @@ class MessageReaction { /** * The users that have given this reaction, mapped by their ID. - * @type {Collection} + * @type {Collection} */ this.users = new Collection(); diff --git a/src/structures/OAuth2Application.js b/src/structures/OAuth2Application.js index b7c728581..cbe162466 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/OAuth2Application.js @@ -17,7 +17,7 @@ class OAuth2Application { setup(data) { /** * The ID of the app - * @type {string} + * @type {Snowflake} */ this.id = data.id; diff --git a/src/structures/PartialGuild.js b/src/structures/PartialGuild.js index 407212e2f..e16f67bee 100644 --- a/src/structures/PartialGuild.js +++ b/src/structures/PartialGuild.js @@ -24,7 +24,7 @@ class PartialGuild { setup(data) { /** * The ID of this guild - * @type {string} + * @type {Snowflake} */ this.id = data.id; diff --git a/src/structures/PartialGuildChannel.js b/src/structures/PartialGuildChannel.js index e58a6bb91..13cf4c2f9 100644 --- a/src/structures/PartialGuildChannel.js +++ b/src/structures/PartialGuildChannel.js @@ -23,7 +23,7 @@ class PartialGuildChannel { setup(data) { /** * The ID of this guild channel - * @type {string} + * @type {Snowflake} */ this.id = data.id; diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 9b2f536f5..8044be45a 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -17,7 +17,7 @@ class PermissionOverwrites { setup(data) { /** * The ID of this overwrite, either a user ID or a role ID - * @type {string} + * @type {Snowflake} */ this.id = data.id; diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index 58d00b9b8..b504a3635 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -19,7 +19,7 @@ class ReactionEmoji { /** * The ID of this reaction emoji. - * @type {string} + * @type {?Snowflake} */ this.id = id; } diff --git a/src/structures/Role.js b/src/structures/Role.js index c15ff4be0..329a2264c 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -25,7 +25,7 @@ class Role { setup(data) { /** * The ID of the role (unique to the guild it is part of) - * @type {string} + * @type {Snowflake} */ this.id = data.id; @@ -103,7 +103,7 @@ class Role { /** * The cached guild members that have this role. - * @type {Collection} + * @type {Collection} * @readonly */ get members() { diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 6e98e9e89..38e6a56be 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -29,7 +29,7 @@ class TextChannel extends GuildChannel { /** * A collection of members that can see this channel, mapped by their ID. - * @type {Collection} + * @type {Collection} * @readonly */ get members() { diff --git a/src/structures/User.js b/src/structures/User.js index fd0a8d305..6beb3f352 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -22,7 +22,7 @@ class User { setup(data) { /** * The ID of the user - * @type {string} + * @type {Snowflake} */ this.id = data.id; @@ -52,7 +52,7 @@ class User { /** * The ID of the last message sent by the user, if one was sent. - * @type {?string} + * @type {?Snowflake} */ this.lastMessageID = null; } diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 848a6d513..ad18a94b0 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -11,7 +11,7 @@ class VoiceChannel extends GuildChannel { /** * The members in this voice channel. - * @type {Collection} + * @type {Collection} */ this.members = new Collection(); diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 96984ffe0..153d8f3a5 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -43,19 +43,19 @@ class Webhook { /** * The ID of the webhook - * @type {string} + * @type {Snowflake} */ this.id = data.id; /** * The guild the webhook belongs to - * @type {string} + * @type {Snowflake} */ this.guildID = data.guild_id; /** * The channel the webhook belongs to - * @type {string} + * @type {Snowflake} */ this.channelID = data.channel_id; diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 0daf8600a..5e06cb543 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -11,13 +11,13 @@ class TextBasedChannel { constructor() { /** * A collection containing the messages sent to this channel. - * @type {Collection} + * @type {Collection} */ this.messages = new Collection(); /** * The ID of the last message in the channel, if one was sent. - * @type {?string} + * @type {?Snowflake} */ this.lastMessageID = null; } diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js new file mode 100644 index 000000000..ad8259977 --- /dev/null +++ b/src/util/Snowflake.js @@ -0,0 +1,62 @@ +const Long = require('long'); + +// Discord epoch (2015-01-01T00:00:00.000Z) +const EPOCH = 1420070400000; +let INCREMENT = 0; + +function pad(v, n, c = '0') { + return String(v).length >= n ? String(v) : (String(c).repeat(n) + v).slice(-n); +} + + /** + * A deconstructed snowflake + * @typedef {Object} DeconstructedSnowflake + * @property {Date} date Date in the snowflake + * @property {number} workerID Worker id in the snowflake + * @property {number} processID Process id in the snowflake + * @property {number} increment Increment in the snowflake + * @property {string} binary Binary representation of the snowflake + */ + + /** + * A 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 + * ``` + * Note: this generator hardcodes the worker id as 1 and the process id as 0 + * @typedef {string} Snowflake + * @class Snowflake + */ +class Snowflake { + /** + * Generate a Discord snowflake + * @returns {Snowflake} The generated snowflake + */ + static generate() { + if (INCREMENT >= 4095) INCREMENT = 0; + const BINARY = `${pad((Date.now() - EPOCH).toString(2), 42)}0000100000${pad((INCREMENT++).toString(2), 12)}`; + return Long.fromString(BINARY, 2).toString(); + } + + /** + * Deconstruct a Discord snowflake + * @param {Snowflake} snowflake Snowflake to deconstruct + * @returns {DeconstructedSnowflake} Deconstructed snowflake + */ + static deconstruct(snowflake) { + const BINARY = pad(Long.fromString(snowflake).toString(2), 64); + return { + date: new Date(parseInt(BINARY.substring(0, 42), 2) + EPOCH), + workerID: parseInt(BINARY.substring(42, 47), 2), + processID: parseInt(BINARY.substring(47, 52), 2), + increment: parseInt(BINARY.substring(52, 64), 2), + binary: BINARY, + }; + } +} + +module.exports = Snowflake;