diff --git a/deploy/deploy_key.enc b/deploy/deploy-key.enc similarity index 100% rename from deploy/deploy_key.enc rename to deploy/deploy-key.enc diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 13811122b..97b3539bc 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -47,10 +47,10 @@ ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR} ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} -openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy/deploy_key.enc -out deploy_key -d -chmod 600 deploy_key +openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy/deploy-key.enc -out deploy-key -d +chmod 600 deploy-key eval `ssh-agent -s` -ssh-add deploy_key +ssh-add deploy-key # Checkout the repo in the target branch so we can build docs and push to it TARGET_BRANCH="docs" diff --git a/package.json b/package.json index 44e762eb9..d407dc03b 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,13 @@ "ws": "^1.1.1" }, "peerDependencies": { - "erlpack": "hammerandchisel/erlpack#master", + "erlpack": "hammerandchisel/erlpack", "node-opus": "^0.2.0", "opusscript": "^0.0.1", "uws": "^0.12.0" }, "devDependencies": { - "discord.js-docgen": "hydrabolt/discord.js-docgen#master", + "discord.js-docgen": "hydrabolt/discord.js-docgen", "eslint": "^3.12.0", "parallel-webpack": "^1.6.0", "uglify-js": "mishoo/UglifyJS2#harmony", 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/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index b1b51b4aa..5ecae4610 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -139,9 +139,15 @@ class RESTMethods { } const url = `${Constants.Endpoints[`${type}Search`](target.id)}?${queryString}`; - return this.rest.makeRequest('get', url, true).then(body => - body.messages.map(x => x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))) - ); + return this.rest.makeRequest('get', url, true).then(body => { + const messages = body.messages.map(x => + x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)) + ); + return { + totalResults: body.total_results, + messages, + }; + }); } createChannel(guild, channelName, channelType, overwrites) { diff --git a/src/index.js b/src/index.js index 85326bfc1..d6cdffc97 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,8 @@ module.exports = { splitMessage: require('./util/SplitMessage'), escapeMarkdown: require('./util/EscapeMarkdown'), fetchRecommendedShards: require('./util/FetchRecommendedShards'), + Snowflake: require('./util/Snowflake'), + SnowflakeUtil: 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..5ff0057df 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(); @@ -50,6 +50,27 @@ class ClientUser extends User { * @type {Object} */ this.settings = {}; + + /** + * If the user has discord premium (nitro) + * This is only filled when using a user account. + * @type {?boolean} + */ + this.premium = typeof data.premium === 'boolean' ? data.premium : null; + + /** + * If the user has MFA enabled on their account + * This is only filled when using a user account. + * @type {?boolean} + */ + this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + + /** + * If the user has ever used a mobile device on discord + * This is only filled when using a user account. + * @type {?boolean} + */ + this.mobile = typeof data.mobile === 'boolean' ? data.mobile : null; } edit(data) { @@ -229,7 +250,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 d8a62e17e..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() { @@ -91,6 +91,16 @@ class Emoji { return Constants.Endpoints.emoji(this.id); } + /** + * The identifier of this emoji, used for message reactions + * @type {string} + * @readonly + */ + get identifier() { + if (this.id) return `${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + /** * When concatenated with a string, this automatically returns the emoji mention rather than the object. * @returns {string} @@ -123,18 +133,6 @@ class Emoji { ); } } - - /** - * The identifier of this emoji, used for message reactions - * @readonly - * @type {string} - */ - get identifier() { - if (this.id) { - return `${this.name}:${this.id}`; - } - return encodeURIComponent(this.name); - } } module.exports = Emoji; 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 aa0f717ab..faa5ce55a 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 { @@ -106,7 +106,7 @@ class Guild { * Whether the guild is "large" (has more than 250 members) * @type {boolean} */ - this.large = data.large || this.large; + this.large = Boolean('large' in data ? data.large : this.large); /** * An array of guild features. @@ -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; } @@ -373,8 +373,8 @@ class Guild { * content: 'discord.js', * before: '2016-11-17' * }).then(res => { - * const hit = res[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**`); + * const hit = res.messages[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); * }).catch(console.error); */ search(options) { 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 85a17c549..138169389 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -4,9 +4,7 @@ const MessageReaction = require('./MessageReaction'); const Collection = require('../util/Collection'); const Constants = require('../util/Constants'); const escapeMarkdown = require('../util/EscapeMarkdown'); - -// Done purely for GuildMember, which would cause a bad circular dependency -const Discord = require('..'); +let GuildMember; /** * Represents a message on Discord @@ -33,7 +31,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 +73,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 +92,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 +112,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 +154,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 +167,7 @@ class Message { /** * ID of the webhook that sent the message, if applicable - * @type {?string} + * @type {?Snowflake} */ this.webhookID = data.webhook_id || null; @@ -365,9 +363,11 @@ class Message { * @returns {boolean} */ isMemberMentioned(member) { + // Lazy-loading is used here to get around a circular dependency that breaks things + if (!GuildMember) GuildMember = require('./GuildMember'); if (this.mentions.everyone) return true; if (this.mentions.users.has(member.id)) return true; - if (member instanceof Discord.GuildMember && member.roles.some(r => this.mentions.roles.has(r.id))) return true; + if (member instanceof GuildMember && member.roles.some(r => this.mentions.roles.has(r.id))) return true; return false; } @@ -390,7 +390,7 @@ class Message { * .catch(console.error); */ edit(content, options) { - if (!options && typeof content === 'object') { + if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; content = ''; } else if (!options) { @@ -535,7 +535,7 @@ class Message { } _addReaction(emoji, user) { - const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; + const emojiID = emoji.identifier; let reaction; if (this.reactions.has(emojiID)) { reaction = this.reactions.get(emojiID); @@ -553,7 +553,7 @@ class Message { } _removeReaction(emoji, user) { - const emojiID = emoji.id || emoji; + const emojiID = emoji.identifier; if (this.reactions.has(emojiID)) { const reaction = this.reactions.get(emojiID); if (reaction.users.has(user.id)) { 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 b6d2cdbd6..b504a3635 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -19,15 +19,15 @@ class ReactionEmoji { /** * The ID of this reaction emoji. - * @type {string} + * @type {?Snowflake} */ this.id = id; } /** * The identifier of this emoji, used for message reactions - * @readonly * @type {string} + * @readonly */ get identifier() { if (this.id) return `${this.name}:${this.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 dd763d7f0..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; } @@ -224,8 +224,8 @@ class TextBasedChannel { * content: 'discord.js', * before: '2016-11-17' * }).then(res => { - * const hit = res[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**`); + * const hit = res.messages[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); * }).catch(console.error); */ search(options) { diff --git a/src/util/ConvertArrayBuffer.js b/src/util/ConvertArrayBuffer.js index 26b1cc8b7..c6bd36433 100644 --- a/src/util/ConvertArrayBuffer.js +++ b/src/util/ConvertArrayBuffer.js @@ -1,10 +1,3 @@ -function arrayBufferToBuffer(ab) { - const buffer = new Buffer(ab.byteLength); - const view = new Uint8Array(ab); - for (var i = 0; i < buffer.length; ++i) buffer[i] = view[i]; - return buffer; -} - function str2ab(str) { const buffer = new ArrayBuffer(str.length * 2); const view = new Uint16Array(buffer); @@ -14,5 +7,5 @@ function str2ab(str) { module.exports = function convertArrayBuffer(x) { if (typeof x === 'string') x = str2ab(x); - return arrayBufferToBuffer(x); + return Buffer.from(x); }; diff --git a/src/util/FetchRecommendedShards.js b/src/util/FetchRecommendedShards.js index a60f51006..217009077 100644 --- a/src/util/FetchRecommendedShards.js +++ b/src/util/FetchRecommendedShards.js @@ -3,17 +3,18 @@ const botGateway = require('./Constants').Endpoints.botGateway; /** * Gets the recommended shard count from Discord - * @param {number} token Discord auth token + * @param {string} token Discord auth token + * @param {number} [guildsPerShard=1000] Number of guilds per shard * @returns {Promise} the recommended number of shards */ -module.exports = function fetchRecommendedShards(token) { +module.exports = function fetchRecommendedShards(token, guildsPerShard = 1000) { return new Promise((resolve, reject) => { if (!token) throw new Error('A token must be provided.'); superagent.get(botGateway) .set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`) .end((err, res) => { if (err) reject(err); - resolve(res.body.shards); + resolve(res.body.shards * (1000 / guildsPerShard)); }); }); }; diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js new file mode 100644 index 000000000..60d0926ac --- /dev/null +++ b/src/util/Snowflake.js @@ -0,0 +1,65 @@ +const Long = require('long'); + +// Discord epoch (2015-01-01T00:00:00.000Z) +const EPOCH = 1420070400000; +let INCREMENT = 0; + +/** + * A container for useful snowflake-related methods + */ +class SnowflakeUtil { + /** + * 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 + * ``` + * @typedef {string} Snowflake + */ + + /** + * Generates a Discord snowflake + * This hardcodes the worker ID as 1 and the process ID as 0. + * @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(); + } + + /** + * 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 + */ + + /** + * Deconstructs 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, + }; + } +} + +function pad(v, n, c = '0') { + return String(v).length >= n ? String(v) : (String(c).repeat(n) + v).slice(-n); +} + +module.exports = SnowflakeUtil; diff --git a/src/util/TransformSearchOptions.js b/src/util/TransformSearchOptions.js index e61056aed..a71ceb0fd 100644 --- a/src/util/TransformSearchOptions.js +++ b/src/util/TransformSearchOptions.js @@ -21,7 +21,7 @@ const long = require('long'); * @property {string} [embedProvider] The name of an embed provider * @property {string} [embedType] one of `image`, `video`, `url`, `rich` * @property {string} [attachmentFilename] The name of an attachment - * @property {string} [attachmentExtention] The extension of an attachment + * @property {string} [attachmentExtension] The extension of an attachment * @property {Date} [before] Date to find messages before * @property {Date} [after] Date to find messages before * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) @@ -42,7 +42,7 @@ module.exports = function TransformSearchOptions(options, client) { if (!(options.during instanceof Date)) options.during = new Date(options.during); const t = options.during.getTime() - 14200704e5; options.minID = long.fromNumber(t).shiftLeft(22).toString(); - options.maxID = long.fromNumber(t + 86400000).shift(222).toString(); + options.maxID = long.fromNumber(t + 86400000).shiftLeft(22).toString(); } if (options.channel) options.channel = client.resolver.resolveChannelID(options.channel);