diff options
| author | 8cy <[email protected]> | 2020-04-30 15:46:16 -0700 |
|---|---|---|
| committer | 8cy <[email protected]> | 2020-04-30 15:46:16 -0700 |
| commit | 3a4deac89054021b56ad5bd8005b2044cc085c98 (patch) | |
| tree | 3dd6af8503e497e46180b6b5231674f36bdce9f2 /node_modules/discord.js/src/client | |
| download | uppity-3a4deac89054021b56ad5bd8005b2044cc085c98.tar.xz uppity-3a4deac89054021b56ad5bd8005b2044cc085c98.zip | |
Up, up, uppity.
Diffstat (limited to 'node_modules/discord.js/src/client')
91 files changed, 5476 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/client/BaseClient.js b/node_modules/discord.js/src/client/BaseClient.js new file mode 100644 index 0000000..a381e1a --- /dev/null +++ b/node_modules/discord.js/src/client/BaseClient.js @@ -0,0 +1,147 @@ +'use strict'; + +require('setimmediate'); +const EventEmitter = require('events'); +const RESTManager = require('../rest/RESTManager'); +const { DefaultOptions } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * The base class for all clients. + * @extends {EventEmitter} + */ +class BaseClient extends EventEmitter { + constructor(options = {}) { + super(); + + /** + * Timeouts set by {@link BaseClient#setTimeout} that are still active + * @type {Set<Timeout>} + * @private + */ + this._timeouts = new Set(); + + /** + * Intervals set by {@link BaseClient#setInterval} that are still active + * @type {Set<Timeout>} + * @private + */ + this._intervals = new Set(); + + /** + * Intervals set by {@link BaseClient#setImmediate} that are still active + * @type {Set<Immediate>} + * @private + */ + this._immediates = new Set(); + + /** + * The options the client was instantiated with + * @type {ClientOptions} + */ + this.options = Util.mergeDefault(DefaultOptions, options); + + /** + * The REST manager of the client + * @type {RESTManager} + * @private + */ + this.rest = new RESTManager(this, options._tokenType); + } + + /** + * API shortcut + * @type {Object} + * @readonly + * @private + */ + get api() { + return this.rest.api; + } + + /** + * Destroys all assets used by the base client. + */ + destroy() { + for (const t of this._timeouts) this.clearTimeout(t); + for (const i of this._intervals) this.clearInterval(i); + for (const i of this._immediates) this.clearImmediate(i); + this._timeouts.clear(); + this._intervals.clear(); + this._immediates.clear(); + } + + /** + * Sets a timeout that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {number} delay Time to wait before executing (in milliseconds) + * @param {...*} args Arguments for the function + * @returns {Timeout} + */ + setTimeout(fn, delay, ...args) { + const timeout = setTimeout(() => { + fn(...args); + this._timeouts.delete(timeout); + }, delay); + this._timeouts.add(timeout); + return timeout; + } + + /** + * Clears a timeout. + * @param {Timeout} timeout Timeout to cancel + */ + clearTimeout(timeout) { + clearTimeout(timeout); + this._timeouts.delete(timeout); + } + + /** + * Sets an interval that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {number} delay Time to wait between executions (in milliseconds) + * @param {...*} args Arguments for the function + * @returns {Timeout} + */ + setInterval(fn, delay, ...args) { + const interval = setInterval(fn, delay, ...args); + this._intervals.add(interval); + return interval; + } + + /** + * Clears an interval. + * @param {Timeout} interval Interval to cancel + */ + clearInterval(interval) { + clearInterval(interval); + this._intervals.delete(interval); + } + + /** + * Sets an immediate that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {...*} args Arguments for the function + * @returns {Immediate} + */ + setImmediate(fn, ...args) { + const immediate = setImmediate(fn, ...args); + this._immediates.add(immediate); + return immediate; + } + + /** + * Clears an immediate. + * @param {Immediate} immediate Immediate to cancel + */ + clearImmediate(immediate) { + clearImmediate(immediate); + this._immediates.delete(immediate); + } + + toJSON(...props) { + return Util.flatten(this, { domain: false }, ...props); + } +} + +module.exports = BaseClient; diff --git a/node_modules/discord.js/src/client/Client.js b/node_modules/discord.js/src/client/Client.js new file mode 100644 index 0000000..4a12e85 --- /dev/null +++ b/node_modules/discord.js/src/client/Client.js @@ -0,0 +1,455 @@ +'use strict'; + +const BaseClient = require('./BaseClient'); +const ActionsManager = require('./actions/ActionsManager'); +const ClientVoiceManager = require('./voice/ClientVoiceManager'); +const WebSocketManager = require('./websocket/WebSocketManager'); +const { Error, TypeError, RangeError } = require('../errors'); +const ChannelManager = require('../managers/ChannelManager'); +const GuildEmojiManager = require('../managers/GuildEmojiManager'); +const GuildManager = require('../managers/GuildManager'); +const UserManager = require('../managers/UserManager'); +const ShardClientUtil = require('../sharding/ShardClientUtil'); +const ClientApplication = require('../structures/ClientApplication'); +const GuildPreview = require('../structures/GuildPreview'); +const Invite = require('../structures/Invite'); +const VoiceRegion = require('../structures/VoiceRegion'); +const Webhook = require('../structures/Webhook'); +const Collection = require('../util/Collection'); +const { Events, browser, DefaultOptions } = require('../util/Constants'); +const DataResolver = require('../util/DataResolver'); +const Intents = require('../util/Intents'); +const Permissions = require('../util/Permissions'); +const Structures = require('../util/Structures'); + +/** + * The main hub for interacting with the Discord API, and the starting point for any bot. + * @extends {BaseClient} + */ +class Client extends BaseClient { + /** + * @param {ClientOptions} [options] Options for the client + */ + constructor(options = {}) { + super(Object.assign({ _tokenType: 'Bot' }, options)); + + // Obtain shard details from environment or if present, worker threads + let data = process.env; + try { + // Test if worker threads module is present and used + data = require('worker_threads').workerData || data; + } catch { + // Do nothing + } + + if (this.options.shards === DefaultOptions.shards) { + if ('SHARDS' in data) { + this.options.shards = JSON.parse(data.SHARDS); + } + } + + if (this.options.shardCount === DefaultOptions.shardCount) { + if ('SHARD_COUNT' in data) { + this.options.shardCount = Number(data.SHARD_COUNT); + } else if (Array.isArray(this.options.shards)) { + this.options.shardCount = this.options.shards.length; + } + } + + const typeofShards = typeof this.options.shards; + + if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { + this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); + } + + if (typeofShards === 'number') this.options.shards = [this.options.shards]; + + if (Array.isArray(this.options.shards)) { + this.options.shards = [ + ...new Set( + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)), + ), + ]; + } + + this._validateOptions(); + + /** + * The WebSocket manager of the client + * @type {WebSocketManager} + */ + this.ws = new WebSocketManager(this); + + /** + * The action manager of the client + * @type {ActionsManager} + * @private + */ + this.actions = new ActionsManager(this); + + /** + * The voice manager of the client (`null` in browsers) + * @type {?ClientVoiceManager} + */ + this.voice = !browser ? new ClientVoiceManager(this) : null; + + /** + * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) + * @type {?ShardClientUtil} + */ + this.shard = + !browser && process.env.SHARDING_MANAGER + ? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE) + : null; + + /** + * All of the {@link User} objects that have been cached at any point, mapped by their IDs + * @type {UserManager} + */ + this.users = new UserManager(this); + + /** + * All of the guilds the client is currently handling, mapped by their IDs - + * as long as sharding isn't being used, this will be *every* guild the bot is a member of + * @type {GuildManager} + */ + this.guilds = new GuildManager(this); + + /** + * All of the {@link Channel}s that the client is currently handling, mapped by their IDs - + * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot + * is a member of. Note that DM channels will not be initially cached, and thus not be present + * in the Manager without their explicit fetching or use. + * @type {ChannelManager} + */ + this.channels = new ChannelManager(this); + + const ClientPresence = Structures.get('ClientPresence'); + /** + * The presence of the Client + * @private + * @type {ClientPresence} + */ + this.presence = new ClientPresence(this); + + Object.defineProperty(this, 'token', { writable: true }); + if (!browser && !this.token && 'DISCORD_TOKEN' in process.env) { + /** + * Authorization token for the logged in bot + * <warn>This should be kept private at all times.</warn> + * @type {?string} + */ + this.token = process.env.DISCORD_TOKEN; + } else { + this.token = null; + } + + /** + * User that the client is logged in as + * @type {?ClientUser} + */ + this.user = null; + + /** + * Time at which the client was last regarded as being in the `READY` state + * (each time the client disconnects and successfully reconnects, this will be overwritten) + * @type {?Date} + */ + this.readyAt = null; + + if (this.options.messageSweepInterval > 0) { + this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); + } + } + + /** + * All custom emojis that the client has access to, mapped by their IDs + * @type {GuildEmojiManager} + * @readonly + */ + get emojis() { + const emojis = new GuildEmojiManager({ client: this }); + for (const guild of this.guilds.cache.values()) { + if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji); + } + return emojis; + } + + /** + * Timestamp of the time the client was last `READY` at + * @type {?number} + * @readonly + */ + get readyTimestamp() { + return this.readyAt ? this.readyAt.getTime() : null; + } + + /** + * How long it has been since the client last entered the `READY` state in milliseconds + * @type {?number} + * @readonly + */ + get uptime() { + return this.readyAt ? Date.now() - this.readyAt : null; + } + + /** + * Logs the client in, establishing a websocket connection to Discord. + * @param {string} token Token of the account to log in with + * @returns {Promise<string>} Token of the account used + * @example + * client.login('my token'); + */ + async login(token = this.token) { + if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); + this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); + this.emit( + Events.DEBUG, + `Provided token: ${token + .split('.') + .map((val, i) => (i > 1 ? val.replace(/./g, '*') : val)) + .join('.')}`, + ); + + if (this.options.presence) { + this.options.ws.presence = await this.presence._parse(this.options.presence); + } + + this.emit(Events.DEBUG, 'Preparing to connect to the gateway...'); + + try { + await this.ws.connect(); + return this.token; + } catch (error) { + this.destroy(); + throw error; + } + } + + /** + * Logs out, terminates the connection to Discord, and destroys the client. + * @returns {void} + */ + destroy() { + super.destroy(); + this.ws.destroy(); + this.token = null; + } + + /** + * Obtains an invite from Discord. + * @param {InviteResolvable} invite Invite code or URL + * @returns {Promise<Invite>} + * @example + * client.fetchInvite('https://discord.gg/bRCvFy9') + * .then(invite => console.log(`Obtained invite with code: ${invite.code}`)) + * .catch(console.error); + */ + fetchInvite(invite) { + const code = DataResolver.resolveInviteCode(invite); + return this.api + .invites(code) + .get({ query: { with_counts: true } }) + .then(data => new Invite(this, data)); + } + + /** + * Obtains a webhook from Discord. + * @param {Snowflake} id ID of the webhook + * @param {string} [token] Token for the webhook + * @returns {Promise<Webhook>} + * @example + * client.fetchWebhook('id', 'token') + * .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`)) + * .catch(console.error); + */ + fetchWebhook(id, token) { + return this.api + .webhooks(id, token) + .get() + .then(data => new Webhook(this, data)); + } + + /** + * Obtains the available voice regions from Discord. + * @returns {Promise<Collection<string, VoiceRegion>>} + * @example + * client.fetchVoiceRegions() + * .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`)) + * .catch(console.error); + */ + fetchVoiceRegions() { + return this.api.voice.regions.get().then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); + } + + /** + * Sweeps all text-based channels' messages and removes the ones older than the max message lifetime. + * If the message has been edited, the time of the edit is used rather than the time of the original message. + * @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds) + * will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime} + * @returns {number} Amount of messages that were removed from the caches, + * or -1 if the message cache lifetime is unlimited + * @example + * // Remove all messages older than 1800 seconds from the messages cache + * const amount = client.sweepMessages(1800); + * console.log(`Successfully removed ${amount} messages from the cache.`); + */ + sweepMessages(lifetime = this.options.messageCacheLifetime) { + if (typeof lifetime !== 'number' || isNaN(lifetime)) { + throw new TypeError('INVALID_TYPE', 'lifetime', 'number'); + } + if (lifetime <= 0) { + this.emit(Events.DEBUG, "Didn't sweep messages - lifetime is unlimited"); + return -1; + } + + const lifetimeMs = lifetime * 1000; + const now = Date.now(); + let channels = 0; + let messages = 0; + + for (const channel of this.channels.cache.values()) { + if (!channel.messages) continue; + channels++; + + messages += channel.messages.cache.sweep( + message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs, + ); + } + + this.emit( + Events.DEBUG, + `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`, + ); + return messages; + } + + /** + * Obtains the OAuth Application of this bot from Discord. + * @returns {Promise<ClientApplication>} + */ + fetchApplication() { + return this.api.oauth2 + .applications('@me') + .get() + .then(app => new ClientApplication(this, app)); + } + + /** + * Obtains a guild preview from Discord, only available for public guilds. + * @param {GuildResolvable} guild The guild to fetch the preview for + * @returns {Promise<GuildPreview>} + */ + fetchGuildPreview(guild) { + const id = this.guilds.resolveID(guild); + if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable'); + return this.api + .guilds(id) + .preview.get() + .then(data => new GuildPreview(this, data)); + } + + /** + * Generates a link that can be used to invite the bot to a guild. + * @param {PermissionResolvable} [permissions] Permissions to request + * @returns {Promise<string>} + * @example + * client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE']) + * .then(link => console.log(`Generated bot invite link: ${link}`)) + * .catch(console.error); + */ + async generateInvite(permissions) { + permissions = Permissions.resolve(permissions); + const application = await this.fetchApplication(); + const query = new URLSearchParams({ + client_id: application.id, + permissions: permissions, + scope: 'bot', + }); + return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`; + } + + toJSON() { + return super.toJSON({ + readyAt: false, + presences: false, + }); + } + + /** + * Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script + * with the client as `this`. + * @param {string} script Script to eval + * @returns {*} + * @private + */ + _eval(script) { + return eval(script); + } + + /** + * Validates the client options. + * @param {ClientOptions} [options=this.options] Options to validate + * @private + */ + _validateOptions(options = this.options) { + if (typeof options.ws.intents !== 'undefined') { + options.ws.intents = Intents.resolve(options.ws.intents); + } + if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1'); + } + if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shards', "'auto', a number or array of numbers"); + } + if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS'); + if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); + } + if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number'); + } + if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number'); + } + if (typeof options.fetchAllMembers !== 'boolean') { + throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); + } + if (typeof options.disableMentions !== 'string') { + throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a string'); + } + if (!Array.isArray(options.partials)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); + } + if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); + } + if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number'); + } + if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); + } + if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number'); + } + } +} + +module.exports = Client; + +/** + * Emitted for general warnings. + * @event Client#warn + * @param {string} info The warning + */ + +/** + * Emitted for general debugging information. + * @event Client#debug + * @param {string} info The debug information + */ diff --git a/node_modules/discord.js/src/client/WebhookClient.js b/node_modules/discord.js/src/client/WebhookClient.js new file mode 100644 index 0000000..cea6631 --- /dev/null +++ b/node_modules/discord.js/src/client/WebhookClient.js @@ -0,0 +1,31 @@ +'use strict'; + +const BaseClient = require('./BaseClient'); +const Webhook = require('../structures/Webhook'); + +/** + * The webhook client. + * @implements {Webhook} + * @extends {BaseClient} + */ +class WebhookClient extends BaseClient { + /** + * @param {Snowflake} id ID of the webhook + * @param {string} token Token of the webhook + * @param {ClientOptions} [options] Options for the client + * @example + * // Create a new webhook and send a message + * const hook = new Discord.WebhookClient('1234', 'abcdef'); + * hook.send('This will send a message').catch(console.error); + */ + constructor(id, token, options) { + super(options); + Object.defineProperty(this, 'client', { value: this }); + this.id = id; + Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true }); + } +} + +Webhook.applyToClass(WebhookClient); + +module.exports = WebhookClient; diff --git a/node_modules/discord.js/src/client/actions/Action.js b/node_modules/discord.js/src/client/actions/Action.js new file mode 100644 index 0000000..d9519b5 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/Action.js @@ -0,0 +1,103 @@ +'use strict'; + +const { PartialTypes } = require('../../util/Constants'); + +/* + +ABOUT ACTIONS + +Actions are similar to WebSocket Packet Handlers, but since introducing +the REST API methods, in order to prevent rewriting code to handle data, +"actions" have been introduced. They're basically what Packet Handlers +used to be but they're strictly for manipulating data and making sure +that WebSocket events don't clash with REST methods. + +*/ + +class GenericAction { + constructor(client) { + this.client = client; + } + + handle(data) { + return data; + } + + getPayload(data, manager, id, partialType, cache) { + const existing = manager.cache.get(id); + if (!existing && this.client.options.partials.includes(partialType)) { + return manager.add(data, cache); + } + return existing; + } + + getChannel(data) { + const id = data.channel_id || data.id; + return ( + data.channel || + this.getPayload( + { + id, + guild_id: data.guild_id, + recipients: [data.author || { id: data.user_id }], + }, + this.client.channels, + id, + PartialTypes.CHANNEL, + ) + ); + } + + getMessage(data, channel, cache) { + const id = data.message_id || data.id; + return ( + data.message || + this.getPayload( + { + id, + channel_id: channel.id, + guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), + }, + channel.messages, + id, + PartialTypes.MESSAGE, + cache, + ) + ); + } + + getReaction(data, message, user) { + const id = data.emoji.id || decodeURIComponent(data.emoji.name); + return this.getPayload( + { + emoji: data.emoji, + count: message.partial ? null : 0, + me: user ? user.id === this.client.user.id : false, + }, + message.reactions, + id, + PartialTypes.REACTION, + ); + } + + getMember(data, guild) { + const id = data.user.id; + return this.getPayload( + { + user: { + id, + }, + }, + guild.members, + id, + PartialTypes.GUILD_MEMBER, + ); + } + + getUser(data) { + const id = data.user_id; + return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER); + } +} + +module.exports = GenericAction; diff --git a/node_modules/discord.js/src/client/actions/ActionsManager.js b/node_modules/discord.js/src/client/actions/ActionsManager.js new file mode 100644 index 0000000..7e1df12 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ActionsManager.js @@ -0,0 +1,45 @@ +'use strict'; + +class ActionsManager { + constructor(client) { + this.client = client; + + this.register(require('./MessageCreate')); + this.register(require('./MessageDelete')); + this.register(require('./MessageDeleteBulk')); + this.register(require('./MessageUpdate')); + this.register(require('./MessageReactionAdd')); + this.register(require('./MessageReactionRemove')); + this.register(require('./MessageReactionRemoveAll')); + this.register(require('./MessageReactionRemoveEmoji')); + this.register(require('./ChannelCreate')); + this.register(require('./ChannelDelete')); + this.register(require('./ChannelUpdate')); + this.register(require('./GuildDelete')); + this.register(require('./GuildUpdate')); + this.register(require('./InviteCreate')); + this.register(require('./InviteDelete')); + this.register(require('./GuildMemberRemove')); + this.register(require('./GuildBanRemove')); + this.register(require('./GuildRoleCreate')); + this.register(require('./GuildRoleDelete')); + this.register(require('./GuildRoleUpdate')); + this.register(require('./PresenceUpdate')); + this.register(require('./UserUpdate')); + this.register(require('./VoiceStateUpdate')); + this.register(require('./GuildEmojiCreate')); + this.register(require('./GuildEmojiDelete')); + this.register(require('./GuildEmojiUpdate')); + this.register(require('./GuildEmojisUpdate')); + this.register(require('./GuildRolesPositionUpdate')); + this.register(require('./GuildChannelsPositionUpdate')); + this.register(require('./GuildIntegrationsUpdate')); + this.register(require('./WebhooksUpdate')); + } + + register(Action) { + this[Action.name.replace(/Action$/, '')] = new Action(this.client); + } +} + +module.exports = ActionsManager; diff --git a/node_modules/discord.js/src/client/actions/ChannelCreate.js b/node_modules/discord.js/src/client/actions/ChannelCreate.js new file mode 100644 index 0000000..fa60a0b --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelCreate.js @@ -0,0 +1,23 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class ChannelCreateAction extends Action { + handle(data) { + const client = this.client; + const existing = client.channels.cache.has(data.id); + const channel = client.channels.add(data); + if (!existing && channel) { + /** + * Emitted whenever a channel is created. + * @event Client#channelCreate + * @param {DMChannel|GuildChannel} channel The channel that was created + */ + client.emit(Events.CHANNEL_CREATE, channel); + } + return { channel }; + } +} + +module.exports = ChannelCreateAction; diff --git a/node_modules/discord.js/src/client/actions/ChannelDelete.js b/node_modules/discord.js/src/client/actions/ChannelDelete.js new file mode 100644 index 0000000..2956d72 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelDelete.js @@ -0,0 +1,37 @@ +'use strict'; + +const Action = require('./Action'); +const DMChannel = require('../../structures/DMChannel'); +const { Events } = require('../../util/Constants'); + +class ChannelDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + let channel = client.channels.cache.get(data.id); + + if (channel) { + client.channels.remove(channel.id); + channel.deleted = true; + if (channel.messages && !(channel instanceof DMChannel)) { + for (const message of channel.messages.cache.values()) { + message.deleted = true; + } + } + /** + * Emitted whenever a channel is deleted. + * @event Client#channelDelete + * @param {DMChannel|GuildChannel} channel The channel that was deleted + */ + client.emit(Events.CHANNEL_DELETE, channel); + } + + return { channel }; + } +} + +module.exports = ChannelDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/ChannelUpdate.js b/node_modules/discord.js/src/client/actions/ChannelUpdate.js new file mode 100644 index 0000000..06bb71b --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelUpdate.js @@ -0,0 +1,33 @@ +'use strict'; + +const Action = require('./Action'); +const Channel = require('../../structures/Channel'); +const { ChannelTypes } = require('../../util/Constants'); + +class ChannelUpdateAction extends Action { + handle(data) { + const client = this.client; + + let channel = client.channels.cache.get(data.id); + if (channel) { + const old = channel._update(data); + + if (ChannelTypes[channel.type.toUpperCase()] !== data.type) { + const newChannel = Channel.create(this.client, data, channel.guild); + for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message); + newChannel._typing = new Map(channel._typing); + channel = newChannel; + this.client.channels.cache.set(channel.id, channel); + } + + return { + old, + updated: channel, + }; + } + + return {}; + } +} + +module.exports = ChannelUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildBanRemove.js b/node_modules/discord.js/src/client/actions/GuildBanRemove.js new file mode 100644 index 0000000..fc28e11 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildBanRemove.js @@ -0,0 +1,21 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildBanRemove extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + const user = client.users.add(data.user); + /** + * Emitted whenever a member is unbanned from a guild. + * @event Client#guildBanRemove + * @param {Guild} guild The guild that the unban occurred in + * @param {User} user The user that was unbanned + */ + if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user); + } +} + +module.exports = GuildBanRemove; diff --git a/node_modules/discord.js/src/client/actions/GuildChannelsPositionUpdate.js b/node_modules/discord.js/src/client/actions/GuildChannelsPositionUpdate.js new file mode 100644 index 0000000..a393167 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildChannelsPositionUpdate.js @@ -0,0 +1,21 @@ +'use strict'; + +const Action = require('./Action'); + +class GuildChannelsPositionUpdate extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.cache.get(data.guild_id); + if (guild) { + for (const partialChannel of data.channels) { + const channel = guild.channels.cache.get(partialChannel.id); + if (channel) channel.rawPosition = partialChannel.position; + } + } + + return { guild }; + } +} + +module.exports = GuildChannelsPositionUpdate; diff --git a/node_modules/discord.js/src/client/actions/GuildDelete.js b/node_modules/discord.js/src/client/actions/GuildDelete.js new file mode 100644 index 0000000..8bb630f --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildDelete.js @@ -0,0 +1,67 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + + let guild = client.guilds.cache.get(data.id); + if (guild) { + for (const channel of guild.channels.cache.values()) { + if (channel.type === 'text') channel.stopTyping(true); + } + + if (data.unavailable) { + // Guild is unavailable + guild.available = false; + + /** + * Emitted whenever a guild becomes unavailable, likely due to a server outage. + * @event Client#guildUnavailable + * @param {Guild} guild The guild that has become unavailable + */ + client.emit(Events.GUILD_UNAVAILABLE, guild); + + // Stops the GuildDelete packet thinking a guild was actually deleted, + // handles emitting of event itself + return { + guild: null, + }; + } + + for (const channel of guild.channels.cache.values()) this.client.channels.remove(channel.id); + if (guild.voice && guild.voice.connection) guild.voice.connection.disconnect(); + + // Delete guild + client.guilds.cache.delete(guild.id); + guild.deleted = true; + + /** + * Emitted whenever a guild kicks the client or the guild is deleted/left. + * @event Client#guildDelete + * @param {Guild} guild The guild that was deleted + */ + client.emit(Events.GUILD_DELETE, guild); + + this.deleted.set(guild.id, guild); + this.scheduleForDeletion(guild.id); + } else { + guild = this.deleted.get(data.id) || null; + } + + return { guild }; + } + + scheduleForDeletion(id) { + this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout); + } +} + +module.exports = GuildDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js b/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js new file mode 100644 index 0000000..379c62e --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js @@ -0,0 +1,19 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildEmojiCreateAction extends Action { + handle(guild, createdEmoji) { + const emoji = guild.emojis.add(createdEmoji); + /** + * Emitted whenever a custom emoji is created in a guild. + * @event Client#emojiCreate + * @param {GuildEmoji} emoji The emoji that was created + */ + this.client.emit(Events.GUILD_EMOJI_CREATE, emoji); + return { emoji }; + } +} + +module.exports = GuildEmojiCreateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojiDelete.js b/node_modules/discord.js/src/client/actions/GuildEmojiDelete.js new file mode 100644 index 0000000..42af70c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiDelete.js @@ -0,0 +1,20 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildEmojiDeleteAction extends Action { + handle(emoji) { + emoji.guild.emojis.cache.delete(emoji.id); + emoji.deleted = true; + /** + * Emitted whenever a custom emoji is deleted in a guild. + * @event Client#emojiDelete + * @param {GuildEmoji} emoji The emoji that was deleted + */ + this.client.emit(Events.GUILD_EMOJI_DELETE, emoji); + return { emoji }; + } +} + +module.exports = GuildEmojiDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js b/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js new file mode 100644 index 0000000..9fa59c9 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js @@ -0,0 +1,20 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildEmojiUpdateAction extends Action { + handle(current, data) { + const old = current._update(data); + /** + * Emitted whenever a custom emoji is updated in a guild. + * @event Client#emojiUpdate + * @param {GuildEmoji} oldEmoji The old emoji + * @param {GuildEmoji} newEmoji The new emoji + */ + this.client.emit(Events.GUILD_EMOJI_UPDATE, old, current); + return { emoji: current }; + } +} + +module.exports = GuildEmojiUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojisUpdate.js b/node_modules/discord.js/src/client/actions/GuildEmojisUpdate.js new file mode 100644 index 0000000..7711933 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojisUpdate.js @@ -0,0 +1,34 @@ +'use strict'; + +const Action = require('./Action'); + +class GuildEmojisUpdateAction extends Action { + handle(data) { + const guild = this.client.guilds.cache.get(data.guild_id); + if (!guild || !guild.emojis) return; + + const deletions = new Map(guild.emojis.cache); + + for (const emoji of data.emojis) { + // Determine type of emoji event + const cachedEmoji = guild.emojis.cache.get(emoji.id); + if (cachedEmoji) { + deletions.delete(emoji.id); + if (!cachedEmoji.equals(emoji)) { + // Emoji updated + this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji); + } + } else { + // Emoji added + this.client.actions.GuildEmojiCreate.handle(guild, emoji); + } + } + + for (const emoji of deletions.values()) { + // Emoji deleted + this.client.actions.GuildEmojiDelete.handle(emoji); + } + } +} + +module.exports = GuildEmojisUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildIntegrationsUpdate.js b/node_modules/discord.js/src/client/actions/GuildIntegrationsUpdate.js new file mode 100644 index 0000000..bbce076 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildIntegrationsUpdate.js @@ -0,0 +1,19 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildIntegrationsUpdate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + /** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsUpdate; diff --git a/node_modules/discord.js/src/client/actions/GuildMemberRemove.js b/node_modules/discord.js/src/client/actions/GuildMemberRemove.js new file mode 100644 index 0000000..f0fe3b3 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildMemberRemove.js @@ -0,0 +1,30 @@ +'use strict'; + +const Action = require('./Action'); +const { Events, Status } = require('../../util/Constants'); + +class GuildMemberRemoveAction extends Action { + handle(data, shard) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + let member = null; + if (guild) { + member = this.getMember(data, guild); + guild.memberCount--; + if (member) { + member.deleted = true; + guild.members.cache.delete(member.id); + /** + * Emitted whenever a member leaves a guild, or is kicked. + * @event Client#guildMemberRemove + * @param {GuildMember} member The member that has left/been kicked from the guild + */ + if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); + } + guild.voiceStates.cache.delete(data.user.id); + } + return { guild, member }; + } +} + +module.exports = GuildMemberRemoveAction; diff --git a/node_modules/discord.js/src/client/actions/GuildRoleCreate.js b/node_modules/discord.js/src/client/actions/GuildRoleCreate.js new file mode 100644 index 0000000..d7d5214 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleCreate.js @@ -0,0 +1,25 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildRoleCreate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + let role; + if (guild) { + const already = guild.roles.cache.has(data.role.id); + role = guild.roles.add(data.role); + /** + * Emitted whenever a role is created. + * @event Client#roleCreate + * @param {Role} role The role that was created + */ + if (!already) client.emit(Events.GUILD_ROLE_CREATE, role); + } + return { role }; + } +} + +module.exports = GuildRoleCreate; diff --git a/node_modules/discord.js/src/client/actions/GuildRoleDelete.js b/node_modules/discord.js/src/client/actions/GuildRoleDelete.js new file mode 100644 index 0000000..51754b3 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleDelete.js @@ -0,0 +1,30 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildRoleDeleteAction extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + let role; + + if (guild) { + role = guild.roles.cache.get(data.role_id); + if (role) { + guild.roles.cache.delete(data.role_id); + role.deleted = true; + /** + * Emitted whenever a guild role is deleted. + * @event Client#roleDelete + * @param {Role} role The role that was deleted + */ + client.emit(Events.GUILD_ROLE_DELETE, role); + } + } + + return { role }; + } +} + +module.exports = GuildRoleDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/GuildRoleUpdate.js b/node_modules/discord.js/src/client/actions/GuildRoleUpdate.js new file mode 100644 index 0000000..faea120 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleUpdate.js @@ -0,0 +1,39 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildRoleUpdateAction extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + + if (guild) { + let old = null; + + const role = guild.roles.cache.get(data.role.id); + if (role) { + old = role._update(data.role); + /** + * Emitted whenever a guild role is updated. + * @event Client#roleUpdate + * @param {Role} oldRole The role before the update + * @param {Role} newRole The role after the update + */ + client.emit(Events.GUILD_ROLE_UPDATE, old, role); + } + + return { + old, + updated: role, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +module.exports = GuildRoleUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js b/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js new file mode 100644 index 0000000..d7abca9 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js @@ -0,0 +1,21 @@ +'use strict'; + +const Action = require('./Action'); + +class GuildRolesPositionUpdate extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.cache.get(data.guild_id); + if (guild) { + for (const partialRole of data.roles) { + const role = guild.roles.cache.get(partialRole.id); + if (role) role.rawPosition = partialRole.position; + } + } + + return { guild }; + } +} + +module.exports = GuildRolesPositionUpdate; diff --git a/node_modules/discord.js/src/client/actions/GuildUpdate.js b/node_modules/discord.js/src/client/actions/GuildUpdate.js new file mode 100644 index 0000000..78a8afb --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildUpdate.js @@ -0,0 +1,33 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildUpdateAction extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.cache.get(data.id); + if (guild) { + const old = guild._update(data); + /** + * Emitted whenever a guild is updated - e.g. name change. + * @event Client#guildUpdate + * @param {Guild} oldGuild The guild before the update + * @param {Guild} newGuild The guild after the update + */ + client.emit(Events.GUILD_UPDATE, old, guild); + return { + old, + updated: guild, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +module.exports = GuildUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/InviteCreate.js b/node_modules/discord.js/src/client/actions/InviteCreate.js new file mode 100644 index 0000000..6381331 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/InviteCreate.js @@ -0,0 +1,28 @@ +'use strict'; + +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteCreateAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.cache.get(data.channel_id); + const guild = client.guilds.cache.get(data.guild_id); + if (!channel && !guild) return false; + + const inviteData = Object.assign(data, { channel, guild }); + const invite = new Invite(client, inviteData); + /** + * Emitted when an invite is created. + * <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel.</info> + * @event Client#inviteCreate + * @param {Invite} invite The invite that was created + */ + client.emit(Events.INVITE_CREATE, invite); + return { invite }; + } +} + +module.exports = InviteCreateAction; diff --git a/node_modules/discord.js/src/client/actions/InviteDelete.js b/node_modules/discord.js/src/client/actions/InviteDelete.js new file mode 100644 index 0000000..92692c3 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/InviteDelete.js @@ -0,0 +1,29 @@ +'use strict'; + +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteDeleteAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.cache.get(data.channel_id); + const guild = client.guilds.cache.get(data.guild_id); + if (!channel && !guild) return false; + + const inviteData = Object.assign(data, { channel, guild }); + const invite = new Invite(client, inviteData); + + /** + * Emitted when an invite is deleted. + * <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel.</info> + * @event Client#inviteDelete + * @param {Invite} invite The invite that was deleted + */ + client.emit(Events.INVITE_DELETE, invite); + return { invite }; + } +} + +module.exports = InviteDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/MessageCreate.js b/node_modules/discord.js/src/client/actions/MessageCreate.js new file mode 100644 index 0000000..7138707 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageCreate.js @@ -0,0 +1,39 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class MessageCreateAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.cache.get(data.channel_id); + if (channel) { + const existing = channel.messages.cache.get(data.id); + if (existing) return { message: existing }; + const message = channel.messages.add(data); + const user = message.author; + let member = message.member; + channel.lastMessageID = data.id; + if (user) { + user.lastMessageID = data.id; + user.lastMessageChannelID = channel.id; + } + if (member) { + member.lastMessageID = data.id; + member.lastMessageChannelID = channel.id; + } + + /** + * Emitted whenever a message is created. + * @event Client#message + * @param {Message} message The created message + */ + client.emit(Events.MESSAGE_CREATE, message); + return { message }; + } + + return {}; + } +} + +module.exports = MessageCreateAction; diff --git a/node_modules/discord.js/src/client/actions/MessageDelete.js b/node_modules/discord.js/src/client/actions/MessageDelete.js new file mode 100644 index 0000000..0210625 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDelete.js @@ -0,0 +1,29 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class MessageDeleteAction extends Action { + handle(data) { + const client = this.client; + const channel = this.getChannel(data); + let message; + if (channel) { + message = this.getMessage(data, channel); + if (message) { + channel.messages.cache.delete(message.id); + message.deleted = true; + /** + * Emitted whenever a message is deleted. + * @event Client#messageDelete + * @param {Message} message The deleted message + */ + client.emit(Events.MESSAGE_DELETE, message); + } + } + + return { message }; + } +} + +module.exports = MessageDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js b/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js new file mode 100644 index 0000000..8686b1d --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js @@ -0,0 +1,43 @@ +'use strict'; + +const Action = require('./Action'); +const Collection = require('../../util/Collection'); +const { Events } = require('../../util/Constants'); + +class MessageDeleteBulkAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.cache.get(data.channel_id); + + if (channel) { + const ids = data.ids; + const messages = new Collection(); + for (const id of ids) { + const message = this.getMessage( + { + id, + guild_id: data.guild_id, + }, + channel, + false, + ); + if (message) { + message.deleted = true; + messages.set(message.id, message); + channel.messages.cache.delete(id); + } + } + + /** + * Emitted whenever messages are deleted in bulk. + * @event Client#messageDeleteBulk + * @param {Collection<Snowflake, Message>} messages The deleted messages, mapped by their ID + */ + if (messages.size > 0) client.emit(Events.MESSAGE_BULK_DELETE, messages); + return { messages }; + } + return {}; + } +} + +module.exports = MessageDeleteBulkAction; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionAdd.js b/node_modules/discord.js/src/client/actions/MessageReactionAdd.js new file mode 100644 index 0000000..22287db --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionAdd.js @@ -0,0 +1,50 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); +const { PartialTypes } = require('../../util/Constants'); + +/* +{ user_id: 'id', + message_id: 'id', + emoji: { name: '�', id: null }, + channel_id: 'id' } } +*/ + +class MessageReactionAdd extends Action { + handle(data) { + if (!data.emoji) return false; + + const user = this.getUser(data); + if (!user) return false; + + // Verify channel + const channel = this.getChannel(data); + if (!channel || channel.type === 'voice') return false; + + // Verify message + const message = this.getMessage(data, channel); + if (!message) return false; + + // Verify reaction + if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false; + const reaction = message.reactions.add({ + emoji: data.emoji, + count: message.partial ? null : 0, + me: user.id === this.client.user.id, + }); + if (!reaction) return false; + reaction._add(user); + /** + * Emitted whenever a reaction is added to a cached message. + * @event Client#messageReactionAdd + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user that applied the guild or reaction emoji + */ + this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); + + return { message, reaction, user }; + } +} + +module.exports = MessageReactionAdd; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionRemove.js b/node_modules/discord.js/src/client/actions/MessageReactionRemove.js new file mode 100644 index 0000000..a40aa75 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemove.js @@ -0,0 +1,44 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +/* +{ user_id: 'id', + message_id: 'id', + emoji: { name: '�', id: null }, + channel_id: 'id' } } +*/ + +class MessageReactionRemove extends Action { + handle(data) { + if (!data.emoji) return false; + + const user = this.getUser(data); + if (!user) return false; + + // Verify channel + const channel = this.getChannel(data); + if (!channel || channel.type === 'voice') return false; + + // Verify message + const message = this.getMessage(data, channel); + if (!message) return false; + + // Verify reaction + const reaction = this.getReaction(data, message, user); + if (!reaction) return false; + reaction._remove(user); + /** + * Emitted whenever a reaction is removed from a cached message. + * @event Client#messageReactionRemove + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user whose emoji or reaction emoji was removed + */ + this.client.emit(Events.MESSAGE_REACTION_REMOVE, reaction, user); + + return { message, reaction, user }; + } +} + +module.exports = MessageReactionRemove; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionRemoveAll.js b/node_modules/discord.js/src/client/actions/MessageReactionRemoveAll.js new file mode 100644 index 0000000..14b79bf --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemoveAll.js @@ -0,0 +1,29 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class MessageReactionRemoveAll extends Action { + handle(data) { + // Verify channel + const channel = this.getChannel(data); + if (!channel || channel.type === 'voice') return false; + + // Verify message + const message = this.getMessage(data, channel); + if (!message) return false; + + message.reactions.cache.clear(); + this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message); + + return { message }; + } +} + +/** + * Emitted whenever all reactions are removed from a cached message. + * @event Client#messageReactionRemoveAll + * @param {Message} message The message the reactions were removed from + */ + +module.exports = MessageReactionRemoveAll; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionRemoveEmoji.js b/node_modules/discord.js/src/client/actions/MessageReactionRemoveEmoji.js new file mode 100644 index 0000000..143702c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemoveEmoji.js @@ -0,0 +1,28 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class MessageReactionRemoveEmoji extends Action { + handle(data) { + const channel = this.getChannel(data); + if (!channel || channel.type === 'voice') return false; + + const message = this.getMessage(data, channel); + if (!message) return false; + + const reaction = this.getReaction(data, message); + if (!reaction) return false; + if (!message.partial) message.reactions.cache.delete(reaction.emoji.id || reaction.emoji.name); + + /** + * Emitted when a bot removes an emoji reaction from a cached message. + * @event Client#messageReactionRemoveEmoji + * @param {MessageReaction} reaction The reaction that was removed + */ + this.client.emit(Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction); + return { reaction }; + } +} + +module.exports = MessageReactionRemoveEmoji; diff --git a/node_modules/discord.js/src/client/actions/MessageUpdate.js b/node_modules/discord.js/src/client/actions/MessageUpdate.js new file mode 100644 index 0000000..07e2aac --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageUpdate.js @@ -0,0 +1,24 @@ +'use strict'; + +const Action = require('./Action'); + +class MessageUpdateAction extends Action { + handle(data) { + const channel = this.getChannel(data); + if (channel) { + const { id, channel_id, guild_id, author, timestamp, type } = data; + const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel); + if (message) { + message.patch(data); + return { + old: message._edits[0], + updated: message, + }; + } + } + + return {}; + } +} + +module.exports = MessageUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/PresenceUpdate.js b/node_modules/discord.js/src/client/actions/PresenceUpdate.js new file mode 100644 index 0000000..f74fbeb --- /dev/null +++ b/node_modules/discord.js/src/client/actions/PresenceUpdate.js @@ -0,0 +1,44 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class PresenceUpdateAction extends Action { + handle(data) { + let user = this.client.users.cache.get(data.user.id); + if (!user && data.user.username) user = this.client.users.add(data.user); + if (!user) return; + + if (data.user && data.user.username) { + if (!user.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); + } + + const guild = this.client.guilds.cache.get(data.guild_id); + if (!guild) return; + + let oldPresence = guild.presences.cache.get(user.id); + if (oldPresence) oldPresence = oldPresence._clone(); + let member = guild.members.cache.get(user.id); + if (!member && data.status !== 'offline') { + member = guild.members.add({ + user, + roles: data.roles, + deaf: false, + mute: false, + }); + this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member); + } + guild.presences.add(Object.assign(data, { guild })); + if (member && this.client.listenerCount(Events.PRESENCE_UPDATE)) { + /** + * Emitted whenever a guild member's presence (e.g. status, activity) is changed. + * @event Client#presenceUpdate + * @param {?Presence} oldPresence The presence before the update, if one at all + * @param {Presence} newPresence The presence after the update + */ + this.client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); + } + } +} + +module.exports = PresenceUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/UserUpdate.js b/node_modules/discord.js/src/client/actions/UserUpdate.js new file mode 100644 index 0000000..7279ca7 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserUpdate.js @@ -0,0 +1,34 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class UserUpdateAction extends Action { + handle(data) { + const client = this.client; + + const newUser = client.users.cache.get(data.id); + const oldUser = newUser._update(data); + + if (!oldUser.equals(newUser)) { + /** + * Emitted whenever a user's details (e.g. username) are changed. + * @event Client#userUpdate + * @param {User} oldUser The user before the update + * @param {User} newUser The user after the update + */ + client.emit(Events.USER_UPDATE, oldUser, newUser); + return { + old: oldUser, + updated: newUser, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +module.exports = UserUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/VoiceStateUpdate.js b/node_modules/discord.js/src/client/actions/VoiceStateUpdate.js new file mode 100644 index 0000000..6f2ee9d --- /dev/null +++ b/node_modules/discord.js/src/client/actions/VoiceStateUpdate.js @@ -0,0 +1,44 @@ +'use strict'; + +const Action = require('./Action'); +const VoiceState = require('../../structures/VoiceState'); +const { Events } = require('../../util/Constants'); + +class VoiceStateUpdate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.cache.get(data.guild_id); + if (guild) { + // Update the state + const oldState = guild.voiceStates.cache.has(data.user_id) + ? guild.voiceStates.cache.get(data.user_id)._clone() + : new VoiceState(guild, { user_id: data.user_id }); + + const newState = guild.voiceStates.add(data); + + // Get the member + let member = guild.members.cache.get(data.user_id); + if (member && data.member) { + member._patch(data.member); + } else if (data.member && data.member.user && data.member.joined_at) { + member = guild.members.add(data.member); + } + + // Emit event + if (member && member.user.id === client.user.id) { + client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); + client.voice.onVoiceStateUpdate(data); + } + + /** + * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * @event Client#voiceStateUpdate + * @param {VoiceState} oldState The voice state before the update + * @param {VoiceState} newState The voice state after the update + */ + client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); + } + } +} + +module.exports = VoiceStateUpdate; diff --git a/node_modules/discord.js/src/client/actions/WebhooksUpdate.js b/node_modules/discord.js/src/client/actions/WebhooksUpdate.js new file mode 100644 index 0000000..fdf9c56 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/WebhooksUpdate.js @@ -0,0 +1,19 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class WebhooksUpdate extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.cache.get(data.channel_id); + /** + * Emitted whenever a guild text channel has its webhooks changed. + * @event Client#webhookUpdate + * @param {TextChannel} channel The channel that had a webhook update + */ + if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); + } +} + +module.exports = WebhooksUpdate; diff --git a/node_modules/discord.js/src/client/voice/ClientVoiceManager.js b/node_modules/discord.js/src/client/voice/ClientVoiceManager.js new file mode 100644 index 0000000..1eab808 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/ClientVoiceManager.js @@ -0,0 +1,110 @@ +'use strict'; + +const VoiceBroadcast = require('./VoiceBroadcast'); +const VoiceConnection = require('./VoiceConnection'); +const { Error } = require('../../errors'); +const Collection = require('../../util/Collection'); + +/** + * Manages voice connections for the client + */ +class ClientVoiceManager { + constructor(client) { + /** + * The client that instantiated this voice manager + * @type {Client} + * @readonly + * @name ClientVoiceManager#client + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * A collection mapping connection IDs to the Connection objects + * @type {Collection<Snowflake, VoiceConnection>} + */ + this.connections = new Collection(); + + /** + * Active voice broadcasts that have been created + * @type {VoiceBroadcast[]} + */ + this.broadcasts = []; + } + + /** + * Creates a voice broadcast. + * @returns {VoiceBroadcast} + */ + createBroadcast() { + const broadcast = new VoiceBroadcast(this.client); + this.broadcasts.push(broadcast); + return broadcast; + } + + onVoiceServer({ guild_id, token, endpoint }) { + this.client.emit('debug', `[VOICE] voiceServer guild: ${guild_id} token: ${token} endpoint: ${endpoint}`); + const connection = this.connections.get(guild_id); + if (connection) connection.setTokenAndEndpoint(token, endpoint); + } + + onVoiceStateUpdate({ guild_id, session_id, channel_id }) { + const connection = this.connections.get(guild_id); + this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`); + if (!connection) return; + if (!channel_id) { + connection._disconnect(); + this.connections.delete(guild_id); + return; + } + connection.channel = this.client.channels.cache.get(channel_id); + connection.setSessionID(session_id); + } + + /** + * Sets up a request to join a voice channel. + * @param {VoiceChannel} channel The voice channel to join + * @returns {Promise<VoiceConnection>} + * @private + */ + joinChannel(channel) { + return new Promise((resolve, reject) => { + if (!channel.joinable) { + throw new Error('VOICE_JOIN_CHANNEL', channel.full); + } + + let connection = this.connections.get(channel.guild.id); + + if (connection) { + if (connection.channel.id !== channel.id) { + this.connections.get(channel.guild.id).updateChannel(channel); + } + resolve(connection); + return; + } else { + connection = new VoiceConnection(this, channel); + connection.on('debug', msg => + this.client.emit('debug', `[VOICE (${channel.guild.id}:${connection.status})]: ${msg}`), + ); + connection.authenticate(); + this.connections.set(channel.guild.id, connection); + } + + connection.once('failed', reason => { + this.connections.delete(channel.guild.id); + reject(reason); + }); + + connection.on('error', reject); + + connection.once('authenticated', () => { + connection.once('ready', () => { + resolve(connection); + connection.removeListener('error', reject); + }); + connection.once('disconnect', () => this.connections.delete(channel.guild.id)); + }); + }); + } +} + +module.exports = ClientVoiceManager; diff --git a/node_modules/discord.js/src/client/voice/VoiceBroadcast.js b/node_modules/discord.js/src/client/voice/VoiceBroadcast.js new file mode 100644 index 0000000..5755b52 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceBroadcast.js @@ -0,0 +1,111 @@ +'use strict'; + +const EventEmitter = require('events'); +const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer'); +const PlayInterface = require('./util/PlayInterface'); +const { Events } = require('../../util/Constants'); + +/** + * A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency. + * + * Example usage: + * ```js + * const broadcast = client.voice.createBroadcast(); + * broadcast.play('./music.mp3'); + * // Play "music.mp3" in all voice connections that the client is in + * for (const connection of client.voice.connections.values()) { + * connection.play(broadcast); + * } + * ``` + * @implements {PlayInterface} + * @extends {EventEmitter} + */ +class VoiceBroadcast extends EventEmitter { + constructor(client) { + super(); + /** + * The client that created the broadcast + * @type {Client} + */ + this.client = client; + /** + * The subscribed StreamDispatchers of this broadcast + * @type {StreamDispatcher[]} + */ + this.subscribers = []; + this.player = new BroadcastAudioPlayer(this); + } + + /** + * The current master dispatcher, if any. This dispatcher controls all that is played by subscribed dispatchers. + * @type {?BroadcastDispatcher} + * @readonly + */ + get dispatcher() { + return this.player.dispatcher; + } + + /** + * Play an audio resource. + * @param {ReadableStream|string} resource The resource to play. + * @param {StreamOptions} [options] The options to play. + * @example + * // Play a local audio file + * broadcast.play('/home/hydrabolt/audio.mp3', { volume: 0.5 }); + * @example + * // Play a ReadableStream + * broadcast.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' })); + * @example + * // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html + * broadcast.play('http://www.sample-videos.com/audio/mp3/wave.mp3'); + * @returns {BroadcastDispatcher} + */ + play() { + return null; + } + + /** + * Ends the broadcast, unsubscribing all subscribed channels and deleting the broadcast + */ + end() { + for (const dispatcher of this.subscribers) this.delete(dispatcher); + const index = this.client.voice.broadcasts.indexOf(this); + if (index !== -1) this.client.voice.broadcasts.splice(index, 1); + } + + add(dispatcher) { + const index = this.subscribers.indexOf(dispatcher); + if (index === -1) { + this.subscribers.push(dispatcher); + /** + * Emitted whenever a stream dispatcher subscribes to the broadcast. + * @event VoiceBroadcast#subscribe + * @param {StreamDispatcher} subscriber The subscribed dispatcher + */ + this.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); + return true; + } else { + return false; + } + } + + delete(dispatcher) { + const index = this.subscribers.indexOf(dispatcher); + if (index !== -1) { + this.subscribers.splice(index, 1); + dispatcher.destroy(); + /** + * Emitted whenever a stream dispatcher unsubscribes to the broadcast. + * @event VoiceBroadcast#unsubscribe + * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher + */ + this.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); + return true; + } + return false; + } +} + +PlayInterface.applyToClass(VoiceBroadcast); + +module.exports = VoiceBroadcast; diff --git a/node_modules/discord.js/src/client/voice/VoiceConnection.js b/node_modules/discord.js/src/client/voice/VoiceConnection.js new file mode 100644 index 0000000..5f0e995 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceConnection.js @@ -0,0 +1,523 @@ +'use strict'; + +const EventEmitter = require('events'); +const VoiceUDP = require('./networking/VoiceUDPClient'); +const VoiceWebSocket = require('./networking/VoiceWebSocket'); +const AudioPlayer = require('./player/AudioPlayer'); +const VoiceReceiver = require('./receiver/Receiver'); +const PlayInterface = require('./util/PlayInterface'); +const Silence = require('./util/Silence'); +const { Error } = require('../../errors'); +const { OPCodes, VoiceOPCodes, VoiceStatus, Events } = require('../../util/Constants'); +const Speaking = require('../../util/Speaking'); +const Util = require('../../util/Util'); + +// Workaround for Discord now requiring silence to be sent before being able to receive audio +class SingleSilence extends Silence { + _read() { + super._read(); + this.push(null); + } +} + +const SUPPORTED_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305']; + +/** + * Represents a connection to a guild's voice server. + * ```js + * // Obtained using: + * voiceChannel.join() + * .then(connection => { + * + * }); + * ``` + * @extends {EventEmitter} + * @implements {PlayInterface} + */ +class VoiceConnection extends EventEmitter { + constructor(voiceManager, channel) { + super(); + + /** + * The voice manager that instantiated this connection + * @type {ClientVoiceManager} + */ + this.voiceManager = voiceManager; + + /** + * The voice channel this connection is currently serving + * @type {VoiceChannel} + */ + this.channel = channel; + + /** + * The current status of the voice connection + * @type {VoiceStatus} + */ + this.status = VoiceStatus.AUTHENTICATING; + + /** + * Our current speaking state + * @type {Readonly<Speaking>} + */ + this.speaking = new Speaking().freeze(); + + /** + * The authentication data needed to connect to the voice server + * @type {Object} + * @private + */ + this.authentication = {}; + + /** + * The audio player for this voice connection + * @type {AudioPlayer} + */ + this.player = new AudioPlayer(this); + + this.player.on('debug', m => { + /** + * Debug info from the connection. + * @event VoiceConnection#debug + * @param {string} message The debug message + */ + this.emit('debug', `audio player - ${m}`); + }); + + this.player.on('error', e => { + /** + * Warning info from the connection. + * @event VoiceConnection#warn + * @param {string|Error} warning The warning + */ + this.emit('warn', e); + }); + + this.once('closing', () => this.player.destroy()); + + /** + * Map SSRC values to user IDs + * @type {Map<number, Snowflake>} + * @private + */ + this.ssrcMap = new Map(); + + /** + * Tracks which users are talking + * @type {Map<Snowflake, Readonly<Speaking>>} + * @private + */ + this._speaking = new Map(); + + /** + * Object that wraps contains the `ws` and `udp` sockets of this voice connection + * @type {Object} + * @private + */ + this.sockets = {}; + + /** + * The voice receiver of this connection + * @type {VoiceReceiver} + */ + this.receiver = new VoiceReceiver(this); + } + + /** + * The client that instantiated this connection + * @type {Client} + * @readonly + */ + get client() { + return this.voiceManager.client; + } + + /** + * The current stream dispatcher (if any) + * @type {?StreamDispatcher} + * @readonly + */ + get dispatcher() { + return this.player.dispatcher; + } + + /** + * Sets whether the voice connection should display as "speaking", "soundshare" or "none". + * @param {BitFieldResolvable} value The new speaking state + * @private + */ + setSpeaking(value) { + if (this.speaking.equals(value)) return; + if (this.status !== VoiceStatus.CONNECTED) return; + this.speaking = new Speaking(value).freeze(); + this.sockets.ws + .sendPacket({ + op: VoiceOPCodes.SPEAKING, + d: { + speaking: this.speaking.bitfield, + delay: 0, + ssrc: this.authentication.ssrc, + }, + }) + .catch(e => { + this.emit('debug', e); + }); + } + + /** + * The voice state of this connection + * @type {VoiceState} + */ + get voice() { + return this.channel.guild.voice; + } + + /** + * Sends a request to the main gateway to join a voice channel. + * @param {Object} [options] The options to provide + * @returns {Promise<Shard>} + * @private + */ + sendVoiceStateUpdate(options = {}) { + options = Util.mergeDefault( + { + guild_id: this.channel.guild.id, + channel_id: this.channel.id, + self_mute: this.voice ? this.voice.selfMute : false, + self_deaf: this.voice ? this.voice.selfDeaf : false, + }, + options, + ); + + this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`); + + return this.channel.guild.shard.send( + { + op: OPCodes.VOICE_STATE_UPDATE, + d: options, + }, + true, + ); + } + + /** + * Set the token and endpoint required to connect to the voice servers. + * @param {string} token The voice token + * @param {string} endpoint The voice endpoint + * @private + * @returns {void} + */ + setTokenAndEndpoint(token, endpoint) { + this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); + if (!endpoint) { + // Signifies awaiting endpoint stage + return; + } + + if (!token) { + this.authenticateFailed('VOICE_TOKEN_ABSENT'); + return; + } + + endpoint = endpoint.match(/([^:]*)/)[0]; + this.emit('debug', `Endpoint resolved as ${endpoint}`); + + if (!endpoint) { + this.authenticateFailed('VOICE_INVALID_ENDPOINT'); + return; + } + + if (this.status === VoiceStatus.AUTHENTICATING) { + this.authentication.token = token; + this.authentication.endpoint = endpoint; + this.checkAuthenticated(); + } else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) { + this.reconnect(token, endpoint); + } + } + + /** + * Sets the Session ID for the connection. + * @param {string} sessionID The voice session ID + * @private + */ + setSessionID(sessionID) { + this.emit('debug', `Setting sessionID ${sessionID} (stored as "${this.authentication.sessionID}")`); + if (!sessionID) { + this.authenticateFailed('VOICE_SESSION_ABSENT'); + return; + } + + if (this.status === VoiceStatus.AUTHENTICATING) { + this.authentication.sessionID = sessionID; + this.checkAuthenticated(); + } else if (sessionID !== this.authentication.sessionID) { + this.authentication.sessionID = sessionID; + /** + * Emitted when a new session ID is received. + * @event VoiceConnection#newSession + * @private + */ + this.emit('newSession', sessionID); + } + } + + /** + * Checks whether the voice connection is authenticated. + * @private + */ + checkAuthenticated() { + const { token, endpoint, sessionID } = this.authentication; + this.emit('debug', `Authenticated with sessionID ${sessionID}`); + if (token && endpoint && sessionID) { + this.status = VoiceStatus.CONNECTING; + /** + * Emitted when we successfully initiate a voice connection. + * @event VoiceConnection#authenticated + */ + this.emit('authenticated'); + this.connect(); + } + } + + /** + * Invoked when we fail to initiate a voice connection. + * @param {string} reason The reason for failure + * @private + */ + authenticateFailed(reason) { + this.client.clearTimeout(this.connectTimeout); + this.emit('debug', `Authenticate failed - ${reason}`); + if (this.status === VoiceStatus.AUTHENTICATING) { + /** + * Emitted when we fail to initiate a voice connection. + * @event VoiceConnection#failed + * @param {Error} error The encountered error + */ + this.emit('failed', new Error(reason)); + } else { + /** + * Emitted whenever the connection encounters an error. + * @event VoiceConnection#error + * @param {Error} error The encountered error + */ + this.emit('error', new Error(reason)); + } + this.status = VoiceStatus.DISCONNECTED; + } + + /** + * Move to a different voice channel in the same guild. + * @param {VoiceChannel} channel The channel to move to + * @private + */ + updateChannel(channel) { + this.channel = channel; + this.sendVoiceStateUpdate(); + } + + /** + * Attempts to authenticate to the voice server. + * @private + */ + authenticate() { + this.sendVoiceStateUpdate(); + this.connectTimeout = this.client.setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15000); + } + + /** + * Attempts to reconnect to the voice server (typically after a region change). + * @param {string} token The voice token + * @param {string} endpoint The voice endpoint + * @private + */ + reconnect(token, endpoint) { + this.authentication.token = token; + this.authentication.endpoint = endpoint; + this.speaking = new Speaking().freeze(); + this.status = VoiceStatus.RECONNECTING; + this.emit('debug', `Reconnecting to ${endpoint}`); + /** + * Emitted when the voice connection is reconnecting (typically after a region change). + * @event VoiceConnection#reconnecting + */ + this.emit('reconnecting'); + this.connect(); + } + + /** + * Disconnects the voice connection, causing a disconnect and closing event to be emitted. + */ + disconnect() { + this.emit('closing'); + this.emit('debug', 'disconnect() triggered'); + this.client.clearTimeout(this.connectTimeout); + const conn = this.voiceManager.connections.get(this.channel.guild.id); + if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id); + this.sendVoiceStateUpdate({ + channel_id: null, + }); + this._disconnect(); + } + + /** + * Internally disconnects (doesn't send disconnect packet). + * @private + */ + _disconnect() { + this.cleanup(); + this.status = VoiceStatus.DISCONNECTED; + /** + * Emitted when the voice connection disconnects. + * @event VoiceConnection#disconnect + */ + this.emit('disconnect'); + } + + /** + * Cleans up after disconnect. + * @private + */ + cleanup() { + this.player.destroy(); + this.speaking = new Speaking().freeze(); + const { ws, udp } = this.sockets; + + this.emit('debug', 'Connection clean up'); + + if (ws) { + ws.removeAllListeners('error'); + ws.removeAllListeners('ready'); + ws.removeAllListeners('sessionDescription'); + ws.removeAllListeners('speaking'); + ws.shutdown(); + } + + if (udp) udp.removeAllListeners('error'); + + this.sockets.ws = null; + this.sockets.udp = null; + } + + /** + * Connect the voice connection. + * @private + */ + connect() { + this.emit('debug', `Connect triggered`); + if (this.status !== VoiceStatus.RECONNECTING) { + if (this.sockets.ws) throw new Error('WS_CONNECTION_EXISTS'); + if (this.sockets.udp) throw new Error('UDP_CONNECTION_EXISTS'); + } + + if (this.sockets.ws) this.sockets.ws.shutdown(); + if (this.sockets.udp) this.sockets.udp.shutdown(); + + this.sockets.ws = new VoiceWebSocket(this); + this.sockets.udp = new VoiceUDP(this); + + const { ws, udp } = this.sockets; + + ws.on('debug', msg => this.emit('debug', msg)); + udp.on('debug', msg => this.emit('debug', msg)); + ws.on('error', err => this.emit('error', err)); + udp.on('error', err => this.emit('error', err)); + ws.on('ready', this.onReady.bind(this)); + ws.on('sessionDescription', this.onSessionDescription.bind(this)); + ws.on('startSpeaking', this.onStartSpeaking.bind(this)); + + this.sockets.ws.connect(); + } + + /** + * Invoked when the voice websocket is ready. + * @param {Object} data The received data + * @private + */ + onReady(data) { + Object.assign(this.authentication, data); + for (let mode of data.modes) { + if (SUPPORTED_MODES.includes(mode)) { + this.authentication.mode = mode; + this.emit('debug', `Selecting the ${mode} mode`); + break; + } + } + this.sockets.udp.createUDPSocket(data.ip); + } + + /** + * Invoked when a session description is received. + * @param {Object} data The received data + * @private + */ + onSessionDescription(data) { + Object.assign(this.authentication, data); + this.status = VoiceStatus.CONNECTED; + const ready = () => { + this.client.clearTimeout(this.connectTimeout); + this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); + /** + * Emitted once the connection is ready, when a promise to join a voice channel resolves, + * the connection will already be ready. + * @event VoiceConnection#ready + */ + this.emit('ready'); + }; + if (this.dispatcher) { + ready(); + } else { + // This serves to provide support for voice receive, sending audio is required to receive it. + const dispatcher = this.play(new SingleSilence(), { type: 'opus', volume: false }); + dispatcher.once('finish', ready); + } + } + + onStartSpeaking({ user_id, ssrc, speaking }) { + this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking }); + } + + /** + * Invoked when a speaking event is received. + * @param {Object} data The received data + * @private + */ + onSpeaking({ user_id, speaking }) { + speaking = new Speaking(speaking).freeze(); + const guild = this.channel.guild; + const user = this.client.users.cache.get(user_id); + const old = this._speaking.get(user_id); + this._speaking.set(user_id, speaking); + /** + * Emitted whenever a user changes speaking state. + * @event VoiceConnection#speaking + * @param {User} user The user that has changed speaking state + * @param {Readonly<Speaking>} speaking The speaking state of the user + */ + if (this.status === VoiceStatus.CONNECTED) { + this.emit('speaking', user, speaking); + if (!speaking.has(Speaking.FLAGS.SPEAKING)) { + this.receiver.packets._stoppedSpeaking(user_id); + } + } + + if (guild && user && !speaking.equals(old)) { + const member = guild.member(user); + if (member) { + /** + * Emitted once a guild member changes speaking state. + * @event Client#guildMemberSpeaking + * @param {GuildMember} member The member that started/stopped speaking + * @param {Readonly<Speaking>} speaking The speaking state of the member + */ + this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); + } + } + } + + play() {} // eslint-disable-line no-empty-function +} + +PlayInterface.applyToClass(VoiceConnection); + +module.exports = VoiceConnection; diff --git a/node_modules/discord.js/src/client/voice/dispatcher/BroadcastDispatcher.js b/node_modules/discord.js/src/client/voice/dispatcher/BroadcastDispatcher.js new file mode 100644 index 0000000..ae8d412 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -0,0 +1,46 @@ +'use strict'; + +const StreamDispatcher = require('./StreamDispatcher'); + +/** + * The class that sends voice packet data to the voice connection. + * @implements {VolumeInterface} + * @extends {StreamDispatcher} + */ +class BroadcastDispatcher extends StreamDispatcher { + constructor(player, options, streams) { + super(player, options, streams); + this.broadcast = player.broadcast; + } + + _write(chunk, enc, done) { + if (!this.startTime) this.startTime = Date.now(); + for (const dispatcher of this.broadcast.subscribers) { + dispatcher._write(chunk, enc); + } + this._step(done); + } + + _destroy(err, cb) { + if (this.player.dispatcher === this) this.player.dispatcher = null; + const { streams } = this; + if (streams.opus) streams.opus.unpipe(this); + if (streams.ffmpeg) streams.ffmpeg.destroy(); + super._destroy(err, cb); + } + + /** + * Set the bitrate of the current Opus encoder if using a compatible Opus stream. + * @param {number} value New bitrate, in kbps + * If set to 'auto', 48kbps will be used + * @returns {boolean} true if the bitrate has been successfully changed. + */ + setBitrate(value) { + if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false; + const bitrate = value === 'auto' ? 48 : value; + this.streams.opus.setBitrate(bitrate * 1000); + return true; + } +} + +module.exports = BroadcastDispatcher; diff --git a/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js b/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js new file mode 100644 index 0000000..62c46d3 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js @@ -0,0 +1,354 @@ +'use strict'; + +const { Writable } = require('stream'); +const secretbox = require('../util/Secretbox'); +const Silence = require('../util/Silence'); +const VolumeInterface = require('../util/VolumeInterface'); + +const FRAME_LENGTH = 20; +const CHANNELS = 2; +const TIMESTAMP_INC = (48000 / 100) * CHANNELS; + +const MAX_NONCE_SIZE = 2 ** 32 - 1; +const nonce = Buffer.alloc(24); + +/** + * @external WritableStream + * @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable} + */ + +/** + * The class that sends voice packet data to the voice connection. + * ```js + * // Obtained using: + * voiceChannel.join().then(connection => { + * // You can play a file or a stream here: + * const dispatcher = connection.play('/home/hydrabolt/audio.mp3'); + * }); + * ``` + * @implements {VolumeInterface} + * @extends {WritableStream} + */ +class StreamDispatcher extends Writable { + constructor(player, { seek = 0, volume = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, streams) { + const streamOptions = { seek, volume, fec, plp, bitrate, highWaterMark }; + super(streamOptions); + /** + * The Audio Player that controls this dispatcher + * @type {AudioPlayer} + */ + this.player = player; + this.streamOptions = streamOptions; + this.streams = streams; + this.streams.silence = new Silence(); + + this._nonce = 0; + this._nonceBuffer = Buffer.alloc(24); + + /** + * The time that the stream was paused at (null if not paused) + * @type {?number} + */ + this.pausedSince = null; + this._writeCallback = null; + + /** + * The broadcast controlling this dispatcher, if any + * @type {?VoiceBroadcast} + */ + this.broadcast = this.streams.broadcast; + + this._pausedTime = 0; + this._silentPausedTime = 0; + this.count = 0; + + this.on('finish', () => { + this._cleanup(); + this._setSpeaking(0); + }); + + this.setVolume(volume); + this.setBitrate(bitrate); + if (typeof fec !== 'undefined') this.setFEC(fec); + if (typeof plp !== 'undefined') this.setPLP(plp); + + const streamError = (type, err) => { + /** + * Emitted when the dispatcher encounters an error. + * @event StreamDispatcher#error + */ + if (type && err) { + err.message = `${type} stream: ${err.message}`; + this.emit(this.player.dispatcher === this ? 'error' : 'debug', err); + } + this.destroy(); + }; + + this.on('error', () => streamError()); + if (this.streams.input) this.streams.input.on('error', err => streamError('input', err)); + if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', err => streamError('ffmpeg', err)); + if (this.streams.opus) this.streams.opus.on('error', err => streamError('opus', err)); + if (this.streams.volume) this.streams.volume.on('error', err => streamError('volume', err)); + } + + get _sdata() { + return this.player.streamingData; + } + + _write(chunk, enc, done) { + if (!this.startTime) { + /** + * Emitted once the stream has started to play. + * @event StreamDispatcher#start + */ + this.emit('start'); + this.startTime = Date.now(); + } + this._playChunk(chunk); + this._step(done); + } + + _destroy(err, cb) { + this._cleanup(); + super._destroy(err, cb); + } + + _cleanup() { + if (this.player.dispatcher === this) this.player.dispatcher = null; + const { streams } = this; + if (streams.broadcast) streams.broadcast.delete(this); + if (streams.opus) streams.opus.destroy(); + if (streams.ffmpeg) streams.ffmpeg.destroy(); + } + + /** + * Pauses playback + * @param {boolean} [silence=false] Whether to play silence while paused to prevent audio glitches + */ + pause(silence = false) { + if (this.paused) return; + if (this.streams.opus) this.streams.opus.unpipe(this); + if (silence) { + this.streams.silence.pipe(this); + this._silence = true; + } else { + this._setSpeaking(0); + } + this.pausedSince = Date.now(); + } + + /** + * Whether or not playback is paused + * @type {boolean} + * @readonly + */ + get paused() { + return Boolean(this.pausedSince); + } + + /** + * Total time that this dispatcher has been paused in milliseconds + * @type {number} + * @readonly + */ + get pausedTime() { + return this._silentPausedTime + this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0); + } + + /** + * Resumes playback + */ + resume() { + if (!this.pausedSince) return; + this.streams.silence.unpipe(this); + if (this.streams.opus) this.streams.opus.pipe(this); + if (this._silence) { + this._silentPausedTime += Date.now() - this.pausedSince; + this._silence = false; + } else { + this._pausedTime += Date.now() - this.pausedSince; + } + this.pausedSince = null; + if (typeof this._writeCallback === 'function') this._writeCallback(); + } + + /** + * The time (in milliseconds) that the dispatcher has actually been playing audio for + * @type {number} + * @readonly + */ + get streamTime() { + return this.count * FRAME_LENGTH; + } + + /** + * The time (in milliseconds) that the dispatcher has been playing audio for, taking into account skips and pauses + * @type {number} + * @readonly + */ + get totalStreamTime() { + return Date.now() - this.startTime; + } + + /** + * Set the bitrate of the current Opus encoder if using a compatible Opus stream. + * @param {number} value New bitrate, in kbps + * If set to 'auto', the voice channel's bitrate will be used + * @returns {boolean} true if the bitrate has been successfully changed. + */ + setBitrate(value) { + if (!value || !this.bitrateEditable) return false; + const bitrate = value === 'auto' ? this.player.voiceConnection.channel.bitrate : value; + this.streams.opus.setBitrate(bitrate * 1000); + return true; + } + + /** + * Sets the expected packet loss percentage if using a compatible Opus stream. + * @param {number} value between 0 and 1 + * @returns {boolean} Returns true if it was successfully set. + */ + setPLP(value) { + if (!this.bitrateEditable) return false; + this.streams.opus.setPLP(value); + return true; + } + + /** + * Enables or disables forward error correction if using a compatible Opus stream. + * @param {boolean} enabled true to enable + * @returns {boolean} Returns true if it was successfully set. + */ + setFEC(enabled) { + if (!this.bitrateEditable) return false; + this.streams.opus.setFEC(enabled); + return true; + } + + _step(done) { + this._writeCallback = () => { + this._writeCallback = null; + done(); + }; + if (!this.streams.broadcast) { + const next = FRAME_LENGTH + this.count * FRAME_LENGTH - (Date.now() - this.startTime - this._pausedTime); + setTimeout(() => { + if ((!this.pausedSince || this._silence) && this._writeCallback) this._writeCallback(); + }, next); + } + this._sdata.sequence++; + this._sdata.timestamp += TIMESTAMP_INC; + if (this._sdata.sequence >= 2 ** 16) this._sdata.sequence = 0; + if (this._sdata.timestamp >= 2 ** 32) this._sdata.timestamp = 0; + this.count++; + } + + _final(callback) { + this._writeCallback = null; + callback(); + } + + _playChunk(chunk) { + if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secret_key) return; + this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk)); + } + + _encrypt(buffer) { + const { secret_key, mode } = this.player.voiceConnection.authentication; + if (mode === 'xsalsa20_poly1305_lite') { + this._nonce++; + if (this._nonce > MAX_NONCE_SIZE) this._nonce = 0; + this._nonceBuffer.writeUInt32BE(this._nonce, 0); + return [secretbox.methods.close(buffer, this._nonceBuffer, secret_key), this._nonceBuffer.slice(0, 4)]; + } else if (mode === 'xsalsa20_poly1305_suffix') { + const random = secretbox.methods.random(24); + return [secretbox.methods.close(buffer, random, secret_key), random]; + } else { + return [secretbox.methods.close(buffer, nonce, secret_key)]; + } + } + + _createPacket(sequence, timestamp, buffer) { + const packetBuffer = Buffer.alloc(12); + packetBuffer[0] = 0x80; + packetBuffer[1] = 0x78; + + packetBuffer.writeUIntBE(sequence, 2, 2); + packetBuffer.writeUIntBE(timestamp, 4, 4); + packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4); + + packetBuffer.copy(nonce, 0, 0, 12); + return Buffer.concat([packetBuffer, ...this._encrypt(buffer)]); + } + + _sendPacket(packet) { + /** + * Emitted whenever the dispatcher has debug information. + * @event StreamDispatcher#debug + * @param {string} info The debug info + */ + this._setSpeaking(1); + if (!this.player.voiceConnection.sockets.udp) { + this.emit('debug', 'Failed to send a packet - no UDP socket'); + return; + } + this.player.voiceConnection.sockets.udp.send(packet).catch(e => { + this._setSpeaking(0); + this.emit('debug', `Failed to send a packet - ${e}`); + }); + } + + _setSpeaking(value) { + if (typeof this.player.voiceConnection !== 'undefined') { + this.player.voiceConnection.setSpeaking(value); + } + /** + * Emitted when the dispatcher starts/stops speaking. + * @event StreamDispatcher#speaking + * @param {boolean} value Whether or not the dispatcher is speaking + */ + this.emit('speaking', value); + } + + get volumeEditable() { + return Boolean(this.streams.volume); + } + + /** + * Whether or not the Opus bitrate of this stream is editable + * @type {boolean} + * @readonly + */ + get bitrateEditable() { + return this.streams.opus && this.streams.opus.setBitrate; + } + + // Volume + get volume() { + return this.streams.volume ? this.streams.volume.volume : 1; + } + + setVolume(value) { + if (!this.streams.volume) return false; + /** + * Emitted when the volume of this dispatcher changes. + * @event StreamDispatcher#volumeChange + * @param {number} oldVolume The old volume of this dispatcher + * @param {number} newVolume The new volume of this dispatcher + */ + this.emit('volumeChange', this.volume, value); + this.streams.volume.setVolume(value); + return true; + } + + // Volume stubs for docs + /* eslint-disable no-empty-function*/ + get volumeDecibels() {} + get volumeLogarithmic() {} + setVolumeDecibels() {} + setVolumeLogarithmic() {} +} + +VolumeInterface.applyToClass(StreamDispatcher); + +module.exports = StreamDispatcher; diff --git a/node_modules/discord.js/src/client/voice/networking/VoiceUDPClient.js b/node_modules/discord.js/src/client/voice/networking/VoiceUDPClient.js new file mode 100644 index 0000000..b86428a --- /dev/null +++ b/node_modules/discord.js/src/client/voice/networking/VoiceUDPClient.js @@ -0,0 +1,154 @@ +'use strict'; + +const udp = require('dgram'); +const EventEmitter = require('events'); +const { Error } = require('../../../errors'); +const { VoiceOPCodes } = require('../../../util/Constants'); + +/** + * Represents a UDP client for a Voice Connection. + * @extends {EventEmitter} + * @private + */ +class VoiceConnectionUDPClient extends EventEmitter { + constructor(voiceConnection) { + super(); + + /** + * The voice connection that this UDP client serves + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + + /** + * The UDP socket + * @type {?Socket} + */ + this.socket = null; + + /** + * The address of the Discord voice server + * @type {?string} + */ + this.discordAddress = null; + + /** + * The local IP address + * @type {?string} + */ + this.localAddress = null; + + /** + * The local port + * @type {?string} + */ + this.localPort = null; + + this.voiceConnection.on('closing', this.shutdown.bind(this)); + } + + shutdown() { + this.emit('debug', `[UDP] shutdown requested`); + if (this.socket) { + this.socket.removeAllListeners('message'); + try { + this.socket.close(); + } finally { + this.socket = null; + } + } + } + + /** + * The port of the Discord voice server + * @type {number} + * @readonly + */ + get discordPort() { + return this.voiceConnection.authentication.port; + } + + /** + * Send a packet to the UDP client. + * @param {Object} packet The packet to send + * @returns {Promise<Object>} + */ + send(packet) { + return new Promise((resolve, reject) => { + if (!this.socket) throw new Error('UDP_SEND_FAIL'); + if (!this.discordAddress || !this.discordPort) throw new Error('UDP_ADDRESS_MALFORMED'); + this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { + if (error) { + this.emit('debug', `[UDP] >> ERROR: ${error}`); + reject(error); + } else { + resolve(packet); + } + }); + }); + } + + async createUDPSocket(address) { + this.discordAddress = address; + const socket = (this.socket = udp.createSocket('udp4')); + socket.on('error', e => { + this.emit('debug', `[UDP] Error: ${e}`); + this.emit('error', e); + }); + socket.on('close', () => { + this.emit('debug', '[UDP] socket closed'); + }); + this.emit('debug', `[UDP] created socket`); + socket.once('message', message => { + this.emit('debug', `[UDP] message: [${[...message]}] (${message})`); + // Stop if the sockets have been deleted because the connection has been closed already + if (!this.voiceConnection.sockets.ws) return; + + const packet = parseLocalPacket(message); + if (packet.error) { + this.emit('debug', `[UDP] ERROR: ${packet.error}`); + this.emit('error', packet.error); + return; + } + + this.localAddress = packet.address; + this.localPort = packet.port; + + this.voiceConnection.sockets.ws.sendPacket({ + op: VoiceOPCodes.SELECT_PROTOCOL, + d: { + protocol: 'udp', + data: { + address: packet.address, + port: packet.port, + mode: this.voiceConnection.authentication.mode, + }, + }, + }); + + this.emit('debug', `[UDP] << ${JSON.stringify(packet)}`); + + socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer)); + }); + + const blankMessage = Buffer.alloc(70); + blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4); + this.emit('debug', `Sending IP discovery packet: [${[...blankMessage]}]`); + await this.send(blankMessage); + this.emit('debug', `Successfully sent IP discovery packet`); + } +} + +function parseLocalPacket(message) { + try { + const packet = Buffer.from(message); + let address = ''; + for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]); + const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10); + return { address, port }; + } catch (error) { + return { error }; + } +} + +module.exports = VoiceConnectionUDPClient; diff --git a/node_modules/discord.js/src/client/voice/networking/VoiceWebSocket.js b/node_modules/discord.js/src/client/voice/networking/VoiceWebSocket.js new file mode 100644 index 0000000..efc97af --- /dev/null +++ b/node_modules/discord.js/src/client/voice/networking/VoiceWebSocket.js @@ -0,0 +1,264 @@ +'use strict'; + +const EventEmitter = require('events'); +const WebSocket = require('../../../WebSocket'); +const { Error } = require('../../../errors'); +const { OPCodes, VoiceOPCodes } = require('../../../util/Constants'); + +/** + * Represents a Voice Connection's WebSocket. + * @extends {EventEmitter} + * @private + */ +class VoiceWebSocket extends EventEmitter { + constructor(connection) { + super(); + /** + * The Voice Connection that this WebSocket serves + * @type {VoiceConnection} + */ + this.connection = connection; + + /** + * How many connection attempts have been made + * @type {number} + */ + this.attempts = 0; + + this.dead = false; + this.connection.on('closing', this.shutdown.bind(this)); + } + + /** + * The client of this voice WebSocket + * @type {Client} + * @readonly + */ + get client() { + return this.connection.client; + } + + shutdown() { + this.emit('debug', `[WS] shutdown requested`); + this.dead = true; + this.reset(); + } + + /** + * Resets the current WebSocket. + */ + reset() { + this.emit('debug', `[WS] reset requested`); + if (this.ws) { + if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close(); + this.ws = null; + } + this.clearHeartbeat(); + } + + /** + * Starts connecting to the Voice WebSocket Server. + */ + connect() { + this.emit('debug', `[WS] connect requested`); + if (this.dead) return; + if (this.ws) this.reset(); + if (this.attempts >= 5) { + this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts)); + return; + } + + this.attempts++; + + /** + * The actual WebSocket used to connect to the Voice WebSocket Server. + * @type {WebSocket} + */ + this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 4 }); + this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`); + this.ws.onopen = this.onOpen.bind(this); + this.ws.onmessage = this.onMessage.bind(this); + this.ws.onclose = this.onClose.bind(this); + this.ws.onerror = this.onError.bind(this); + } + + /** + * Sends data to the WebSocket if it is open. + * @param {string} data The data to send to the WebSocket + * @returns {Promise<string>} + */ + send(data) { + this.emit('debug', `[WS] >> ${data}`); + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data); + this.ws.send(data, null, error => { + if (error) reject(error); + else resolve(data); + }); + }); + } + + /** + * JSON.stringify's a packet and then sends it to the WebSocket Server. + * @param {Object} packet The packet to send + * @returns {Promise<string>} + */ + sendPacket(packet) { + try { + packet = JSON.stringify(packet); + } catch (error) { + return Promise.reject(error); + } + return this.send(packet); + } + + /** + * Called whenever the WebSocket opens. + */ + onOpen() { + this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`); + this.sendPacket({ + op: OPCodes.DISPATCH, + d: { + server_id: this.connection.channel.guild.id, + user_id: this.client.user.id, + token: this.connection.authentication.token, + session_id: this.connection.authentication.sessionID, + }, + }).catch(() => { + this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED')); + }); + } + + /** + * Called whenever a message is received from the WebSocket. + * @param {MessageEvent} event The message event that was received + * @returns {void} + */ + onMessage(event) { + try { + return this.onPacket(WebSocket.unpack(event.data, 'json')); + } catch (error) { + return this.onError(error); + } + } + + /** + * Called whenever the connection to the WebSocket server is lost. + */ + onClose() { + this.emit('debug', `[WS] closed`); + if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000); + } + + /** + * Called whenever an error occurs with the WebSocket. + * @param {Error} error The error that occurred + */ + onError(error) { + this.emit('debug', `[WS] Error: ${error}`); + this.emit('error', error); + } + + /** + * Called whenever a valid packet is received from the WebSocket. + * @param {Object} packet The received packet + */ + onPacket(packet) { + this.emit('debug', `[WS] << ${JSON.stringify(packet)}`); + switch (packet.op) { + case VoiceOPCodes.HELLO: + this.setHeartbeat(packet.d.heartbeat_interval); + break; + case VoiceOPCodes.READY: + /** + * Emitted once the voice WebSocket receives the ready packet. + * @param {Object} packet The received packet + * @event VoiceWebSocket#ready + */ + this.emit('ready', packet.d); + break; + /* eslint-disable no-case-declarations */ + case VoiceOPCodes.SESSION_DESCRIPTION: + packet.d.secret_key = new Uint8Array(packet.d.secret_key); + /** + * Emitted once the Voice Websocket receives a description of this voice session. + * @param {Object} packet The received packet + * @event VoiceWebSocket#sessionDescription + */ + this.emit('sessionDescription', packet.d); + break; + case VoiceOPCodes.CLIENT_CONNECT: + this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id); + break; + case VoiceOPCodes.CLIENT_DISCONNECT: + const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id); + if (streamInfo) { + this.connection.receiver.packets.streams.delete(packet.d.user_id); + streamInfo.stream.push(null); + } + break; + case VoiceOPCodes.SPEAKING: + /** + * Emitted whenever a speaking packet is received. + * @param {Object} data + * @event VoiceWebSocket#startSpeaking + */ + this.emit('startSpeaking', packet.d); + break; + default: + /** + * Emitted when an unhandled packet is received. + * @param {Object} packet + * @event VoiceWebSocket#unknownPacket + */ + this.emit('unknownPacket', packet); + break; + } + } + + /** + * Sets an interval at which to send a heartbeat packet to the WebSocket. + * @param {number} interval The interval at which to send a heartbeat packet + */ + setHeartbeat(interval) { + if (!interval || isNaN(interval)) { + this.onError(new Error('VOICE_INVALID_HEARTBEAT')); + return; + } + if (this.heartbeatInterval) { + /** + * Emitted whenever the voice WebSocket encounters a non-fatal error. + * @param {string} warn The warning + * @event VoiceWebSocket#warn + */ + this.emit('warn', 'A voice heartbeat interval is being overwritten'); + this.client.clearInterval(this.heartbeatInterval); + } + this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval); + } + + /** + * Clears a heartbeat interval, if one exists. + */ + clearHeartbeat() { + if (!this.heartbeatInterval) { + this.emit('warn', 'Tried to clear a heartbeat interval that does not exist'); + return; + } + this.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + + /** + * Sends a heartbeat packet. + */ + sendHeartbeat() { + this.sendPacket({ op: VoiceOPCodes.HEARTBEAT, d: Math.floor(Math.random() * 10e10) }).catch(() => { + this.emit('warn', 'Tried to send heartbeat, but connection is not open'); + this.clearHeartbeat(); + }); + } +} + +module.exports = VoiceWebSocket; diff --git a/node_modules/discord.js/src/client/voice/player/AudioPlayer.js b/node_modules/discord.js/src/client/voice/player/AudioPlayer.js new file mode 100644 index 0000000..6f719a7 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/AudioPlayer.js @@ -0,0 +1,27 @@ +'use strict'; + +const BasePlayer = require('./BasePlayer'); + +/** + * An Audio Player for a Voice Connection. + * @private + * @extends {BasePlayer} + */ +class AudioPlayer extends BasePlayer { + constructor(voiceConnection) { + super(); + /** + * The voice connection that the player serves + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + } + + playBroadcast(broadcast, options) { + const dispatcher = this.createDispatcher(options, { broadcast }); + broadcast.add(dispatcher); + return dispatcher; + } +} + +module.exports = AudioPlayer; diff --git a/node_modules/discord.js/src/client/voice/player/BasePlayer.js b/node_modules/discord.js/src/client/voice/player/BasePlayer.js new file mode 100644 index 0000000..b968f82 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/BasePlayer.js @@ -0,0 +1,92 @@ +'use strict'; + +const EventEmitter = require('events'); +const { Readable: ReadableStream } = require('stream'); +const prism = require('prism-media'); +const StreamDispatcher = require('../dispatcher/StreamDispatcher'); + +const FFMPEG_ARGUMENTS = ['-analyzeduration', '0', '-loglevel', '0', '-f', 's16le', '-ar', '48000', '-ac', '2']; + +/** + * An Audio Player for a Voice Connection. + * @private + * @extends {EventEmitter} + */ +class BasePlayer extends EventEmitter { + constructor() { + super(); + + this.dispatcher = null; + + this.streamingData = { + channels: 2, + sequence: 0, + timestamp: 0, + }; + } + + destroy() { + this.destroyDispatcher(); + } + + destroyDispatcher() { + if (this.dispatcher) { + this.dispatcher.destroy(); + this.dispatcher = null; + } + } + + playUnknown(input, options) { + this.destroyDispatcher(); + + const isStream = input instanceof ReadableStream; + + const args = isStream ? FFMPEG_ARGUMENTS.slice() : ['-i', input, ...FFMPEG_ARGUMENTS]; + if (options.seek) args.unshift('-ss', String(options.seek)); + + const ffmpeg = new prism.FFmpeg({ args }); + const streams = { ffmpeg }; + if (isStream) { + streams.input = input; + input.pipe(ffmpeg); + } + return this.playPCMStream(ffmpeg, options, streams); + } + + playPCMStream(stream, options, streams = {}) { + this.destroyDispatcher(); + const opus = (streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 })); + if (options && options.volume === false) { + stream.pipe(opus); + return this.playOpusStream(opus, options, streams); + } + streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); + stream.pipe(streams.volume).pipe(opus); + return this.playOpusStream(opus, options, streams); + } + + playOpusStream(stream, options, streams = {}) { + this.destroyDispatcher(); + streams.opus = stream; + if (options.volume !== false && !streams.input) { + streams.input = stream; + const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); + streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); + streams.opus = stream + .pipe(decoder) + .pipe(streams.volume) + .pipe(new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 })); + } + const dispatcher = this.createDispatcher(options, streams); + streams.opus.pipe(dispatcher); + return dispatcher; + } + + createDispatcher(options, streams, broadcast) { + this.destroyDispatcher(); + const dispatcher = (this.dispatcher = new StreamDispatcher(this, options, streams, broadcast)); + return dispatcher; + } +} + +module.exports = BasePlayer; diff --git a/node_modules/discord.js/src/client/voice/player/BroadcastAudioPlayer.js b/node_modules/discord.js/src/client/voice/player/BroadcastAudioPlayer.js new file mode 100644 index 0000000..05197a4 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/BroadcastAudioPlayer.js @@ -0,0 +1,28 @@ +'use strict'; + +const BasePlayer = require('./BasePlayer'); +const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher'); + +/** + * An Audio Player for a Voice Connection. + * @private + * @extends {BasePlayer} + */ +class AudioPlayer extends BasePlayer { + constructor(broadcast) { + super(); + /** + * The broadcast that the player serves + * @type {VoiceBroadcast} + */ + this.broadcast = broadcast; + } + + createDispatcher(options, streams) { + this.destroyDispatcher(); + const dispatcher = (this.dispatcher = new BroadcastDispatcher(this, options, streams)); + return dispatcher; + } +} + +module.exports = AudioPlayer; diff --git a/node_modules/discord.js/src/client/voice/receiver/PacketHandler.js b/node_modules/discord.js/src/client/voice/receiver/PacketHandler.js new file mode 100644 index 0000000..c441c5e --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/PacketHandler.js @@ -0,0 +1,116 @@ +'use strict'; + +const EventEmitter = require('events'); +const secretbox = require('../util/Secretbox'); + +// The delay between packets when a user is considered to have stopped speaking +// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200 +const DISCORD_SPEAKING_DELAY = 250; + +class Readable extends require('stream').Readable { + _read() {} // eslint-disable-line no-empty-function +} + +class PacketHandler extends EventEmitter { + constructor(receiver) { + super(); + this.nonce = Buffer.alloc(24); + this.receiver = receiver; + this.streams = new Map(); + this.speakingTimeouts = new Map(); + } + + get connection() { + return this.receiver.connection; + } + + _stoppedSpeaking(userID) { + const streamInfo = this.streams.get(userID); + if (streamInfo && streamInfo.end === 'silence') { + this.streams.delete(userID); + streamInfo.stream.push(null); + } + } + + makeStream(user, end) { + if (this.streams.has(user)) return this.streams.get(user).stream; + const stream = new Readable(); + stream.on('end', () => this.streams.delete(user)); + this.streams.set(user, { stream, end }); + return stream; + } + + parseBuffer(buffer) { + const { secret_key, mode } = this.receiver.connection.authentication; + + // Choose correct nonce depending on encryption + let end; + if (mode === 'xsalsa20_poly1305_lite') { + buffer.copy(this.nonce, 0, buffer.length - 4); + end = buffer.length - 4; + } else if (mode === 'xsalsa20_poly1305_suffix') { + buffer.copy(this.nonce, 0, buffer.length - 24); + end = buffer.length - 24; + } else { + buffer.copy(this.nonce, 0, 0, 12); + } + + // Open packet + let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key); + if (!packet) return new Error('Failed to decrypt voice packet'); + packet = Buffer.from(packet); + + // Strip RTP Header Extensions (one-byte only) + if (packet[0] === 0xbe && packet[1] === 0xde && packet.length > 4) { + const headerExtensionLength = packet.readUInt16BE(2); + let offset = 4; + for (let i = 0; i < headerExtensionLength; i++) { + const byte = packet[offset]; + offset++; + if (byte === 0) continue; + offset += 1 + (0b1111 & (byte >> 4)); + } + // Skip over undocumented Discord byte + offset++; + + packet = packet.slice(offset); + } + + return packet; + } + + push(buffer) { + const ssrc = buffer.readUInt32BE(8); + const userStat = this.connection.ssrcMap.get(ssrc); + if (!userStat) return; + + let speakingTimeout = this.speakingTimeouts.get(ssrc); + if (typeof speakingTimeout === 'undefined') { + this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking }); + speakingTimeout = this.receiver.connection.client.setTimeout(() => { + try { + this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: 0 }); + this.receiver.connection.client.clearTimeout(speakingTimeout); + this.speakingTimeouts.delete(ssrc); + } catch { + // Connection already closed, ignore + } + }, DISCORD_SPEAKING_DELAY); + this.speakingTimeouts.set(ssrc, speakingTimeout); + } else { + speakingTimeout.refresh(); + } + + let stream = this.streams.get(userStat.userID); + if (!stream) return; + stream = stream.stream; + const opusPacket = this.parseBuffer(buffer); + if (opusPacket instanceof Error) { + this.emit('error', opusPacket); + return; + } + stream.push(opusPacket); + } +} + +module.exports = PacketHandler; diff --git a/node_modules/discord.js/src/client/voice/receiver/Receiver.js b/node_modules/discord.js/src/client/voice/receiver/Receiver.js new file mode 100644 index 0000000..605d992 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/Receiver.js @@ -0,0 +1,58 @@ +'use strict'; + +const EventEmitter = require('events'); +const prism = require('prism-media'); +const PacketHandler = require('./PacketHandler'); +const { Error } = require('../../../errors'); + +/** + * Receives audio packets from a voice connection. + * @example + * const receiver = connection.createReceiver(); + * // opusStream is a ReadableStream - that means you could play it back to a voice channel if you wanted to! + * const opusStream = receiver.createStream(user); + */ +class VoiceReceiver extends EventEmitter { + constructor(connection) { + super(); + this.connection = connection; + this.packets = new PacketHandler(this); + /** + * Emitted whenever there is a warning + * @event VoiceReceiver#debug + * @param {Error|string} error The error or message to debug + */ + this.packets.on('error', err => this.emit('debug', err)); + } + + /** + * Options passed to `VoiceReceiver#createStream`. + * @typedef {Object} ReceiveStreamOptions + * @property {string} [mode='opus'] The mode for audio output. This defaults to opus, meaning discord.js won't decode + * the packets for you. You can set this to 'pcm' so that the stream's output will be 16-bit little-endian stereo + * audio + * @property {string} [end='silence'] When the stream should be destroyed. If `silence`, this will be when the user + * stops talking. Otherwise, if `manual`, this should be handled by you. + */ + + /** + * Creates a new audio receiving stream. If a stream already exists for a user, then that stream will be returned + * rather than generating a new one. + * @param {UserResolvable} user The user to start listening to. + * @param {ReceiveStreamOptions} options Options. + * @returns {ReadableStream} + */ + createStream(user, { mode = 'opus', end = 'silence' } = {}) { + user = this.connection.client.users.resolve(user); + if (!user) throw new Error('VOICE_USER_MISSING'); + const stream = this.packets.makeStream(user.id, end); + if (mode === 'pcm') { + const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); + stream.pipe(decoder); + return decoder; + } + return stream; + } +} + +module.exports = VoiceReceiver; diff --git a/node_modules/discord.js/src/client/voice/util/PlayInterface.js b/node_modules/discord.js/src/client/voice/util/PlayInterface.js new file mode 100644 index 0000000..9478ee8 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/PlayInterface.js @@ -0,0 +1,94 @@ +'use strict'; + +const { Readable } = require('stream'); +const prism = require('prism-media'); +const { Error } = require('../../../errors'); + +/** + * Options that can be passed to stream-playing methods: + * @typedef {Object} StreamOptions + * @property {StreamType} [type='unknown'] The type of stream. + * @property {number} [seek=0] The time to seek to, will be ignored when playing `ogg/opus` or `webm/opus` streams + * @property {number|boolean} [volume=1] The volume to play at. Set this to false to disable volume transforms for + * this stream to improve performance. + * @property {number} [plp] Expected packet loss percentage + * @property {boolean} [fec] Enabled forward error correction + * @property {number|string} [bitrate=96] The bitrate (quality) of the audio in kbps. + * If set to 'auto', the voice channel's bitrate will be used + * @property {number} [highWaterMark=12] The maximum number of opus packets to make and store before they are + * actually needed. See https://nodejs.org/en/docs/guides/backpressuring-in-streams/. Setting this value to + * 1 means that changes in volume will be more instant. + */ + +/** + * An option passed as part of `StreamOptions` specifying the type of the stream. + * * `unknown`: The default type, streams/input will be passed through to ffmpeg before encoding. + * Will play most streams. + * * `converted`: Play a stream of 16bit signed stereo PCM data, skipping ffmpeg. + * * `opus`: Play a stream of opus packets, skipping ffmpeg. You lose the ability to alter volume. + * * `ogg/opus`: Play an ogg file with the opus encoding, skipping ffmpeg. You lose the ability to alter volume. + * * `webm/opus`: Play a webm file with opus audio, skipping ffmpeg. You lose the ability to alter volume. + * @typedef {string} StreamType + */ + +/** + * An interface class to allow you to play audio over VoiceConnections and VoiceBroadcasts. + */ +class PlayInterface { + constructor(player) { + this.player = player; + } + + /** + * Play an audio resource. + * @param {VoiceBroadcast|ReadableStream|string} resource The resource to play. + * @param {StreamOptions} [options] The options to play. + * @example + * // Play a local audio file + * connection.play('/home/hydrabolt/audio.mp3', { volume: 0.5 }); + * @example + * // Play a ReadableStream + * connection.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { quality: 'highestaudio' })); + * @example + * // Play a voice broadcast + * const broadcast = client.voice.createBroadcast(); + * broadcast.play('/home/hydrabolt/audio.mp3'); + * connection.play(broadcast); + * @example + * // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html + * connection.play('http://www.sample-videos.com/audio/mp3/wave.mp3'); + * @returns {StreamDispatcher} + */ + play(resource, options = {}) { + const VoiceBroadcast = require('../VoiceBroadcast'); + if (resource instanceof VoiceBroadcast) { + if (!this.player.playBroadcast) throw new Error('VOICE_PLAY_INTERFACE_NO_BROADCAST'); + return this.player.playBroadcast(resource, options); + } + if (resource instanceof Readable || typeof resource === 'string') { + const type = options.type || 'unknown'; + if (type === 'unknown') { + return this.player.playUnknown(resource, options); + } else if (type === 'converted') { + return this.player.playPCMStream(resource, options); + } else if (type === 'opus') { + return this.player.playOpusStream(resource, options); + } else if (type === 'ogg/opus') { + if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); + return this.player.playOpusStream(resource.pipe(new prism.opus.OggDemuxer()), options); + } else if (type === 'webm/opus') { + if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); + return this.player.playOpusStream(resource.pipe(new prism.opus.WebmDemuxer()), options); + } + } + throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); + } + + static applyToClass(structure) { + for (const prop of ['play']) { + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop)); + } + } +} + +module.exports = PlayInterface; diff --git a/node_modules/discord.js/src/client/voice/util/Secretbox.js b/node_modules/discord.js/src/client/voice/util/Secretbox.js new file mode 100644 index 0000000..c16a435 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/Secretbox.js @@ -0,0 +1,32 @@ +'use strict'; + +const libs = { + sodium: sodium => ({ + open: sodium.api.crypto_secretbox_open_easy, + close: sodium.api.crypto_secretbox_easy, + random: n => sodium.randombytes_buf(n), + }), + 'libsodium-wrappers': sodium => ({ + open: sodium.crypto_secretbox_open_easy, + close: sodium.crypto_secretbox_easy, + random: n => sodium.randombytes_buf(n), + }), + tweetnacl: tweetnacl => ({ + open: tweetnacl.secretbox.open, + close: tweetnacl.secretbox, + random: n => tweetnacl.randomBytes(n), + }), +}; + +exports.methods = {}; + +(async () => { + for (const libName of Object.keys(libs)) { + try { + const lib = require(libName); + if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; // eslint-disable-line no-await-in-loop + exports.methods = libs[libName](lib); + break; + } catch {} // eslint-disable-line no-empty + } +})(); diff --git a/node_modules/discord.js/src/client/voice/util/Silence.js b/node_modules/discord.js/src/client/voice/util/Silence.js new file mode 100644 index 0000000..9bea3d0 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/Silence.js @@ -0,0 +1,13 @@ +'use strict'; + +const { Readable } = require('stream'); + +const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]); + +class Silence extends Readable { + _read() { + this.push(SILENCE_FRAME); + } +} + +module.exports = Silence; diff --git a/node_modules/discord.js/src/client/voice/util/VolumeInterface.js b/node_modules/discord.js/src/client/voice/util/VolumeInterface.js new file mode 100644 index 0000000..0dca04f --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/VolumeInterface.js @@ -0,0 +1,103 @@ +'use strict'; + +const EventEmitter = require('events'); + +/** + * An interface class for volume transformation. + * @extends {EventEmitter} + */ +class VolumeInterface extends EventEmitter { + constructor({ volume = 1 } = {}) { + super(); + this.setVolume(volume); + } + + /** + * Whether or not the volume of this stream is editable + * @type {boolean} + * @readonly + */ + get volumeEditable() { + return true; + } + + /** + * The current volume of the stream + * @type {number} + * @readonly + */ + get volume() { + return this._volume; + } + + /** + * The current volume of the stream in decibels + * @type {number} + * @readonly + */ + get volumeDecibels() { + return Math.log10(this.volume) * 20; + } + + /** + * The current volume of the stream from a logarithmic scale + * @type {number} + * @readonly + */ + get volumeLogarithmic() { + return Math.pow(this.volume, 1 / 1.660964); + } + + applyVolume(buffer, volume) { + volume = volume || this._volume; + if (volume === 1) return buffer; + + const out = Buffer.alloc(buffer.length); + for (let i = 0; i < buffer.length; i += 2) { + if (i >= buffer.length - 1) break; + const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i)))); + out.writeInt16LE(uint, i); + } + + return out; + } + + /** + * Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double. + * @param {number} volume The volume that you want to set + */ + setVolume(volume) { + /** + * Emitted when the volume of this interface changes. + * @event VolumeInterface#volumeChange + * @param {number} oldVolume The old volume of this interface + * @param {number} newVolume The new volume of this interface + */ + this.emit('volumeChange', this._volume, volume); + this._volume = volume; + } + + /** + * Sets the volume in decibels. + * @param {number} db The decibels + */ + setVolumeDecibels(db) { + this.setVolume(Math.pow(10, db / 20)); + } + + /** + * Sets the volume so that a perceived value of 0.5 is half the perceived volume etc. + * @param {number} value The value for the volume + */ + setVolumeLogarithmic(value) { + this.setVolume(Math.pow(value, 1.660964)); + } +} + +const props = ['volumeDecibels', 'volumeLogarithmic', 'setVolumeDecibels', 'setVolumeLogarithmic']; + +exports.applyToClass = function applyToClass(structure) { + for (const prop of props) { + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop)); + } +}; diff --git a/node_modules/discord.js/src/client/websocket/WebSocketManager.js b/node_modules/discord.js/src/client/websocket/WebSocketManager.js new file mode 100644 index 0000000..1801106 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/WebSocketManager.js @@ -0,0 +1,439 @@ +'use strict'; + +const EventEmitter = require('events'); +const WebSocketShard = require('./WebSocketShard'); +const PacketHandlers = require('./handlers'); +const { Error: DJSError } = require('../../errors'); +const Collection = require('../../util/Collection'); +const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants'); +const Util = require('../../util/Util'); + +const BeforeReadyWhitelist = [ + WSEvents.READY, + WSEvents.RESUMED, + WSEvents.GUILD_CREATE, + WSEvents.GUILD_DELETE, + WSEvents.GUILD_MEMBERS_CHUNK, + WSEvents.GUILD_MEMBER_ADD, + WSEvents.GUILD_MEMBER_REMOVE, +]; + +const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes) + .slice(1) + .map(Number); +const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; + +/** + * The WebSocket manager for this client. + * <info>This class forwards raw dispatch events, + * read more about it here {@link https://discordapp.com/developers/docs/topics/gateway}</info> + * @extends EventEmitter + */ +class WebSocketManager extends EventEmitter { + constructor(client) { + super(); + + /** + * The client that instantiated this WebSocketManager + * @type {Client} + * @readonly + * @name WebSocketManager#client + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The gateway this manager uses + * @type {?string} + */ + this.gateway = undefined; + + /** + * The amount of shards this manager handles + * @private + * @type {number} + */ + this.totalShards = this.client.options.shards.length; + + /** + * A collection of all shards this manager handles + * @type {Collection<number, WebSocketShard>} + */ + this.shards = new Collection(); + + /** + * An array of shards to be connected or that need to reconnect + * @type {Set<WebSocketShard>} + * @private + * @name WebSocketManager#shardQueue + */ + Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true }); + + /** + * An array of queued events before this WebSocketManager became ready + * @type {object[]} + * @private + * @name WebSocketManager#packetQueue + */ + Object.defineProperty(this, 'packetQueue', { value: [] }); + + /** + * The current status of this WebSocketManager + * @type {number} + */ + this.status = Status.IDLE; + + /** + * If this manager was destroyed. It will prevent shards from reconnecting + * @type {boolean} + * @private + */ + this.destroyed = false; + + /** + * If this manager is currently reconnecting one or multiple shards + * @type {boolean} + * @private + */ + this.reconnecting = false; + + /** + * The current session limit of the client + * @private + * @type {?Object} + * @prop {number} total Total number of identifies available + * @prop {number} remaining Number of identifies remaining + * @prop {number} reset_after Number of milliseconds after which the limit resets + */ + this.sessionStartLimit = undefined; + } + + /** + * The average ping of all WebSocketShards + * @type {number} + * @readonly + */ + get ping() { + const sum = this.shards.reduce((a, b) => a + b.ping, 0); + return sum / this.shards.size; + } + + /** + * Emits a debug message. + * @param {string} message The debug message + * @param {?WebSocketShard} [shard] The shard that emitted this message, if any + * @private + */ + debug(message, shard) { + this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`); + } + + /** + * Connects this manager to the gateway. + * @private + */ + async connect() { + const invalidToken = new DJSError(WSCodes[4004]); + const { + url: gatewayURL, + shards: recommendedShards, + session_start_limit: sessionStartLimit, + } = await this.client.api.gateway.bot.get().catch(error => { + throw error.httpStatus === 401 ? invalidToken : error; + }); + + this.sessionStartLimit = sessionStartLimit; + + const { total, remaining, reset_after } = sessionStartLimit; + + this.debug(`Fetched Gateway Information + URL: ${gatewayURL} + Recommended Shards: ${recommendedShards}`); + + this.debug(`Session Limit Information + Total: ${total} + Remaining: ${remaining}`); + + this.gateway = `${gatewayURL}/`; + + let { shards } = this.client.options; + + if (shards === 'auto') { + this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); + this.totalShards = this.client.options.shardCount = recommendedShards; + shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); + } + + this.totalShards = shards.length; + this.debug(`Spawning shards: ${shards.join(', ')}`); + this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); + + await this._handleSessionLimit(remaining, reset_after); + + return this.createShards(); + } + + /** + * Handles the creation of a shard. + * @returns {Promise<boolean>} + * @private + */ + async createShards() { + // If we don't have any shards to handle, return + if (!this.shardQueue.size) return false; + + const [shard] = this.shardQueue; + + this.shardQueue.delete(shard); + + if (!shard.eventsAttached) { + shard.on(ShardEvents.ALL_READY, unavailableGuilds => { + /** + * Emitted when a shard turns ready. + * @event Client#shardReady + * @param {number} id The shard ID that turned ready + * @param {?Set<string>} unavailableGuilds Set of unavailable guild IDs, if any + */ + this.client.emit(Events.SHARD_READY, shard.id, unavailableGuilds); + + if (!this.shardQueue.size) this.reconnecting = false; + this.checkShardsReady(); + }); + + shard.on(ShardEvents.CLOSE, event => { + if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { + /** + * Emitted when a shard's WebSocket disconnects and will no longer reconnect. + * @event Client#shardDisconnect + * @param {CloseEvent} event The WebSocket close event + * @param {number} id The shard ID that disconnected + */ + this.client.emit(Events.SHARD_DISCONNECT, event, shard.id); + this.debug(WSCodes[event.code], shard); + return; + } + + if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) { + // These event codes cannot be resumed + shard.sessionID = undefined; + } + + /** + * Emitted when a shard is attempting to reconnect or re-identify. + * @event Client#shardReconnecting + * @param {number} id The shard ID that is attempting to reconnect + */ + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + this.shardQueue.add(shard); + + if (shard.sessionID) { + this.debug(`Session ID is present, attempting an immediate reconnect...`, shard); + this.reconnect(true); + } else { + shard.destroy({ reset: true, emit: false, log: false }); + this.reconnect(); + } + }); + + shard.on(ShardEvents.INVALID_SESSION, () => { + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + }); + + shard.on(ShardEvents.DESTROYED, () => { + this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); + + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + this.shardQueue.add(shard); + this.reconnect(); + }); + + shard.eventsAttached = true; + } + + this.shards.set(shard.id, shard); + + try { + await shard.connect(); + } catch (error) { + if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { + throw new DJSError(WSCodes[error.code]); + // Undefined if session is invalid, error event for regular closes + } else if (!error || error.code) { + this.debug('Failed to connect to the gateway, requeueing...', shard); + this.shardQueue.add(shard); + } else { + throw error; + } + } + // If we have more shards, add a 5s delay + if (this.shardQueue.size) { + this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`); + await Util.delayFor(5000); + await this._handleSessionLimit(); + return this.createShards(); + } + + return true; + } + + /** + * Handles reconnects for this manager. + * @param {boolean} [skipLimit=false] IF this reconnect should skip checking the session limit + * @private + * @returns {Promise<boolean>} + */ + async reconnect(skipLimit = false) { + if (this.reconnecting || this.status !== Status.READY) return false; + this.reconnecting = true; + try { + if (!skipLimit) await this._handleSessionLimit(); + await this.createShards(); + } catch (error) { + this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); + if (error.httpStatus !== 401) { + this.debug(`Possible network error occurred. Retrying in 5s...`); + await Util.delayFor(5000); + this.reconnecting = false; + return this.reconnect(); + } + // If we get an error at this point, it means we cannot reconnect anymore + if (this.client.listenerCount(Events.INVALIDATED)) { + /** + * Emitted when the client's session becomes invalidated. + * You are expected to handle closing the process gracefully and preventing a boot loop + * if you are listening to this event. + * @event Client#invalidated + */ + this.client.emit(Events.INVALIDATED); + // Destroy just the shards. This means you have to handle the cleanup yourself + this.destroy(); + } else { + this.client.destroy(); + } + } finally { + this.reconnecting = false; + } + return true; + } + + /** + * Broadcasts a packet to every shard this manager handles. + * @param {Object} packet The packet to send + * @private + */ + broadcast(packet) { + for (const shard of this.shards.values()) shard.send(packet); + } + + /** + * Destroys this manager and all its shards. + * @private + */ + destroy() { + if (this.destroyed) return; + this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); + this.destroyed = true; + this.shardQueue.clear(); + for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false, log: false }); + } + + /** + * Handles the timeout required if we cannot identify anymore. + * @param {number} [remaining] The amount of remaining identify sessions that can be done today + * @param {number} [resetAfter] The amount of time in which the identify counter resets + * @private + */ + async _handleSessionLimit(remaining, resetAfter) { + if (typeof remaining === 'undefined' && typeof resetAfter === 'undefined') { + const { session_start_limit } = await this.client.api.gateway.bot.get(); + this.sessionStartLimit = session_start_limit; + remaining = session_start_limit.remaining; + resetAfter = session_start_limit.reset_after; + this.debug(`Session Limit Information + Total: ${session_start_limit.total} + Remaining: ${remaining}`); + } + if (!remaining) { + this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`); + await Util.delayFor(resetAfter); + } + } + + /** + * Processes a packet and queues it if this WebSocketManager is not ready. + * @param {Object} [packet] The packet to be handled + * @param {WebSocketShard} [shard] The shard that will handle this packet + * @returns {boolean} + * @private + */ + handlePacket(packet, shard) { + if (packet && this.status !== Status.READY) { + if (!BeforeReadyWhitelist.includes(packet.t)) { + this.packetQueue.push({ packet, shard }); + return false; + } + } + + if (this.packetQueue.length) { + const item = this.packetQueue.shift(); + this.client.setImmediate(() => { + this.handlePacket(item.packet, item.shard); + }); + } + + if (packet && PacketHandlers[packet.t]) { + PacketHandlers[packet.t](this.client, packet, shard); + } + + return true; + } + + /** + * Checks whether the client is ready to be marked as ready. + * @private + */ + async checkShardsReady() { + if (this.status === Status.READY) return; + if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) { + return; + } + + this.status = Status.NEARLY; + + if (this.client.options.fetchAllMembers) { + try { + const promises = this.client.guilds.cache.map(guild => { + if (guild.available) return guild.members.fetch(); + // Return empty promise if guild is unavailable + return Promise.resolve(); + }); + await Promise.all(promises); + } catch (err) { + this.debug(`Failed to fetch all members before ready! ${err}\n${err.stack}`); + } + } + + this.triggerClientReady(); + } + + /** + * Causes the client to be marked as ready and emits the ready event. + * @private + */ + triggerClientReady() { + this.status = Status.READY; + + this.client.readyAt = new Date(); + + /** + * Emitted when the client becomes ready to start working. + * @event Client#ready + */ + this.client.emit(Events.CLIENT_READY); + + this.handlePacket(); + } +} + +module.exports = WebSocketManager; diff --git a/node_modules/discord.js/src/client/websocket/WebSocketShard.js b/node_modules/discord.js/src/client/websocket/WebSocketShard.js new file mode 100644 index 0000000..9f80f45 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/WebSocketShard.js @@ -0,0 +1,763 @@ +'use strict'; + +const EventEmitter = require('events'); +const WebSocket = require('../../WebSocket'); +const { browser, Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); + +const STATUS_KEYS = Object.keys(Status); +const CONNECTION_STATE = Object.keys(WebSocket.WebSocket); + +let zlib; + +if (!browser) { + try { + zlib = require('zlib-sync'); + } catch {} // eslint-disable-line no-empty +} + +/** + * Represents a Shard's WebSocket connection + */ +class WebSocketShard extends EventEmitter { + constructor(manager, id) { + super(); + + /** + * The WebSocketManager of the shard + * @type {WebSocketManager} + */ + this.manager = manager; + + /** + * The ID of the shard + * @type {number} + */ + this.id = id; + + /** + * The current status of the shard + * @type {Status} + */ + this.status = Status.IDLE; + + /** + * The current sequence of the shard + * @type {number} + * @private + */ + this.sequence = -1; + + /** + * The sequence of the shard after close + * @type {number} + * @private + */ + this.closeSequence = 0; + + /** + * The current session ID of the shard + * @type {string} + * @private + */ + this.sessionID = undefined; + + /** + * The previous heartbeat ping of the shard + * @type {number} + */ + this.ping = -1; + + /** + * The last time a ping was sent (a timestamp) + * @type {number} + * @private + */ + this.lastPingTimestamp = -1; + + /** + * If we received a heartbeat ack back. Used to identify zombie connections + * @type {boolean} + * @private + */ + this.lastHeartbeatAcked = true; + + /** + * Contains the rate limit queue and metadata + * @type {Object} + * @private + */ + Object.defineProperty(this, 'ratelimit', { + value: { + queue: [], + total: 120, + remaining: 120, + time: 60e3, + timer: null, + }, + }); + + /** + * The WebSocket connection for the current shard + * @type {?WebSocket} + * @private + */ + Object.defineProperty(this, 'connection', { value: null, writable: true }); + + /** + * @external Inflate + * @see {@link https://www.npmjs.com/package/zlib-sync} + */ + + /** + * The compression to use + * @type {?Inflate} + * @private + */ + Object.defineProperty(this, 'inflate', { value: null, writable: true }); + + /** + * The HELLO timeout + * @type {?NodeJS.Timer} + * @private + */ + Object.defineProperty(this, 'helloTimeout', { value: undefined, writable: true }); + + /** + * If the manager attached its event handlers on the shard + * @type {boolean} + * @private + */ + Object.defineProperty(this, 'eventsAttached', { value: false, writable: true }); + + /** + * A set of guild IDs this shard expects to receive + * @type {?Set<string>} + * @private + */ + Object.defineProperty(this, 'expectedGuilds', { value: undefined, writable: true }); + + /** + * The ready timeout + * @type {?NodeJS.Timer} + * @private + */ + Object.defineProperty(this, 'readyTimeout', { value: undefined, writable: true }); + + /** + * Time when the WebSocket connection was opened + * @type {number} + * @private + */ + Object.defineProperty(this, 'connectedAt', { value: 0, writable: true }); + } + + /** + * Emits a debug event. + * @param {string} message The debug message + * @private + */ + debug(message) { + this.manager.debug(message, this); + } + + /** + * Connects the shard to the gateway. + * @private + * @returns {Promise<void>} A promise that will resolve if the shard turns ready successfully, + * or reject if we couldn't connect + */ + connect() { + const { gateway, client } = this.manager; + + if (this.connection && this.connection.readyState === WebSocket.OPEN && this.status === Status.READY) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const cleanup = () => { + this.removeListener(ShardEvents.CLOSE, onClose); + this.removeListener(ShardEvents.READY, onReady); + this.removeListener(ShardEvents.RESUMED, onResumed); + this.removeListener(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.removeListener(ShardEvents.DESTROYED, onInvalidOrDestroyed); + }; + + const onReady = () => { + cleanup(); + resolve(); + }; + + const onResumed = () => { + cleanup(); + resolve(); + }; + + const onClose = event => { + cleanup(); + reject(event); + }; + + const onInvalidOrDestroyed = () => { + cleanup(); + // eslint-disable-next-line prefer-promise-reject-errors + reject(); + }; + + this.once(ShardEvents.READY, onReady); + this.once(ShardEvents.RESUMED, onResumed); + this.once(ShardEvents.CLOSE, onClose); + this.once(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.once(ShardEvents.DESTROYED, onInvalidOrDestroyed); + + if (this.connection && this.connection.readyState === WebSocket.OPEN) { + this.debug('An open connection was found, attempting an immediate identify.'); + this.identify(); + return; + } + + if (this.connection) { + this.debug(`A connection object was found. Cleaning up before continuing. + State: ${CONNECTION_STATE[this.connection.readyState]}`); + this.destroy({ emit: false }); + } + + const wsQuery = { v: client.options.ws.version }; + + if (zlib) { + this.inflate = new zlib.Inflate({ + chunkSize: 65535, + flush: zlib.Z_SYNC_FLUSH, + to: WebSocket.encoding === 'json' ? 'string' : '', + }); + wsQuery.compress = 'zlib-stream'; + } + + this.debug( + `[CONNECT] + Gateway : ${gateway} + Version : ${client.options.ws.version} + Encoding : ${WebSocket.encoding} + Compression: ${zlib ? 'zlib-stream' : 'none'}`, + ); + + this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; + this.setHelloTimeout(); + + this.connectedAt = Date.now(); + + const ws = (this.connection = WebSocket.create(gateway, wsQuery)); + ws.onopen = this.onOpen.bind(this); + ws.onmessage = this.onMessage.bind(this); + ws.onerror = this.onError.bind(this); + ws.onclose = this.onClose.bind(this); + }); + } + + /** + * Called whenever a connection is opened to the gateway. + * @private + */ + onOpen() { + this.debug(`[CONNECTED] ${this.connection.url} in ${Date.now() - this.connectedAt}ms`); + this.status = Status.NEARLY; + } + + /** + * Called whenever a message is received. + * @param {MessageEvent} event Event received + * @private + */ + onMessage({ data }) { + let raw; + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + if (zlib) { + const l = data.length; + const flush = + l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff; + + this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); + if (!flush) return; + raw = this.inflate.result; + } else { + raw = data; + } + let packet; + try { + packet = WebSocket.unpack(raw); + this.manager.client.emit(Events.RAW, packet, this.id); + if (packet.op === OPCodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id); + } catch (err) { + this.manager.client.emit(Events.SHARD_ERROR, err, this.id); + return; + } + this.onPacket(packet); + } + + /** + * Called whenever an error occurs with the WebSocket. + * @param {ErrorEvent} event The error that occurred + * @private + */ + onError(event) { + const error = event && event.error ? event.error : event; + if (!error) return; + + /** + * Emitted whenever a shard's WebSocket encounters a connection error. + * @event Client#shardError + * @param {Error} error The encountered error + * @param {number} shardID The shard that encountered this error + */ + this.manager.client.emit(Events.SHARD_ERROR, error, this.id); + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * @external ErrorEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent} + */ + + /** + * @external MessageEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} + */ + + /** + * Called whenever a connection to the gateway is closed. + * @param {CloseEvent} event Close event that was received + * @private + */ + onClose(event) { + if (this.sequence !== -1) this.closeSequence = this.sequence; + this.sequence = -1; + + this.debug(`[CLOSE] + Event Code: ${event.code} + Clean : ${event.wasClean} + Reason : ${event.reason || 'No reason received'}`); + + this.setHeartbeatTimer(-1); + this.setHelloTimeout(-1); + // If we still have a connection object, clean up its listeners + if (this.connection) this._cleanupConnection(); + + this.status = Status.DISCONNECTED; + + /** + * Emitted when a shard's WebSocket closes. + * @private + * @event WebSocketShard#close + * @param {CloseEvent} event The received event + */ + this.emit(ShardEvents.CLOSE, event); + } + + /** + * Called whenever a packet is received. + * @param {Object} packet The received packet + * @private + */ + onPacket(packet) { + if (!packet) { + this.debug(`Received broken packet: '${packet}'.`); + return; + } + + switch (packet.t) { + case WSEvents.READY: + /** + * Emitted when the shard receives the READY payload and is now waiting for guilds + * @event WebSocketShard#ready + */ + this.emit(ShardEvents.READY); + + this.sessionID = packet.d.session_id; + this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); + this.status = Status.WAITING_FOR_GUILDS; + this.debug(`[READY] Session ${this.sessionID}.`); + this.lastHeartbeatAcked = true; + this.sendHeartbeat('ReadyHeartbeat'); + break; + case WSEvents.RESUMED: { + /** + * Emitted when the shard resumes successfully + * @event WebSocketShard#resumed + */ + this.emit(ShardEvents.RESUMED); + + this.status = Status.READY; + const replayed = packet.s - this.closeSequence; + this.debug(`[RESUMED] Session ${this.sessionID} | Replayed ${replayed} events.`); + this.lastHeartbeatAcked = true; + this.sendHeartbeat('ResumeHeartbeat'); + break; + } + } + + if (packet.s > this.sequence) this.sequence = packet.s; + + switch (packet.op) { + case OPCodes.HELLO: + this.setHelloTimeout(-1); + this.setHeartbeatTimer(packet.d.heartbeat_interval); + this.identify(); + break; + case OPCodes.RECONNECT: + this.debug('[RECONNECT] Discord asked us to reconnect'); + this.destroy({ closeCode: 4000 }); + break; + case OPCodes.INVALID_SESSION: + this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); + // If we can resume the session, do so immediately + if (packet.d) { + this.identifyResume(); + return; + } + // Reset the sequence + this.sequence = -1; + // Reset the session ID as it's invalid + this.sessionID = undefined; + // Set the status to reconnecting + this.status = Status.RECONNECTING; + // Finally, emit the INVALID_SESSION event + this.emit(ShardEvents.INVALID_SESSION); + break; + case OPCodes.HEARTBEAT_ACK: + this.ackHeartbeat(); + break; + case OPCodes.HEARTBEAT: + this.sendHeartbeat('HeartbeatRequest', true); + break; + default: + this.manager.handlePacket(packet, this); + if (this.status === Status.WAITING_FOR_GUILDS && packet.t === WSEvents.GUILD_CREATE) { + this.expectedGuilds.delete(packet.d.id); + this.checkReady(); + } + } + } + + /** + * Checks if the shard can be marked as ready + * @private + */ + checkReady() { + // Step 0. Clear the ready timeout, if it exists + if (this.readyTimeout) { + this.manager.client.clearTimeout(this.readyTimeout); + this.readyTimeout = undefined; + } + // Step 1. If we don't have any other guilds pending, we are ready + if (!this.expectedGuilds.size) { + this.debug('Shard received all its guilds. Marking as fully ready.'); + this.status = Status.READY; + + /** + * Emitted when the shard is fully ready. + * This event is emitted if: + * * all guilds were received by this shard + * * the ready timeout expired, and some guilds are unavailable + * @event WebSocketShard#allReady + * @param {?Set<string>} unavailableGuilds Set of unavailable guilds, if any + */ + this.emit(ShardEvents.ALL_READY); + return; + } + // Step 2. Create a 15s timeout that will mark the shard as ready if there are still unavailable guilds + this.readyTimeout = this.manager.client.setTimeout(() => { + this.debug(`Shard did not receive any more guild packets in 15 seconds. + Unavailable guild count: ${this.expectedGuilds.size}`); + + this.readyTimeout = undefined; + + this.status = Status.READY; + + this.emit(ShardEvents.ALL_READY, this.expectedGuilds); + }, 15000); + } + + /** + * Sets the HELLO packet timeout. + * @param {number} [time] If set to -1, it will clear the hello timeout timeout + * @private + */ + setHelloTimeout(time) { + if (time === -1) { + if (this.helloTimeout) { + this.debug('Clearing the HELLO timeout.'); + this.manager.client.clearTimeout(this.helloTimeout); + this.helloTimeout = undefined; + } + return; + } + this.debug('Setting a HELLO timeout for 20s.'); + this.helloTimeout = this.manager.client.setTimeout(() => { + this.debug('Did not receive HELLO in time. Destroying and connecting again.'); + this.destroy({ reset: true, closeCode: 4009 }); + }, 20000); + } + + /** + * Sets the heartbeat timer for this shard. + * @param {number} time If -1, clears the interval, any other number sets an interval + * @private + */ + setHeartbeatTimer(time) { + if (time === -1) { + if (this.heartbeatInterval) { + this.debug('Clearing the heartbeat interval.'); + this.manager.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = undefined; + } + return; + } + this.debug(`Setting a heartbeat interval for ${time}ms.`); + // Sanity checks + if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); + } + + /** + * Sends a heartbeat to the WebSocket. + * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect + * @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent + * @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully. + * @private + */ + sendHeartbeat( + tag = 'HeartbeatTimer', + ignoreHeartbeatAck = [Status.WAITING_FOR_GUILDS, Status.IDENTIFYING, Status.RESUMING].includes(this.status), + ) { + if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) { + this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`); + } else if (!this.lastHeartbeatAcked) { + this.debug( + `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. + Status : ${STATUS_KEYS[this.status]} + Sequence : ${this.sequence} + Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`, + ); + + this.destroy({ closeCode: 4009, reset: true }); + return; + } + + this.debug(`[${tag}] Sending a heartbeat.`); + this.lastHeartbeatAcked = false; + this.lastPingTimestamp = Date.now(); + this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }, true); + } + + /** + * Acknowledges a heartbeat. + * @private + */ + ackHeartbeat() { + this.lastHeartbeatAcked = true; + const latency = Date.now() - this.lastPingTimestamp; + this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`); + this.ping = latency; + } + + /** + * Identifies the client on the connection. + * @private + * @returns {void} + */ + identify() { + return this.sessionID ? this.identifyResume() : this.identifyNew(); + } + + /** + * Identifies as a new connection on the gateway. + * @private + */ + identifyNew() { + const { client } = this.manager; + if (!client.token) { + this.debug('[IDENTIFY] No token available to identify a new session.'); + return; + } + + this.status = Status.IDENTIFYING; + + // Clone the identify payload and assign the token and shard info + const d = { + ...client.options.ws, + token: client.token, + shard: [this.id, Number(client.options.shardCount)], + }; + + this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount}`); + this.send({ op: OPCodes.IDENTIFY, d }, true); + } + + /** + * Resumes a session on the gateway. + * @private + */ + identifyResume() { + if (!this.sessionID) { + this.debug('[RESUME] No session ID was present; identifying as a new session.'); + this.identifyNew(); + return; + } + + this.status = Status.RESUMING; + + this.debug(`[RESUME] Session ${this.sessionID}, sequence ${this.closeSequence}`); + + const d = { + token: this.manager.client.token, + session_id: this.sessionID, + seq: this.closeSequence, + }; + + this.send({ op: OPCodes.RESUME, d }, true); + } + + /** + * Adds a packet to the queue to be sent to the gateway. + * <warn>If you use this method, make sure you understand that you need to provide + * a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands). + * Do not use this method if you don't know what you're doing.</warn> + * @param {Object} data The full packet to send + * @param {boolean} [important=false] If this packet should be added first in queue + */ + send(data, important = false) { + this.ratelimit.queue[important ? 'unshift' : 'push'](data); + this.processQueue(); + } + + /** + * Sends data, bypassing the queue. + * @param {Object} data Packet to send + * @returns {void} + * @private + */ + _send(data) { + if (!this.connection || this.connection.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`); + this.destroy({ close: 4000 }); + return; + } + + this.connection.send(WebSocket.pack(data), err => { + if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id); + }); + } + + /** + * Processes the current WebSocket queue. + * @returns {void} + * @private + */ + processQueue() { + if (this.ratelimit.remaining === 0) return; + if (this.ratelimit.queue.length === 0) return; + if (this.ratelimit.remaining === this.ratelimit.total) { + this.ratelimit.timer = this.manager.client.setTimeout(() => { + this.ratelimit.remaining = this.ratelimit.total; + this.processQueue(); + }, this.ratelimit.time); + } + while (this.ratelimit.remaining > 0) { + const item = this.ratelimit.queue.shift(); + if (!item) return; + this._send(item); + this.ratelimit.remaining--; + } + } + + /** + * Destroys this shard and closes its WebSocket connection. + * @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard + * @private + */ + destroy({ closeCode = 1000, reset = false, emit = true, log = true } = {}) { + if (log) { + this.debug(`[DESTROY] + Close Code : ${closeCode} + Reset : ${reset} + Emit DESTROYED: ${emit}`); + } + + // Step 0: Remove all timers + this.setHeartbeatTimer(-1); + this.setHelloTimeout(-1); + + // Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED + if (this.connection) { + // If the connection is currently opened, we will (hopefully) receive close + if (this.connection.readyState === WebSocket.OPEN) { + this.connection.close(closeCode); + } else { + // Connection is not OPEN + this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`); + // Remove listeners from the connection + this._cleanupConnection(); + // Attempt to close the connection just in case + try { + this.connection.close(closeCode); + } catch { + // No-op + } + // Emit the destroyed event if needed + if (emit) this._emitDestroyed(); + } + } else if (emit) { + // We requested a destroy, but we had no connection. Emit destroyed + this._emitDestroyed(); + } + + // Step 2: Null the connection object + this.connection = null; + + // Step 3: Set the shard status to DISCONNECTED + this.status = Status.DISCONNECTED; + + // Step 4: Cache the old sequence (use to attempt a resume) + if (this.sequence !== -1) this.closeSequence = this.sequence; + + // Step 5: Reset the sequence and session ID if requested + if (reset) { + this.sequence = -1; + this.sessionID = undefined; + } + + // Step 6: reset the ratelimit data + this.ratelimit.remaining = this.ratelimit.total; + this.ratelimit.queue.length = 0; + if (this.ratelimit.timer) { + this.manager.client.clearTimeout(this.ratelimit.timer); + this.ratelimit.timer = null; + } + } + + /** + * Cleans up the WebSocket connection listeners. + * @private + */ + _cleanupConnection() { + this.connection.onopen = this.connection.onclose = this.connection.onerror = this.connection.onmessage = null; + } + + /** + * Emits the DESTROYED event on the shard + * @private + */ + _emitDestroyed() { + /** + * Emitted when a shard is destroyed, but no WebSocket connection was present. + * @private + * @event WebSocketShard#destroyed + */ + this.emit(ShardEvents.DESTROYED); + } +} + +module.exports = WebSocketShard; diff --git a/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_CREATE.js b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_CREATE.js new file mode 100644 index 0000000..d6d560d --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.ChannelCreate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_DELETE.js b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_DELETE.js new file mode 100644 index 0000000..cb9f3d8 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.ChannelDelete.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js new file mode 100644 index 0000000..13e6f0f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -0,0 +1,22 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.cache.get(data.channel_id); + const time = new Date(data.last_pin_timestamp); + + if (channel && !Number.isNaN(time.getTime())) { + // Discord sends null for last_pin_timestamp if the last pinned message was removed + channel.lastPinTimestamp = time.getTime() || null; + + /** + * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, + * not much information can be provided easily here - you need to manually check the pins yourself. + * @event Client#channelPinsUpdate + * @param {DMChannel|TextChannel} channel The channel that the pins update occurred in + * @param {Date} time The time of the pins update + */ + client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); + } +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_UPDATE.js new file mode 100644 index 0000000..d441478 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -0,0 +1,16 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { old, updated } = client.actions.ChannelUpdate.handle(packet.d); + if (old && updated) { + /** + * Emitted whenever a channel is updated - e.g. name change, topic change, channel type change. + * @event Client#channelUpdate + * @param {DMChannel|GuildChannel} oldChannel The channel before the update + * @param {DMChannel|GuildChannel} newChannel The channel after the update + */ + client.emit(Events.CHANNEL_UPDATE, old, updated); + } +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_BAN_ADD.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_BAN_ADD.js new file mode 100644 index 0000000..5d4a096 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -0,0 +1,16 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.cache.get(data.guild_id); + const user = client.users.add(data.user); + + /** + * Emitted whenever a member is banned from a guild. + * @event Client#guildBanAdd + * @param {Guild} guild The guild that the ban occurred in + * @param {User} user The user that was banned + */ + if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_BAN_REMOVE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_BAN_REMOVE.js new file mode 100644 index 0000000..8389e46 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_BAN_REMOVE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildBanRemove.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_CREATE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_CREATE.js new file mode 100644 index 0000000..6743204 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_CREATE.js @@ -0,0 +1,36 @@ +'use strict'; + +const { Events, Status } = require('../../../util/Constants'); + +module.exports = async (client, { d: data }, shard) => { + let guild = client.guilds.cache.get(data.id); + if (guild) { + if (!guild.available && !data.unavailable) { + // A newly available guild + guild._patch(data); + // If the client was ready before and we had unavailable guilds, fetch them + if (client.ws.status === Status.READY && client.options.fetchAllMembers) { + await guild.members + .fetch() + .catch(err => client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`)); + } + } + } else { + // A new guild + data.shardID = shard.id; + guild = client.guilds.add(data); + if (client.ws.status === Status.READY) { + /** + * Emitted whenever the client joins a guild. + * @event Client#guildCreate + * @param {Guild} guild The created guild + */ + if (client.options.fetchAllMembers) { + await guild.members + .fetch() + .catch(err => client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`)); + } + client.emit(Events.GUILD_CREATE, guild); + } + } +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_DELETE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_DELETE.js new file mode 100644 index 0000000..27a3256 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildDelete.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js new file mode 100644 index 0000000..e23b671 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildEmojisUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js new file mode 100644 index 0000000..e90a72c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildIntegrationsUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js new file mode 100644 index 0000000..3ceb622 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -0,0 +1,22 @@ +'use strict'; + +const Collection = require('../../../util/Collection'); +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.cache.get(data.guild_id); + if (!guild) return; + const members = new Collection(); + + for (const member of data.members) members.set(member.user.id, guild.members.add(member)); + if (data.presences) { + for (const presence of data.presences) guild.presences.cache.add(Object.assign(presence, { guild })); + } + /** + * Emitted whenever a chunk of guild members is received (all members come from the same guild). + * @event Client#guildMembersChunk + * @param {Collection<Snowflake, GuildMember>} members The members in the chunk + * @param {Guild} guild The guild related to the member chunk + */ + client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js new file mode 100644 index 0000000..5128756 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -0,0 +1,19 @@ +'use strict'; + +const { Events, Status } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.cache.get(data.guild_id); + if (guild) { + guild.memberCount++; + const member = guild.members.add(data); + if (shard.status === Status.READY) { + /** + * Emitted whenever a user joins a guild. + * @event Client#guildMemberAdd + * @param {GuildMember} member The member that has joined a guild + */ + client.emit(Events.GUILD_MEMBER_ADD, member); + } + } +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js new file mode 100644 index 0000000..72432af --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet, shard) => { + client.actions.GuildMemberRemove.handle(packet.d, shard); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js new file mode 100644 index 0000000..92c9da6 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -0,0 +1,22 @@ +'use strict'; + +const { Status, Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.cache.get(data.guild_id); + if (guild) { + const member = guild.members.cache.get(data.user.id); + if (member) { + const old = member._update(data); + if (shard.status === Status.READY) { + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. + * @event Client#guildMemberUpdate + * @param {GuildMember} oldMember The member before the update + * @param {GuildMember} newMember The member after the update + */ + client.emit(Events.GUILD_MEMBER_UPDATE, old, member); + } + } + } +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_CREATE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_CREATE.js new file mode 100644 index 0000000..da9e7bc --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildRoleCreate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_DELETE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_DELETE.js new file mode 100644 index 0000000..cdc6353 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildRoleDelete.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js new file mode 100644 index 0000000..3a9b62e --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildRoleUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/GUILD_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/GUILD_UPDATE.js new file mode 100644 index 0000000..fd0012a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/GUILD_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.GuildUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/INVITE_CREATE.js b/node_modules/discord.js/src/client/websocket/handlers/INVITE_CREATE.js new file mode 100644 index 0000000..50a2e72 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/INVITE_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.InviteCreate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/INVITE_DELETE.js b/node_modules/discord.js/src/client/websocket/handlers/INVITE_DELETE.js new file mode 100644 index 0000000..5971852 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/INVITE_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.InviteDelete.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_CREATE.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_CREATE.js new file mode 100644 index 0000000..c9b79a8 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageCreate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_DELETE.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_DELETE.js new file mode 100644 index 0000000..85ae2bc --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageDelete.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js new file mode 100644 index 0000000..fbcf80f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageDeleteBulk.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js new file mode 100644 index 0000000..e219b4a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageReactionAdd.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js new file mode 100644 index 0000000..2980e69 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageReactionRemove.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js new file mode 100644 index 0000000..ead80f7 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageReactionRemoveAll.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js new file mode 100644 index 0000000..579444c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageReactionRemoveEmoji.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_UPDATE.js new file mode 100644 index 0000000..7428e90 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/MESSAGE_UPDATE.js @@ -0,0 +1,16 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { old, updated } = client.actions.MessageUpdate.handle(packet.d); + if (old && updated) { + /** + * Emitted whenever a message is updated - e.g. embed or content change. + * @event Client#messageUpdate + * @param {Message} oldMessage The message before the update + * @param {Message} newMessage The message after the update + */ + client.emit(Events.MESSAGE_UPDATE, old, updated); + } +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/PRESENCE_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/PRESENCE_UPDATE.js new file mode 100644 index 0000000..bde3629 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/PRESENCE_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.PresenceUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/READY.js b/node_modules/discord.js/src/client/websocket/handlers/READY.js new file mode 100644 index 0000000..c38b681 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/READY.js @@ -0,0 +1,21 @@ +'use strict'; + +let ClientUser; + +module.exports = (client, { d: data }, shard) => { + if (client.user) { + client.user._patch(data.user); + } else { + if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.users.cache.set(clientUser.id, clientUser); + } + + for (const guild of data.guilds) { + guild.shardID = shard.id; + client.guilds.add(guild); + } + + shard.checkReady(); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/RESUMED.js b/node_modules/discord.js/src/client/websocket/handlers/RESUMED.js new file mode 100644 index 0000000..5e5f403 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/RESUMED.js @@ -0,0 +1,14 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet, shard) => { + const replayed = shard.sequence - shard.closeSequence; + /** + * Emitted when a shard resumes successfully. + * @event Client#shardResume + * @param {number} id The shard ID that resumed + * @param {number} replayedEvents The amount of replayed events + */ + client.emit(Events.SHARD_RESUME, shard.id, replayed); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/TYPING_START.js b/node_modules/discord.js/src/client/websocket/handlers/TYPING_START.js new file mode 100644 index 0000000..86fb26b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/TYPING_START.js @@ -0,0 +1,49 @@ +'use strict'; + +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.cache.get(data.channel_id); + const user = client.users.cache.get(data.user_id); + const timestamp = new Date(data.timestamp * 1000); + + if (channel && user) { + if (channel.type === 'voice') { + client.emit(Events.WARN, `Discord sent a typing packet to a voice channel ${channel.id}`); + return; + } + + if (channel._typing.has(user.id)) { + const typing = channel._typing.get(user.id); + + typing.lastTimestamp = timestamp; + typing.elapsedTime = Date.now() - typing.since; + client.clearTimeout(typing.timeout); + typing.timeout = tooLate(channel, user); + } else { + const since = new Date(); + const lastTimestamp = new Date(); + channel._typing.set(user.id, { + user, + since, + lastTimestamp, + elapsedTime: Date.now() - since, + timeout: tooLate(channel, user), + }); + + /** + * Emitted whenever a user starts typing in a channel. + * @event Client#typingStart + * @param {Channel} channel The channel the user started typing in + * @param {User} user The user that started typing + */ + client.emit(Events.TYPING_START, channel, user); + } + } +}; + +function tooLate(channel, user) { + return channel.client.setTimeout(() => { + channel._typing.delete(user.id); + }, 10000); +} diff --git a/node_modules/discord.js/src/client/websocket/handlers/USER_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/USER_UPDATE.js new file mode 100644 index 0000000..a02bf58 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/USER_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.UserUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js new file mode 100644 index 0000000..f9cf534 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = (client, packet) => { + client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`); + client.voice.onVoiceServer(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/VOICE_STATE_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/VOICE_STATE_UPDATE.js new file mode 100644 index 0000000..dbff6ea --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/VOICE_STATE_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.VoiceStateUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/WEBHOOKS_UPDATE.js b/node_modules/discord.js/src/client/websocket/handlers/WEBHOOKS_UPDATE.js new file mode 100644 index 0000000..46cacee --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/WEBHOOKS_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.WebhooksUpdate.handle(packet.d); +}; diff --git a/node_modules/discord.js/src/client/websocket/handlers/index.js b/node_modules/discord.js/src/client/websocket/handlers/index.js new file mode 100644 index 0000000..d69c105 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/handlers/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const { WSEvents } = require('../../../util/Constants'); + +const handlers = {}; + +for (const name of Object.keys(WSEvents)) { + try { + handlers[name] = require(`./${name}.js`); + } catch {} // eslint-disable-line no-empty +} + +module.exports = handlers; |