diff --git a/index.d.ts b/index.d.ts index 68cd1c6cb..2919117d3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -67,8 +67,9 @@ declare namespace Eris { type GuildTextChannelTypes = Constants["ChannelTypes"][keyof Pick]; type GuildThreadChannelTypes = Constants["ChannelTypes"][keyof Pick]; type GuildPublicThreadChannelTypes = Exclude; + type GuildVoiceChannelTypes = Constants["ChannelTypes"][keyof Pick]; type PrivateChannelTypes = Constants["ChannelTypes"][keyof Pick]; - type TextVoiceChannelTypes = Constants["ChannelTypes"][keyof Pick]; + type TextVoiceChannelTypes = Constants["ChannelTypes"][keyof Pick]; // Command type CommandGenerator = CommandGeneratorFunction | MessageContent | MessageContent[] | CommandGeneratorFunction[]; @@ -384,33 +385,48 @@ declare namespace Eris { /** @deprecated */ agent?: HTTPSAgent; allowedMentions?: AllowedMentions; + /** @deprecated */ autoreconnect?: boolean; + /** @deprecated */ compress?: boolean; + /** @deprecated */ connectionTimeout?: number; defaultImageFormat?: string; defaultImageSize?: number; - disableEvents?: { [s: string]: boolean }; + /** @deprecated */ + disableEvents?: Record; + /** @deprecated */ firstShardID?: number; + gateway?: GatewayOptions; + /** @deprecated */ getAllUsers?: boolean; + /** @deprecated */ guildCreateTimeout?: number; - intents: number | (IntentStrings | number)[]; + /** @deprecated */ + intents?: number | (IntentStrings | number)[]; + /** @deprecated */ largeThreshold?: number; + /** @deprecated */ lastShardID?: number; /** @deprecated */ latencyThreshold?: number; + /** @deprecated */ maxReconnectAttempts?: number; + /** @deprecated */ maxResumeAttempts?: number; + /** @deprecated */ maxShards?: number | "auto"; messageLimit?: number; opusOnly?: boolean; /** @deprecated */ ratelimiterOffset?: number; + /** @deprecated */ reconnectDelay?: ReconnectDelayFunction; requestTimeout?: number; rest?: RequestHandlerOptions; restMode?: boolean; + /** @deprecated */ seedVoiceConnections?: boolean; - shardConcurrency?: number | "auto"; ws?: unknown; } interface CommandClientOptions { @@ -823,6 +839,24 @@ declare namespace Eris { } // Gateway/REST + interface GatewayOptions { + autoreconnect?: boolean; + compress?: boolean; + connectionTimeout?: number; + disableEvents?: Record; + firstShardID?: number; + getAllUsers?: boolean; + guildCreateTimeout?: number; + intents?: number | (IntentStrings | number)[]; + largeThreshold?: number; + lastShardID?: number; + maxReconnectAttempts?: number; + maxResumeAttempts?: number; + maxConcurrency?: number | "auto"; + maxShards?: number | "auto"; + reconnectDelay?: ReconnectDelayFunction; + seedVoiceConnections?: boolean; + } interface HTTPResponse { code: number; message: string; @@ -856,9 +890,6 @@ declare namespace Eris { res: (value: Member[]) => void; timeout: NodeJS.Timeout; } - interface ShardManagerOptions { - concurrency?: number | "auto"; - } // Guild interface CreateGuildOptions { @@ -3538,7 +3569,9 @@ declare namespace Eris { buckets: Map; connectQueue: Shard[]; connectTimeout: NodeJS.Timer | null; - constructor(client: Client, options: ShardManagerOptions); + lastConnect: number; + options: GatewayOptions; + constructor(client: Client, options?: GatewayOptions); connect(shard: Shard): void; spawn(id: number): void; tryConnect(): void; @@ -3738,7 +3771,7 @@ declare namespace Eris { export class VoiceChannel extends GuildChannel implements Invitable { bitrate: number; rtcRegion: string | null; - type: TextVoiceChannelTypes; + type: GuildVoiceChannelTypes; userLimit: number; videoQualityMode: VideoQualityMode; voiceMembers: Collection; diff --git a/lib/Client.js b/lib/Client.js index 46c73e042..489ae12a0 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -39,15 +39,7 @@ try { Erlpack = require("erlpack"); } catch(err) { // eslint-disable no-empty } -let ZlibSync; -try { - ZlibSync = require("zlib-sync"); -} catch(err) { - try { - ZlibSync = require("pako"); - } catch(err) { // eslint-disable no-empty - } -} + const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); /** @@ -88,26 +80,43 @@ class Client extends EventEmitter { * @arg {Boolean | Array} [options.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. * @arg {Boolean | Array} [options.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. * @arg {Boolean} [options.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to - * @arg {Boolean} [options.autoreconnect=true] Have Eris autoreconnect when connection is lost - * @arg {Boolean} [options.compress=false] Whether to request WebSocket data to be compressed or not - * @arg {Number} [options.connectionTimeout=30000] How long in milliseconds to wait for the connection to handshake with the server + * @arg {Boolean} [options.autoreconnect=true] [DEPRECATED] Have Eris autoreconnect when connection is lost. This option has been moved under `options.gateway` + * @arg {Boolean} [options.compress=false] [DEPRECATED] Whether to request WebSocket data to be compressed or not. This option has been moved under `options.gateway` + * @arg {Number} [options.connectionTimeout=30000] [DEPRECATED] How long in milliseconds to wait for the connection to handshake with the server. This option has been moved under `options.gateway` * @arg {String} [options.defaultImageFormat="jpg"] The default format to provide user avatars, guild icons, and group icons in. Can be "jpg", "png", "gif", or "webp" * @arg {Number} [options.defaultImageSize=128] The default size to return user avatars, guild icons, banners, splashes, and group icons. Can be any power of two between 16 and 2048. If the height and width are different, the width will be the value specified, and the height relative to that - * @arg {Object} [options.disableEvents] If disableEvents[eventName] is true, the WS event will not be processed. This can cause significant performance increase on large bots. [A full list of the WS event names can be found on the docs reference page](/Eris/docs/reference#ws-event-names) - * @arg {Number} [options.firstShardID=0] The ID of the first shard to run for this client - * @arg {Boolean} [options.getAllUsers=false] Get all the users in every guild. Ready time will be severely delayed - * @arg {Number} [options.guildCreateTimeout=2000] How long in milliseconds to wait for a GUILD_CREATE before "ready" is fired. Increase this value if you notice missing guilds - * @arg {Number | Array} [options.intents] A list of [intent names](/Eris/docs/reference), pre-shifted intent numbers to add, or a raw bitmask value describing the intents to subscribe to. Some intents, like `guildPresences` and `guildMembers`, must be enabled on your application's page to be used. By default, all non-privileged intents are enabled. - * @arg {Number} [options.largeThreshold=250] The maximum number of offline users per guild during initial guild data transmission - * @arg {Number} [options.lastShardID=options.maxShards - 1] The ID of the last shard to run for this client + * @arg {Object} [options.disableEvents] [DEPRECATED] If disableEvents[eventName] is true, the WS event will not be processed. This can cause significant performance increase on large bots. [A full list of the WS event names can be found on the docs reference page](/Eris/docs/reference#ws-event-names). This option has been moved under `options.gateway` + * @arg {Number} [options.firstShardID=0] [DEPRECATED] The ID of the first shard to run for this client. This option has been moved under `options.gateway` + * @arg {Object} [options.gateway] Options for gateway connections + * @arg {Boolean} [options.gateway.autoreconnect=true] Have Eris autoreconnect when connection is lost + * @arg {Boolean} [options.gateway.compress=false] Whether to request WebSocket data to be compressed or not + * @arg {Number} [options.gateway.connectionTimeout=30000] How long in milliseconds to wait for the connection to handshake with the server + * @arg {Object} [options.gateway.disableEvents] If disableEvents[eventName] is true, the WS event will not be processed. This can cause significant performance increase on large bots. [A full list of the WS event names can be found on the docs reference page](/Eris/docs/reference#ws-event-names) + * @arg {Number} [options.gateway.firstShardID=0] The ID of the first shard to run for this client + * @arg {Boolean} [options.gateway.getAllUsers=false] Get all the users in every guild. Ready time will be severely delayed + * @arg {Number} [options.gateway.guildCreateTimeout=2000] How long in milliseconds to wait for a GUILD_CREATE before "ready" is fired. Increase this value if you notice missing guilds + * @arg {Number | Array} [options.gateway.intents] A list of [intent names](/Eris/docs/reference), pre-shifted intent numbers to add, or a raw bitmask value describing the intents to subscribe to. Some intents, like `guildPresences` and `guildMembers`, must be enabled on your application's page to be used. By default, all non-privileged intents are enabled. + * @arg {Number} [options.gateway.largeThreshold=250] The maximum number of offline users per guild during initial guild data transmission + * @arg {Number} [options.gateway.lastShardID=options.maxShards - 1] The ID of the last shard to run for this client + * @arg {Number} [options.gateway.maxReconnectAttempts=Infinity] The maximum amount of times that the client is allowed to try to reconnect to Discord. + * @arg {Number} [options.gateway.maxResumeAttempts=10] The maximum amount of times a shard can attempt to resume a session before considering that session invalid. + * @arg {Number | String} [options.gateway.maxConcurrency=1] The number of shards that can start simultaneously. If "auto" Eris will use Discord's recommended shard concurrency. + * @arg {Number | String} [options.gateway.maxShards=1] The total number of shards you want to run. If "auto" Eris will use Discord's recommended shard count. + * @arg {Function} [options.gateway.reconnectDelay] A function which returns how long the bot should wait until reconnecting to Discord. + * @arg {Boolean} [options.gateway.seedVoiceConnections=false] Whether to populate bot.voiceConnections with existing connections the bot account has during startup. Note that this will disconnect connections from other bot sessions + * @arg {Boolean} [options.getAllUsers=false] [DEPRECATED] Get all the users in every guild. Ready time will be severely delayed. This option has been moved under `options.gateway` + * @arg {Number} [options.guildCreateTimeout=2000] [DEPRECATED] How long in milliseconds to wait for a GUILD_CREATE before "ready" is fired. Increase this value if you notice missing guilds. This option has been moved under `options.gateway` + * @arg {Number | Array} [options.intents] [DEPRECATED] A list of [intent names](/Eris/docs/reference), pre-shifted intent numbers to add, or a raw bitmask value describing the intents to subscribe to. Some intents, like `guildPresences` and `guildMembers`, must be enabled on your application's page to be used. By default, all non-privileged intents are enabled. This option has been moved under `options.gateway` + * @arg {Number} [options.largeThreshold=250] [DEPRECATED] The maximum number of offline users per guild during initial guild data transmission. This option has been moved under `options.gateway` + * @arg {Number} [options.lastShardID=options.maxShards - 1] [DEPRECATED] The ID of the last shard to run for this client. This option has been moved under `options.gateway` * @arg {Number} [options.latencyThreshold=30000] [DEPRECATED] The average request latency at which Eris will start emitting latency errors. This option has been moved under `options.rest` - * @arg {Number} [options.maxReconnectAttempts=Infinity] The maximum amount of times that the client is allowed to try to reconnect to Discord. - * @arg {Number} [options.maxResumeAttempts=10] The maximum amount of times a shard can attempt to resume a session before considering that session invalid. - * @arg {Number | String} [options.maxShards=1] The total number of shards you want to run. If "auto" Eris will use Discord's recommended shard count. + * @arg {Number} [options.maxReconnectAttempts=Infinity] [DEPRECATED] The maximum amount of times that the client is allowed to try to reconnect to Discord. This option has been moved under `options.gateway` + * @arg {Number} [options.maxResumeAttempts=10] [DEPRECATED] The maximum amount of times a shard can attempt to resume a session before considering that session invalid. This option has been moved under `options.gateway` + * @arg {Number | String} [options.maxShards=1] The total number of shards you want to run. If "auto" Eris will use Discord's recommended shard count. This option has been moved under `options.gateway` * @arg {Number} [options.messageLimit=100] The maximum size of a channel message cache * @arg {Boolean} [options.opusOnly=false] Whether to suppress the Opus encoder not found error or not * @arg {Number} [options.ratelimiterOffset=0] [DEPRECATED] A number of milliseconds to offset the ratelimit timing calculations by. This option has been moved under `options.rest` - * @arg {Function} [options.reconnectDelay] A function which returns how long the bot should wait until reconnecting to Discord. + * @arg {Function} [options.reconnectDelay] [DEPRECATED] A function which returns how long the bot should wait until reconnecting to Discord. This option has been moved under `options.gateway` * @arg {Number} [options.requestTimeout=15000] A number of milliseconds before requests are considered timed out. This option will stop affecting REST in a future release; that behavior is [DEPRECATED] and replaced by `options.rest.requestTimeout` * @arg {Object} [options.rest] Options for the REST request handler * @arg {Object} [options.rest.agent] A HTTPS Agent used to proxy requests @@ -119,8 +128,7 @@ class Client extends EventEmitter { * @arg {Number} [options.rest.ratelimiterOffset=0] A number of milliseconds to offset the ratelimit timing calculations by * @arg {Number} [options.rest.requestTimeout=15000] A number of milliseconds before REST requests are considered timed out * @arg {Boolean} [options.restMode=false] Whether to enable getting objects over REST. Even with this option enabled, it is recommended that you check the cache first before using REST - * @arg {Boolean} [options.seedVoiceConnections=false] Whether to populate bot.voiceConnections with existing connections the bot account has during startup. Note that this will disconnect connections from other bot sessions - * @arg {Number | String} [options.shardConcurrency="auto"] The number of shards that can start simultaneously. If "auto" Eris will use Discord's recommended shard concurrency. + * @arg {Boolean} [options.seedVoiceConnections=false] [DEPRECATED] Whether to populate bot.voiceConnections with existing connections the bot account has during startup. Note that this will disconnect connections from other bot sessions. This option has been moved under `options.gateway` * @arg {Object} [options.ws] An object of WebSocket options to pass to the shard WebSocket constructors */ constructor(token, options) { @@ -131,37 +139,17 @@ class Client extends EventEmitter { users: true, roles: true }, - autoreconnect: true, - compress: false, - connectionTimeout: 30000, defaultImageFormat: "jpg", defaultImageSize: 128, - disableEvents: {}, - firstShardID: 0, - getAllUsers: false, - guildCreateTimeout: 2000, - intents: Constants.Intents.allNonPrivileged, - largeThreshold: 250, - maxReconnectAttempts: Infinity, - maxResumeAttempts: 10, - maxShards: 1, messageLimit: 100, opusOnly: false, requestTimeout: 15000, rest: {}, restMode: false, - seedVoiceConnections: false, - shardConcurrency: "auto", ws: {}, - reconnectDelay: (lastDelay, attempts) => Math.pow(attempts + 1, 0.7) * 20000 + gateway: {} }, options); this.options.allowedMentions = this._formatAllowedMentions(this.options.allowedMentions); - if(this.options.lastShardID === undefined && this.options.maxShards !== "auto") { - this.options.lastShardID = this.options.maxShards - 1; - } - if(typeof window !== "undefined" || !ZlibSync) { - this.options.compress = false; // zlib does not like Blobs, Pako is not here - } if(!Constants.ImageFormats.includes(this.options.defaultImageFormat.toLowerCase())) { throw new TypeError(`Invalid default image format: ${this.options.defaultImageFormat}`); } @@ -175,28 +163,6 @@ class Client extends EventEmitter { this.options.ws.agent = this.options.agent; } - if(this.options.hasOwnProperty("intents")) { - // Resolve intents option to the proper integer - if(Array.isArray(this.options.intents)) { - let bitmask = 0; - for(const intent of this.options.intents) { - if(typeof intent === "number") { - bitmask |= intent; - } else if(Constants.Intents[intent]) { - bitmask |= Constants.Intents[intent]; - } else { - this.emit("warn", `Unknown intent: ${intent}`); - } - } - this.options.intents = bitmask; - } - - // Ensure requesting all guild members isn't destined to fail - if(this.options.getAllUsers && !(this.options.intents & Constants.Intents.guildMembers)) { - throw new Error("Cannot request all members without guildMembers intent"); - } - } - Object.defineProperty(this, "_token", { configurable: true, enumerable: false, @@ -207,11 +173,8 @@ class Client extends EventEmitter { this.requestHandler = new RequestHandler(this, this.options.rest); delete this.options.rest; - const shardManagerOptions = {}; - if(typeof this.options.shardConcurrency === "number") { - shardManagerOptions.concurrency = this.options.shardConcurrency; - } - this.shards = new ShardManager(this, shardManagerOptions); + this.shards = new ShardManager(this, this.options.gateway); + delete this.options.gateway; this.ready = false; this.bot = this._token.startsWith("Bot "); @@ -431,8 +394,8 @@ class Client extends EventEmitter { throw new Error(`Invalid token "${this._token}"`); } try { - const data = await (this.options.maxShards === "auto" || (this.options.shardConcurrency === "auto" && this.bot) ? this.getBotGateway() : this.getGateway()); - if(!data.url || (this.options.maxShards === "auto" && !data.shards)) { + const data = await (this.shards.options.maxShards === "auto" || (this.shards.options.shardConcurrency === "auto" && this.bot) ? this.getBotGateway() : this.getGateway()); + if(!data.url || (this.shards.options.maxShards === "auto" && !data.shards)) { throw new Error("Invalid response from gateway REST call"); } if(data.url.includes("?")) { @@ -443,32 +406,32 @@ class Client extends EventEmitter { } this.gatewayURL = `${data.url}?v=${Constants.GATEWAY_VERSION}&encoding=${Erlpack ? "etf" : "json"}`; - if(this.options.compress) { + if(this.shards.options.compress) { this.gatewayURL += "&compress=zlib-stream"; } - if(this.options.maxShards === "auto") { + if(this.shards.options.maxShards === "auto") { if(!data.shards) { throw new Error("Failed to autoshard due to lack of data from Discord."); } - this.options.maxShards = data.shards; - if(this.options.lastShardID === undefined) { - this.options.lastShardID = data.shards - 1; + this.shards.options.maxShards = data.shards; + if(this.shards.options.lastShardID === undefined) { + this.shards.options.lastShardID = data.shards - 1; } } - if(this.options.shardConcurrency === "auto" && data.session_start_limit && typeof data.session_start_limit.max_concurrency === "number") { - this.shards.setConcurrency(data.session_start_limit.max_concurrency); + if(this.shards.options.shardConcurrency === "auto" && data.session_start_limit && typeof data.session_start_limit.max_concurrency === "number") { + this.shards.options.maxConcurrency = data.session_start_limit.max_concurrency; } - for(let i = this.options.firstShardID; i <= this.options.lastShardID; ++i) { + for(let i = this.shards.options.firstShardID; i <= this.shards.options.lastShardID; ++i) { this.shards.spawn(i); } } catch(err) { - if(!this.options.autoreconnect) { + if(!this.shards.options.autoreconnect) { throw err; } - const reconnectDelay = this.options.reconnectDelay(this.lastReconnectDelay, this.reconnectAttempts); + const reconnectDelay = this.shards.options.reconnectDelay(this.lastReconnectDelay, this.reconnectAttempts); await sleep(reconnectDelay); this.lastReconnectDelay = reconnectDelay; this.reconnectAttempts = this.reconnectAttempts + 1; diff --git a/lib/gateway/Shard.js b/lib/gateway/Shard.js index ce0866385..423c9fcee 100644 --- a/lib/gateway/Shard.js +++ b/lib/gateway/Shard.js @@ -118,9 +118,9 @@ class Shard extends EventEmitter { ++this.unsyncedGuilds; this.syncGuild(guild.id); } - if(this.client.options.getAllUsers && guild.members.size < guild.memberCount) { + if(this.client.shards.options.getAllUsers && guild.members.size < guild.memberCount) { this.getGuildMembers(guild.id, { - presences: this.client.options.intents && this.client.options.intents & Constants.Intents.guildPresences + presences: this.client.shards.options.intents && this.client.shards.options.intents & Constants.Intents.guildPresences }); } return guild; @@ -174,12 +174,12 @@ class Shard extends EventEmitter { */ super.emit("disconnect", error); - if(this.sessionID && this.connectAttempts >= this.client.options.maxResumeAttempts) { + if(this.sessionID && this.connectAttempts >= this.client.shards.options.maxResumeAttempts) { this.emit("debug", `Automatically invalidating session due to excessive resume attempts | Attempt ${this.connectAttempts}`, this.id); this.sessionID = null; } - if(options.reconnect === "auto" && this.client.options.autoreconnect) { + if(options.reconnect === "auto" && this.client.shards.options.autoreconnect) { /** * Fired when stuff happens and gives more info * @event Client#debug @@ -255,8 +255,8 @@ class Shard extends EventEmitter { } this.getAllUsersCount[guildID] = true; // Using intents, request one guild at a time - if(this.client.options.intents) { - if(!(this.client.options.intents & Constants.Intents.guildMembers)) { + if(this.client.shards.options.intents) { + if(!(this.client.shards.options.intents & Constants.Intents.guildMembers)) { throw new Error("Cannot request all members without guildMembers intent"); } this.requestGuildMembers([guildID], timeout); @@ -317,7 +317,7 @@ class Shard extends EventEmitter { } identify() { - if(this.client.options.compress && !ZlibSync) { + if(this.client.shards.options.compress && !ZlibSync) { /** * Fired when the shard encounters an error * @event Client#error @@ -331,17 +331,17 @@ class Shard extends EventEmitter { const identify = { token: this._token, v: GATEWAY_VERSION, - compress: !!this.client.options.compress, - large_threshold: this.client.options.largeThreshold, - intents: this.client.options.intents, + compress: !!this.client.shards.options.compress, + large_threshold: this.client.shards.options.largeThreshold, + intents: this.client.shards.options.intents, properties: { "os": process.platform, "browser": "Eris", "device": "Eris" } }; - if(this.client.options.maxShards > 1) { - identify.shard = [this.id, this.client.options.maxShards]; + if(this.client.shards.options.maxShards > 1) { + identify.shard = [this.id, this.client.shards.options.maxShards]; } if(this.presence.status) { identify.presence = this.presence; @@ -355,7 +355,7 @@ class Shard extends EventEmitter { } this.status = "connecting"; - if(this.client.options.compress) { + if(this.client.shards.options.compress) { this.emit("debug", "Initializing zlib-sync-based compression"); this._zlibSync = new ZlibSync.Inflate({ chunkSize: 128 * 1024 @@ -373,7 +373,7 @@ class Shard extends EventEmitter { reconnect: "auto" }, new Error("Connection timeout")); } - }, this.client.options.connectionTimeout); + }, this.client.shards.options.connectionTimeout); } onPacket(packet) { @@ -402,7 +402,7 @@ class Shard extends EventEmitter { switch(packet.op) { case GatewayOPCodes.DISPATCH: { - if(!this.client.options.disableEvents[packet.t]) { + if(!this.client.shards.options.disableEvents[packet.t]) { this.wsEvent(packet); } break; @@ -481,10 +481,10 @@ class Shard extends EventEmitter { if(!opts.user_ids && !opts.query) { opts.query = ""; } - if(!opts.query && !opts.user_ids && (this.client.options.intents && !(this.client.options.intents & Constants.Intents.guildMembers))) { + if(!opts.query && !opts.user_ids && (this.client.shards.options.intents && !(this.client.shards.options.intents & Constants.Intents.guildMembers))) { throw new Error("Cannot request all members without guildMembers intent"); } - if(opts.presences && (this.client.options.intents && !(this.client.options.intents & Constants.Intents.guildPresences))) { + if(opts.presences && (this.client.shards.options.intents && !(this.client.shards.options.intents & Constants.Intents.guildPresences))) { throw new Error("Cannot request members presences without guildPresences intent"); } if(opts.user_ids && opts.user_ids.length > 100) { @@ -498,7 +498,7 @@ class Shard extends EventEmitter { timeout: setTimeout(() => { res(this.requestMembersPromise[opts.nonce].members); delete this.requestMembersPromise[opts.nonce]; - }, (options && options.timeout) || this.client.options.requestTimeout) + }, (options && options.timeout) || this.client.shards.options.requestTimeout) }); } @@ -548,7 +548,7 @@ class Shard extends EventEmitter { } this.guildCreateTimeout = setTimeout(() => { this.checkReady(); - }, this.client.options.guildCreateTimeout); + }, this.client.shards.options.guildCreateTimeout); } } @@ -1123,7 +1123,7 @@ class Shard extends EventEmitter { } case "GUILD_MEMBER_UPDATE": { // Check for member update if guildPresences intent isn't set, to prevent emitting twice - if(!(this.client.options.intents & Constants.Intents.guildPresences) && packet.d.user.username !== undefined) { + if(!(this.client.shards.options.intents & Constants.Intents.guildPresences) && packet.d.user.username !== undefined) { let user = this.client.users.get(packet.d.user.id); let oldUser = null; if(user && (user.username !== packet.d.user.username || user.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar)) { @@ -1832,7 +1832,7 @@ class Shard extends EventEmitter { const channel = guild.channels.get(voiceState.channel_id); if(channel) { channel.voiceMembers.add(guild.members.update(voiceState)); - if(this.client.options.seedVoiceConnections && voiceState.id === this.client.user.id && !this.client.voiceConnections.get(channel.guild ? channel.guild.id : "call")) { + if(this.client.shards.options.seedVoiceConnections && voiceState.id === this.client.user.id && !this.client.voiceConnections.get(channel.guild ? channel.guild.id : "call")) { this.client.joinVoiceChannel(channel.id); } } else { // Phantom voice states from connected users in deleted channels (╯°□°)╯︵ ┻━┻ @@ -2550,13 +2550,13 @@ class Shard extends EventEmitter { _onWSMessage(data) { try { if(data instanceof ArrayBuffer) { - if(this.client.options.compress || Erlpack) { + if(this.client.shards.options.compress || Erlpack) { data = Buffer.from(data); } } else if(Array.isArray(data)) { // Fragmented messages data = Buffer.concat(data); // Copyfull concat is slow, but no alternative } - if(this.client.options.compress) { + if(this.client.shards.options.compress) { if(data.length >= 4 && data.readUInt32BE(data.length - 4) === 0xFFFF) { this._zlibSync.push(data, ZlibSync.Z_SYNC_FLUSH); if(this._zlibSync.err) { diff --git a/lib/gateway/ShardManager.js b/lib/gateway/ShardManager.js index 6b21cfa47..661c7fd50 100644 --- a/lib/gateway/ShardManager.js +++ b/lib/gateway/ShardManager.js @@ -3,16 +3,71 @@ const Base = require("../structures/Base"); const Collection = require("../util/Collection"); const Shard = require("./Shard"); - +const Constants = require("../Constants"); +let ZlibSync; +try { + ZlibSync = require("zlib-sync"); +} catch(err) { + try { + ZlibSync = require("pako"); + } catch(err) { // eslint-disable no-empty + } +} class ShardManager extends Collection { constructor(client, options = {}) { super(Shard); this._client = client; - this.options = Object.assign({ - concurrency: 1 + autoreconnect: typeof client.options.autoreconnect !== "undefined" ? client.options.autoreconnect : true, + compress: typeof client.options.compress !== "undefined" ? client.options.compress : false, + connectionTimeout: typeof client.options.connectionTimeout !== "undefined" ? client.options.connectionTimeout : 30000, + disableEvents: {}, + firstShardID: typeof client.options.firstShardID !== "undefined" ? client.options.firstShardID : 0, + getAllUsers: typeof client.options.getAllUsers !== "undefined" ? client.options.getAllUsers : false, + guildCreateTimeout: typeof client.options.guildCreateTimeout !== "undefined" ? client.options.guildCreateTimeout : 2000, + intents: typeof client.options.intents !== "undefined" ? client.options.intents : Constants.Intents.allNonPrivileged, + largeThreshold: typeof client.options.largeThreshold !== "undefined" ? client.options.largeThreshold : 250, + maxReconnectAttempts: typeof client.options.maxReconnectAttempts !== "undefined" ? client.options.maxReconnectAttempts : Infinity, + maxResumeAttempts: typeof client.options.maxResumeAttempts !== "undefined" ? client.options.maxResumeAttempts : 10, + maxConcurrency: typeof client.options.maxConcurrency !== "undefined" ? client.options.maxConcurrency : 1, + maxShards: typeof client.options.maxShards !== "undefined" ? client.options.maxShards : 1, + seedVoiceConnections: typeof client.options.seedVoiceConnections !== "undefined" ? client.options.seedVoiceConnections : false, + reconnectDelay: client.options.reconnectDelay || ((lastDelay, attempts) => Math.pow(attempts + 1, 0.7) * 20000) }, options); + if(typeof client.options.disableEvents !== "undefined") { + this.options.disableEvents = Object.assign(client.options.disableEvents, this.options.disableEvents); + } + if(typeof this.options.intents !== "undefined") { + // Resolve intents option to the proper integer + if(Array.isArray(this.options.intents)) { + let bitmask = 0; + for(const intent of this.options.intents) { + if(Constants.Intents[intent]) { + bitmask |= Constants.Intents[intent]; + } else if(typeof intent === "number") { + bitmask |= intent; + } else { + this.emit("warn", `Unknown intent: ${intent}`); + } + } + this.options.intents = bitmask; + } + + // Ensure requesting all guild members isn't destined to fail + if(this.options.getAllUsers && !(this.options.intents & Constants.Intents.guildMembers)) { + throw new Error("Cannot request all members without guildMembers intent"); + } + } + + if(this.options.lastShardID === undefined && this.options.maxShards !== "auto") { + this.options.lastShardID = this.options.maxShards - 1; + } + + if(typeof window !== "undefined" || !ZlibSync) { + this.options.compress = false; // zlib does not like Blobs, Pako is not here + } + this.buckets = new Map(); this.connectQueue = []; this.connectTimeout = null; @@ -23,10 +78,6 @@ class ShardManager extends Collection { this.tryConnect(); } - setConcurrency(concurrency) { - this.options.concurrency = concurrency; - } - spawn(id) { let shard = this.get(id); if(!shard) { @@ -107,7 +158,7 @@ class ShardManager extends Collection { // loop over the connectQueue for(const shard of this.connectQueue) { // find the bucket for our shard - const rateLimitKey = (shard.id % this.options.concurrency) || 0; + const rateLimitKey = (shard.id % this.options.maxConcurrency) || 0; const lastConnect = this.buckets.get(rateLimitKey) || 0; // has enough time passed since the last connect for this bucket (5s/bucket)? @@ -117,7 +168,7 @@ class ShardManager extends Collection { } // Are there any connecting shards in the same bucket we should wait on? - if(this.some((s) => s.connecting && ((s.id % this.options.concurrency) || 0) === rateLimitKey)) { + if(this.some((s) => s.connecting && ((s.id % this.options.maxConcurrency) || 0) === rateLimitKey)) { continue; } @@ -140,7 +191,7 @@ class ShardManager extends Collection { } _readyPacketCB(shardID) { - const rateLimitKey = (shardID % this.options.concurrency) || 0; + const rateLimitKey = (shardID % this.options.maxConcurrency) || 0; this.buckets.set(rateLimitKey, Date.now()); this.tryConnect(); diff --git a/lib/structures/Guild.js b/lib/structures/Guild.js index a471f28a5..588020344 100644 --- a/lib/structures/Guild.js +++ b/lib/structures/Guild.js @@ -82,7 +82,7 @@ class Guild extends Base { constructor(data, client) { super(data.id); this._client = client; - this.shard = client.shards.get(client.guildShardMap[this.id] || (Base.getDiscordEpoch(data.id) % client.options.maxShards) || 0); + this.shard = client.shards.get(client.guildShardMap[this.id] || (Base.getDiscordEpoch(data.id) % client.shards.options.maxShards) || 0); this.unavailable = !!data.unavailable; this.joinedAt = Date.parse(data.joined_at); this.voiceStates = new Collection(VoiceState); @@ -201,7 +201,7 @@ class Guild extends Base { client.emit("error", err, this.shard.id); continue; } - if(client.options.seedVoiceConnections && voiceState.id === client.user.id && !client.voiceConnections.get(this.id)) { + if(client.shards.options.seedVoiceConnections && voiceState.id === client.user.id && !client.voiceConnections.get(this.id)) { process.nextTick(() => this._client.joinVoiceChannel(voiceState.channel_id)); } } diff --git a/lib/voice/VoiceConnection.js b/lib/voice/VoiceConnection.js index 74355aae9..6bd187ff8 100644 --- a/lib/voice/VoiceConnection.js +++ b/lib/voice/VoiceConnection.js @@ -168,7 +168,7 @@ class VoiceConnection extends EventEmitter { this.disconnect(new Error("Voice connection timeout")); } this.connectionTimeout = null; - }, this.shard.client ? this.shard.client.options.connectionTimeout : 30000).unref(); + }, this.shard.client ? this.shard.client.shards.options.connectionTimeout : 30000).unref(); if(!data.endpoint) { return; // Endpoint null, wait next update. }