diff options
Diffstat (limited to 'node_modules/discord.js/src/client')
111 files changed, 7997 insertions, 0 deletions
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..9ad7c95 --- /dev/null +++ b/node_modules/discord.js/src/client/Client.js @@ -0,0 +1,564 @@ +const EventEmitter = require('events'); +const Constants = require('../util/Constants'); +const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); +const RESTManager = require('./rest/RESTManager'); +const ClientDataManager = require('./ClientDataManager'); +const ClientManager = require('./ClientManager'); +const ClientDataResolver = require('./ClientDataResolver'); +const ClientVoiceManager = require('./voice/ClientVoiceManager'); +const WebSocketManager = require('./websocket/WebSocketManager'); +const ActionsManager = require('./actions/ActionsManager'); +const Collection = require('../util/Collection'); +const Presence = require('../structures/Presence').Presence; +const ShardClientUtil = require('../sharding/ShardClientUtil'); +const VoiceBroadcast = require('./voice/VoiceBroadcast'); + +/** + * The main hub for interacting with the Discord API, and the starting point for any bot. + * @extends {EventEmitter} + */ +class Client extends EventEmitter { + /** + * @param {ClientOptions} [options] Options for the client + */ + constructor(options = {}) { + super(); + + // Obtain shard details from environment + if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID); + if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT); + + /** + * The options the client was instantiated with + * @type {ClientOptions} + */ + this.options = Util.mergeDefault(Constants.DefaultOptions, options); + this._validateOptions(); + + /** + * The REST manager of the client + * @type {RESTManager} + * @private + */ + this.rest = new RESTManager(this); + + /** + * The data manager of the client + * @type {ClientDataManager} + * @private + */ + this.dataManager = new ClientDataManager(this); + + /** + * The manager of the client + * @type {ClientManager} + * @private + */ + this.manager = new ClientManager(this); + + /** + * The WebSocket manager of the client + * @type {WebSocketManager} + * @private + */ + this.ws = new WebSocketManager(this); + + /** + * The data resolver of the client + * @type {ClientDataResolver} + * @private + */ + this.resolver = new ClientDataResolver(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} + * @private + */ + this.voice = !this.browser ? new ClientVoiceManager(this) : null; + + /** + * The shard helpers for the client + * (only if the process was spawned as a child, such as from a {@link ShardingManager}) + * @type {?ShardClientUtil} + */ + this.shard = process.send ? ShardClientUtil.singleton(this) : null; + + /** + * All of the {@link User} objects that have been cached at any point, mapped by their IDs + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(); + + /** + * 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 {Collection<Snowflake, Guild>} + */ + this.guilds = new Collection(); + + /** + * 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, and all DM channels + * @type {Collection<Snowflake, Channel>} + */ + this.channels = new Collection(); + + /** + * Presences that have been received for the client user's friends, mapped by user IDs + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<Snowflake, Presence>} + * @deprecated + */ + this.presences = new Collection(); + + Object.defineProperty(this, 'token', { writable: true }); + if (!this.token && 'CLIENT_TOKEN' in process.env) { + /** + * Authorization token for the logged in user/bot + * <warn>This should be kept private at all times.</warn> + * @type {?string} + */ + this.token = process.env.CLIENT_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; + + /** + * Active voice broadcasts that have been created + * @type {VoiceBroadcast[]} + */ + this.broadcasts = []; + + /** + * Previous heartbeat pings of the websocket (most recent first, limited to three elements) + * @type {number[]} + */ + this.pings = []; + + /** + * Timeouts set by {@link Client#setTimeout} that are still active + * @type {Set<Timeout>} + * @private + */ + this._timeouts = new Set(); + + /** + * Intervals set by {@link Client#setInterval} that are still active + * @type {Set<Timeout>} + * @private + */ + this._intervals = new Set(); + + if (this.options.messageSweepInterval > 0) { + this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); + } + } + + /** + * Timestamp of the latest ping's start time + * @type {number} + * @private + */ + get _pingTimestamp() { + return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0; + } + + /** + * Current status of the client's connection to Discord + * @type {Status} + * @readonly + */ + get status() { + return this.ws.connection ? this.ws.connection.status : Constants.Status.IDLE; + } + + /** + * 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; + } + + /** + * Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property + * @type {number} + * @readonly + */ + get ping() { + return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; + } + + /** + * All active voice connections that have been established, mapped by guild ID + * @type {Collection<Snowflake, VoiceConnection>} + * @readonly + */ + get voiceConnections() { + if (this.browser) return new Collection(); + return this.voice.connections; + } + + /** + * All custom emojis that the client has access to, mapped by their IDs + * @type {Collection<Snowflake, Emoji>} + * @readonly + */ + get emojis() { + const emojis = new Collection(); + for (const guild of this.guilds.values()) { + for (const emoji of guild.emojis.values()) emojis.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; + } + + /** + * Whether the client is in a browser environment + * @type {boolean} + * @readonly + */ + get browser() { + return typeof window !== 'undefined'; + } + + /** + * Creates a voice broadcast. + * @returns {VoiceBroadcast} + */ + createVoiceBroadcast() { + const broadcast = new VoiceBroadcast(this); + this.broadcasts.push(broadcast); + return broadcast; + } + + /** + * Logs the client in, establishing a websocket connection to Discord. + * <info>Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever + * possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts. + * Bot accounts also have access to many features that user accounts cannot utilise. Automating a user account is + * considered a violation of Discord's ToS.</info> + * @param {string} token Token of the account to log in with + * @returns {Promise<string>} Token of the account used + * @example + * client.login('my token') + * .then(console.log) + * .catch(console.error); + */ + login(token = this.token) { + return this.rest.methods.login(token); + } + + /** + * Logs out, terminates the connection to Discord, and destroys the client. + * @returns {Promise} + */ + destroy() { + for (const t of this._timeouts) clearTimeout(t); + for (const i of this._intervals) clearInterval(i); + this._timeouts.clear(); + this._intervals.clear(); + return this.manager.destroy(); + } + + /** + * Requests a sync of guild data with Discord. + * <info>This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}.</info> + * <warn>This is only available when using a user account.</warn> + * @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] An array or collection of guilds to sync + * @deprecated + */ + syncGuilds(guilds = this.guilds) { + if (this.user.bot) return; + this.ws.send({ + op: 12, + d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id), + }); + } + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * <warn>This is only available when using a bot account.</warn> + * @param {Snowflake} id ID of the user + * @param {boolean} [cache=true] Whether to cache the new user object if it isn't already + * @returns {Promise<User>} + */ + fetchUser(id, cache = true) { + if (this.users.has(id)) return Promise.resolve(this.users.get(id)); + return this.rest.methods.getUser(id, cache); + } + + /** + * 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 = this.resolver.resolveInviteCode(invite); + return this.rest.methods.getInvite(code); + } + + /** + * 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.rest.methods.getWebhook(id, token); + } + + /** + * 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.rest.methods.fetchVoiceRegions(); + } + + /** + * 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 + */ + sweepMessages(lifetime = this.options.messageCacheLifetime) { + if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.'); + if (lifetime <= 0) { + this.emit('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.values()) { + if (!channel.messages) continue; + channels++; + + messages += channel.messages.sweep( + message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs + ); + } + + this.emit('debug', `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`); + return messages; + } + + /** + * Obtains the OAuth Application of the bot from Discord. + * <warn>Bots can only fetch their own profile.</warn> + * @param {Snowflake} [id='@me'] ID of application to fetch + * @returns {Promise<OAuth2Application>} + * @example + * client.fetchApplication() + * .then(application => console.log(`Obtained application with name: ${application.name}`)) + * .catch(console.error); + */ + fetchApplication(id = '@me') { + if (id !== '@me') process.emitWarning('fetchApplication: use "@me" as an argument', 'DeprecationWarning'); + return this.rest.methods.getApplication(id); + } + + /** + * Generates a link that can be used to invite the bot to a guild. + * <warn>This is only available when using a bot account.</warn> + * @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); + */ + generateInvite(permissions) { + permissions = Permissions.resolve(permissions); + return this.fetchApplication().then(application => + `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot` + ); + } + + /** + * 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 before executing (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); + } + + /** + * Adds a ping to {@link Client#pings}. + * @param {number} startTime Starting time of the ping + * @private + */ + _pong(startTime) { + this.pings.unshift(Date.now() - startTime); + if (this.pings.length > 3) this.pings.length = 3; + this.ws.lastHeartbeatAck = true; + } + + /** + * Adds/updates a friend's presence in {@link Client#presences}. + * @param {Snowflake} id ID of the user + * @param {Object} presence Raw presence object from Discord + * @private + */ + _setPresence(id, presence) { + if (this.presences.has(id)) { + this.presences.get(id).update(presence); + return; + } + this.presences.set(id, new Presence(presence, this)); + } + + /** + * 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) { // eslint-disable-line complexity + if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) { + throw new TypeError('The shardCount option must be a number.'); + } + if (typeof options.shardId !== 'number' || isNaN(options.shardId)) { + throw new TypeError('The shardId option must be a number.'); + } + if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.'); + if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.'); + if (options.shardId !== 0 && options.shardId >= options.shardCount) { + throw new RangeError('The shardId option must be less than shardCount.'); + } + if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { + throw new TypeError('The messageCacheMaxSize option must be a number.'); + } + if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) { + throw new TypeError('The messageCacheLifetime option must be a number.'); + } + if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) { + throw new TypeError('The messageSweepInterval option must be a number.'); + } + if (typeof options.fetchAllMembers !== 'boolean') { + throw new TypeError('The fetchAllMembers option must be a boolean.'); + } + if (typeof options.disableEveryone !== 'boolean') { + throw new TypeError('The disableEveryone option must be a boolean.'); + } + if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { + throw new TypeError('The restWsBridgeTimeout option must be a number.'); + } + if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.'); + if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { + throw new TypeError('The retryLimit options must be 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/ClientDataManager.js b/node_modules/discord.js/src/client/ClientDataManager.js new file mode 100644 index 0000000..4f0f2d7 --- /dev/null +++ b/node_modules/discord.js/src/client/ClientDataManager.js @@ -0,0 +1,149 @@ +const Constants = require('../util/Constants'); +const Util = require('../util/Util'); +const Guild = require('../structures/Guild'); +const User = require('../structures/User'); +const Emoji = require('../structures/Emoji'); +const GuildChannel = require('../structures/GuildChannel'); +const TextChannel = require('../structures/TextChannel'); +const VoiceChannel = require('../structures/VoiceChannel'); +const CategoryChannel = require('../structures/CategoryChannel'); +const NewsChannel = require('../structures/NewsChannel'); +const StoreChannel = require('../structures/StoreChannel'); +const DMChannel = require('../structures/DMChannel'); +const GroupDMChannel = require('../structures/GroupDMChannel'); + +class ClientDataManager { + constructor(client) { + this.client = client; + } + + get pastReady() { + return this.client.ws.connection.status === Constants.Status.READY; + } + + newGuild(data) { + const already = this.client.guilds.has(data.id); + const guild = new Guild(this.client, data); + this.client.guilds.set(guild.id, guild); + if (this.pastReady && !already) { + /** + * Emitted whenever the client joins a guild. + * @event Client#guildCreate + * @param {Guild} guild The created guild + */ + if (this.client.options.fetchAllMembers) { + guild.fetchMembers().then(() => { this.client.emit(Constants.Events.GUILD_CREATE, guild); }); + } else { + this.client.emit(Constants.Events.GUILD_CREATE, guild); + } + } + + return guild; + } + + newUser(data, cache = true) { + if (this.client.users.has(data.id)) return this.client.users.get(data.id); + const user = new User(this.client, data); + if (cache) this.client.users.set(user.id, user); + return user; + } + + newChannel(data, guild) { + const already = this.client.channels.has(data.id); + let channel; + if (data.type === Constants.ChannelTypes.DM) { + channel = new DMChannel(this.client, data); + } else if (data.type === Constants.ChannelTypes.GROUP_DM) { + channel = new GroupDMChannel(this.client, data); + } else { + guild = guild || this.client.guilds.get(data.guild_id); + if (already) { + channel = this.client.channels.get(data.id); + } else if (guild) { + switch (data.type) { + case Constants.ChannelTypes.TEXT: + channel = new TextChannel(guild, data); + break; + case Constants.ChannelTypes.VOICE: + channel = new VoiceChannel(guild, data); + break; + case Constants.ChannelTypes.CATEGORY: + channel = new CategoryChannel(guild, data); + break; + case Constants.ChannelTypes.NEWS: + channel = new NewsChannel(guild, data); + break; + case Constants.ChannelTypes.STORE: + channel = new StoreChannel(guild, data); + break; + } + + guild.channels.set(channel.id, channel); + } + } + + if (channel && !already) { + if (this.pastReady) this.client.emit(Constants.Events.CHANNEL_CREATE, channel); + this.client.channels.set(channel.id, channel); + return channel; + } else if (already) { + return channel; + } + + return null; + } + + newEmoji(data, guild) { + const already = guild.emojis.has(data.id); + if (data && !already) { + let emoji = new Emoji(guild, data); + this.client.emit(Constants.Events.GUILD_EMOJI_CREATE, emoji); + guild.emojis.set(emoji.id, emoji); + return emoji; + } else if (already) { + return guild.emojis.get(data.id); + } + + return null; + } + + killEmoji(emoji) { + if (!(emoji instanceof Emoji && emoji.guild)) return; + this.client.emit(Constants.Events.GUILD_EMOJI_DELETE, emoji); + emoji.guild.emojis.delete(emoji.id); + } + + killGuild(guild) { + const already = this.client.guilds.has(guild.id); + this.client.guilds.delete(guild.id); + if (already && this.pastReady) this.client.emit(Constants.Events.GUILD_DELETE, guild); + } + + killUser(user) { + this.client.users.delete(user.id); + } + + killChannel(channel) { + this.client.channels.delete(channel.id); + if (channel instanceof GuildChannel) channel.guild.channels.delete(channel.id); + } + + updateGuild(currentGuild, newData) { + const oldGuild = Util.cloneObject(currentGuild); + currentGuild.setup(newData); + if (this.pastReady) this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild); + } + + updateChannel(currentChannel, newData) { + currentChannel.setup(newData); + } + + updateEmoji(currentEmoji, newData) { + const oldEmoji = Util.cloneObject(currentEmoji); + currentEmoji.setup(newData); + this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji); + return currentEmoji; + } +} + +module.exports = ClientDataManager; diff --git a/node_modules/discord.js/src/client/ClientDataResolver.js b/node_modules/discord.js/src/client/ClientDataResolver.js new file mode 100644 index 0000000..50a7bb9 --- /dev/null +++ b/node_modules/discord.js/src/client/ClientDataResolver.js @@ -0,0 +1,376 @@ +const path = require('path'); +const fs = require('fs'); +const snekfetch = require('snekfetch'); + +const Constants = require('../util/Constants'); +const convertToBuffer = require('../util/Util').convertToBuffer; +const User = require('../structures/User'); +const Message = require('../structures/Message'); +const Guild = require('../structures/Guild'); +const Channel = require('../structures/Channel'); +const GuildMember = require('../structures/GuildMember'); +const Emoji = require('../structures/Emoji'); +const ReactionEmoji = require('../structures/ReactionEmoji'); +const Role = require('../structures/Role'); + +/** + * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g. + * extracting a User from a Message object. + * @private + */ +class ClientDataResolver { + /** + * @param {Client} client The client the resolver is for + */ + constructor(client) { + this.client = client; + } + + /** + * Data that resolves to give a User object. This can be: + * * A User object + * * A Snowflake + * * A Message object (resolves to the message author) + * * A Guild object (owner of the guild) + * * A GuildMember object + * @typedef {User|Snowflake|Message|Guild|GuildMember} UserResolvable + */ + + /** + * Resolves a UserResolvable to a User object. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?User} + */ + resolveUser(user) { + if (user instanceof User) return user; + if (typeof user === 'string') return this.client.users.get(user) || null; + if (user instanceof GuildMember) return user.user; + if (user instanceof Message) return user.author; + if (user instanceof Guild) return this.resolveUser(user.ownerID); + return null; + } + + /** + * Resolves a UserResolvable to a user ID string. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?Snowflake} + */ + resolveUserID(user) { + if (user instanceof User || user instanceof GuildMember) return user.id; + if (typeof user === 'string') return user || null; + if (user instanceof Message) return user.author.id; + if (user instanceof Guild) return user.ownerID; + return null; + } + + /** + * Data that resolves to give a Guild object. This can be: + * * A Guild object + * * A Snowflake + * @typedef {Guild|Snowflake} GuildResolvable + */ + + /** + * Resolves a GuildResolvable to a Guild object. + * @param {GuildResolvable} guild The GuildResolvable to identify + * @returns {?Guild} + */ + resolveGuild(guild) { + if (guild instanceof Guild) return guild; + if (typeof guild === 'string') return this.client.guilds.get(guild) || null; + return null; + } + + /** + * Data that resolves to give a GuildMember object. This can be: + * * A GuildMember object + * * A User object + * @typedef {GuildMember|User} GuildMemberResolvable + */ + + /** + * Resolves a GuildMemberResolvable to a GuildMember object. + * @param {GuildResolvable} guild The guild that the member is part of + * @param {UserResolvable} user The user that is part of the guild + * @returns {?GuildMember} + */ + resolveGuildMember(guild, user) { + if (user instanceof GuildMember) return user; + guild = this.resolveGuild(guild); + user = this.resolveUser(user); + if (!guild || !user) return null; + return guild.members.get(user.id) || null; + } + + /** + * Data that can be resolved to a Role object. This can be: + * * A Role + * * A Snowflake + * @typedef {Role|Snowflake} RoleResolvable + */ + + /** + * Resolves a RoleResolvable to a Role object. + * @param {GuildResolvable} guild The guild that this role is part of + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Role} + */ + resolveRole(guild, role) { + if (role instanceof Role) return role; + guild = this.resolveGuild(guild); + if (!guild) return null; + if (typeof role === 'string') return guild.roles.get(role); + return null; + } + + /** + * Data that can be resolved to give a Channel object. This can be: + * * A Channel object + * * A Message object (the channel the message was sent in) + * * A Guild object (the #general channel) + * * A Snowflake + * @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable + */ + + /** + * Resolves a ChannelResolvable to a Channel object. + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?Channel} + */ + resolveChannel(channel) { + if (channel instanceof Channel) return channel; + if (typeof channel === 'string') return this.client.channels.get(channel) || null; + if (channel instanceof Message) return channel.channel; + if (channel instanceof Guild) return channel.channels.get(channel.id) || null; + return null; + } + + /** + * Resolves a ChannelResolvable to a channel ID. + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?Snowflake} + */ + resolveChannelID(channel) { + if (channel instanceof Channel) return channel.id; + if (typeof channel === 'string') return channel; + if (channel instanceof Message) return channel.channel.id; + if (channel instanceof Guild) return channel.defaultChannel.id; + return null; + } + + /** + * Data that can be resolved to give an invite code. This can be: + * * An invite code + * * An invite URL + * @typedef {string} InviteResolvable + */ + + /** + * Resolves InviteResolvable to an invite code. + * @param {InviteResolvable} data The invite resolvable to resolve + * @returns {string} + */ + resolveInviteCode(data) { + const inviteRegex = /discord(?:app\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/i; + const match = inviteRegex.exec(data); + if (match && match[1]) return match[1]; + return data; + } + + /** + * Data that can be resolved to give a string. This can be: + * * A string + * * An array (joined with a new line delimiter to give a string) + * * Any value + * @typedef {string|Array|*} StringResolvable + */ + + /** + * Resolves a StringResolvable to a string. + * @param {StringResolvable} data The string resolvable to resolve + * @returns {string} + */ + resolveString(data) { + if (typeof data === 'string') return data; + if (data instanceof Array) return data.join('\n'); + return String(data); + } + + + /** + * Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image. + * @param {BufferResolvable|Base64Resolvable} image The image to be resolved + * @returns {Promise<?string>} + */ + resolveImage(image) { + if (!image) return Promise.resolve(null); + if (typeof image === 'string' && image.startsWith('data:')) { + return Promise.resolve(image); + } + return this.resolveFile(image).then(this.resolveBase64); + } + + /** + * Data that resolves to give a Base64 string, typically for image uploading. This can be: + * * A Buffer + * * A base64 string + * @typedef {Buffer|string} Base64Resolvable + */ + + /** + * Resolves a Base64Resolvable to a Base 64 image. + * @param {Base64Resolvable} data The base 64 resolvable you want to resolve + * @returns {?string} + */ + resolveBase64(data) { + if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`; + return data; + } + + /** + * Data that can be resolved to give a Buffer. This can be: + * * A Buffer + * * The path to a local file + * * A URL + * * A Stream + * @typedef {string|Buffer} BufferResolvable + */ + + /** + * @external Stream + * @see {@link https://nodejs.org/api/stream.html} + */ + + /** + * Resolves a BufferResolvable to a Buffer. + * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve + * @returns {Promise<Buffer>} + */ + resolveFile(resource) { + if (resource instanceof Buffer) return Promise.resolve(resource); + if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource)); + + if (typeof resource === 'string') { + if (/^https?:\/\//.test(resource)) { + return snekfetch.get(resource).then(res => res.body instanceof Buffer ? res.body : Buffer.from(res.text)); + } + return new Promise((resolve, reject) => { + const file = path.resolve(resource); + fs.stat(file, (err, stats) => { + if (err) return reject(err); + if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`)); + fs.readFile(file, (err2, data) => { + if (err2) reject(err2); + else resolve(data); + }); + return null; + }); + }); + } else if (resource && resource.pipe && typeof resource.pipe === 'function') { + return new Promise((resolve, reject) => { + const buffers = []; + resource.once('error', reject); + resource.on('data', data => buffers.push(data)); + resource.once('end', () => resolve(Buffer.concat(buffers))); + }); + } + + return Promise.reject(new TypeError('The resource must be a string or Buffer.')); + } + + /** + * Data that can be resolved to give an emoji identifier. This can be: + * * The unicode representation of an emoji + * * A custom emoji ID + * * An Emoji object + * * A ReactionEmoji object + * @typedef {string|Emoji|ReactionEmoji} EmojiIdentifierResolvable + */ + + /** + * Resolves an EmojiResolvable to an emoji identifier. + * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve + * @returns {?string} + */ + resolveEmojiIdentifier(emoji) { + if (emoji instanceof Emoji || emoji instanceof ReactionEmoji) return emoji.identifier; + if (typeof emoji === 'string') { + if (this.client.emojis.has(emoji)) return this.client.emojis.get(emoji).identifier; + else if (!emoji.includes('%')) return encodeURIComponent(emoji); + else return emoji; + } + return null; + } + + /** + * Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following + * ``` + * [ + * 'DEFAULT', + * 'WHITE', + * 'AQUA', + * 'GREEN', + * 'BLUE', + * 'PURPLE', + * 'LUMINOUS_VIVID_PINK', + * 'GOLD', + * 'ORANGE', + * 'RED', + * 'GREY', + * 'DARKER_GREY', + * 'NAVY', + * 'DARK_AQUA', + * 'DARK_GREEN', + * 'DARK_BLUE', + * 'DARK_PURPLE', + * 'DARK_VIVID_PINK', + * 'DARK_GOLD', + * 'DARK_ORANGE', + * 'DARK_RED', + * 'DARK_GREY', + * 'LIGHT_GREY', + * 'DARK_NAVY', + * 'RANDOM', + * ] + * ``` + * or something like + * ``` + * [255, 0, 255] + * ``` + * for purple + * @typedef {string|number|Array} ColorResolvable + */ + + /** + * Resolves a ColorResolvable into a color number. + * @param {ColorResolvable} color Color to resolve + * @returns {number} A color + */ + static resolveColor(color) { + if (typeof color === 'string') { + if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1)); + if (color === 'DEFAULT') return 0; + color = Constants.Colors[color] || parseInt(color.replace('#', ''), 16); + } else if (color instanceof Array) { + color = (color[0] << 16) + (color[1] << 8) + color[2]; + } + + if (color < 0 || color > 0xFFFFFF) { + throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).'); + } else if (color && isNaN(color)) { + throw new TypeError('Unable to convert color to a number.'); + } + + return color; + } + + /** + * @param {ColorResolvable} color Color to resolve + * @returns {number} A color + */ + resolveColor(color) { + return this.constructor.resolveColor(color); + } +} + +module.exports = ClientDataResolver; diff --git a/node_modules/discord.js/src/client/ClientManager.js b/node_modules/discord.js/src/client/ClientManager.js new file mode 100644 index 0000000..0f2480c --- /dev/null +++ b/node_modules/discord.js/src/client/ClientManager.js @@ -0,0 +1,74 @@ +const Constants = require('../util/Constants'); +const WebSocketConnection = require('./websocket/WebSocketConnection'); + +/** + * Manages the state and background tasks of the client. + * @private + */ +class ClientManager { + constructor(client) { + /** + * The client that instantiated this Manager + * @type {Client} + */ + this.client = client; + + /** + * The heartbeat interval + * @type {?number} + */ + this.heartbeatInterval = null; + } + + /** + * The status of the client + * @type {number} + */ + get status() { + return this.connection ? this.connection.status : Constants.Status.IDLE; + } + + /** + * Connects the client to the WebSocket. + * @param {string} token The authorization token + * @param {Function} resolve Function to run when connection is successful + * @param {Function} reject Function to run when connection fails + */ + connectToWebSocket(token, resolve, reject) { + this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`); + this.client.token = token; + const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300); + this.client.rest.methods.getGateway().then(res => { + const protocolVersion = Constants.DefaultOptions.ws.version; + const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`; + this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`); + this.client.ws.connect(gateway); + this.client.ws.connection.once('error', reject); + this.client.ws.connection.once('close', event => { + if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN)); + if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD)); + if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED)); + }); + this.client.once(Constants.Events.READY, () => { + resolve(token); + this.client.clearTimeout(timeout); + }); + }, reject); + } + + destroy() { + this.client.ws.destroy(); + this.client.rest.destroy(); + if (!this.client.user) return Promise.resolve(); + if (this.client.user.bot) { + this.client.token = null; + return Promise.resolve(); + } else { + return this.client.rest.methods.logout().then(() => { + this.client.token = null; + }); + } + } +} + +module.exports = ClientManager; 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..99291b5 --- /dev/null +++ b/node_modules/discord.js/src/client/WebhookClient.js @@ -0,0 +1,118 @@ +const Webhook = require('../structures/Webhook'); +const RESTManager = require('./rest/RESTManager'); +const ClientDataResolver = require('./ClientDataResolver'); +const Constants = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * The webhook client. + * @extends {Webhook} + */ +class WebhookClient extends Webhook { + /** + * @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.sendMessage('This will send a message').catch(console.error); + */ + constructor(id, token, options) { + super(null, id, token); + + /** + * The options the client was instantiated with + * @type {ClientOptions} + */ + this.options = Util.mergeDefault(Constants.DefaultOptions, options); + + /** + * The REST manager of the client + * @type {RESTManager} + * @private + */ + this.rest = new RESTManager(this); + + /** + * The data resolver of the client + * @type {ClientDataResolver} + * @private + */ + this.resolver = new ClientDataResolver(this); + + /** + * Timeouts set by {@link WebhookClient#setTimeout} that are still active + * @type {Set<Timeout>} + * @private + */ + this._timeouts = new Set(); + + /** + * Intervals set by {@link WebhookClient#setInterval} that are still active + * @type {Set<Timeout>} + * @private + */ + this._intervals = new Set(); + } + + /** + * 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 before executing (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); + } + + + /** + * Destroys the client. + */ + destroy() { + for (const t of this._timeouts) clearTimeout(t); + for (const i of this._intervals) clearInterval(i); + this._timeouts.clear(); + this._intervals.clear(); + } +} + +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..8fdadc9 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/Action.js @@ -0,0 +1,23 @@ +/* + +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; + } +} + +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..f9739cc --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ActionsManager.js @@ -0,0 +1,43 @@ +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('./MessageReactionRemoveEmoji')); + this.register(require('./MessageReactionRemoveAll')); + this.register(require('./ChannelCreate')); + this.register(require('./ChannelDelete')); + this.register(require('./ChannelUpdate')); + this.register(require('./GuildDelete')); + this.register(require('./GuildUpdate')); + this.register(require('./GuildMemberGet')); + this.register(require('./GuildMemberRemove')); + this.register(require('./GuildBanRemove')); + this.register(require('./GuildRoleCreate')); + this.register(require('./GuildRoleDelete')); + this.register(require('./GuildRoleUpdate')); + this.register(require('./InviteCreate')); + this.register(require('./InviteDelete')); + this.register(require('./UserGet')); + this.register(require('./UserUpdate')); + this.register(require('./UserNoteUpdate')); + this.register(require('./GuildSync')); + this.register(require('./GuildEmojiCreate')); + this.register(require('./GuildEmojiDelete')); + this.register(require('./GuildEmojiUpdate')); + this.register(require('./GuildEmojisUpdate')); + this.register(require('./GuildRolesPositionUpdate')); + this.register(require('./GuildChannelsPositionUpdate')); + } + + 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..83b1aa0 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelCreate.js @@ -0,0 +1,11 @@ +const Action = require('./Action'); + +class ChannelCreateAction extends Action { + handle(data) { + const client = this.client; + const channel = client.dataManager.newChannel(data); + 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..6def629 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelDelete.js @@ -0,0 +1,38 @@ +const Action = require('./Action'); +const DMChannel = require('../../structures/DMChannel'); + +class ChannelDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + + let channel = client.channels.get(data.id); + if (channel) { + client.dataManager.killChannel(channel); + this.deleted.set(channel.id, channel); + this.scheduleForDeletion(channel.id); + } else { + channel = this.deleted.get(data.id) || null; + } + if (channel) { + if (channel.messages && !(channel instanceof DMChannel)) { + for (const message of channel.messages.values()) { + message.deleted = true; + } + } + channel.deleted = true; + } + + return { channel }; + } + + scheduleForDeletion(id) { + this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout); + } +} + +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..ba1c4ef --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelUpdate.js @@ -0,0 +1,74 @@ +const Action = require('./Action'); +const TextChannel = require('../../structures/TextChannel'); +const VoiceChannel = require('../../structures/VoiceChannel'); +const CategoryChannel = require('../../structures/CategoryChannel'); +const NewsChannel = require('../../structures/NewsChannel'); +const StoreChannel = require('../../structures/StoreChannel'); +const Constants = require('../../util/Constants'); +const ChannelTypes = Constants.ChannelTypes; +const Util = require('../../util/Util'); + +class ChannelUpdateAction extends Action { + handle(data) { + const client = this.client; + + let channel = client.channels.get(data.id); + if (channel) { + const oldChannel = Util.cloneObject(channel); + + // If the channel is changing types, we need to follow a different process + if (ChannelTypes[channel.type.toUpperCase()] !== data.type) { + // Determine which channel class we're changing to + let channelClass; + switch (data.type) { + case ChannelTypes.TEXT: + channelClass = TextChannel; + break; + case ChannelTypes.VOICE: + channelClass = VoiceChannel; + break; + case ChannelTypes.CATEGORY: + channelClass = CategoryChannel; + break; + case ChannelTypes.NEWS: + channelClass = NewsChannel; + break; + case ChannelTypes.STORE: + channelClass = StoreChannel; + break; + } + + // Create the new channel instance and copy over cached data + const newChannel = new channelClass(channel.guild, data); + if (channel.messages && newChannel.messages) { + for (const [id, message] of channel.messages) newChannel.messages.set(id, message); + } + + channel = newChannel; + this.client.channels.set(channel.id, channel); + } else { + channel.setup(data); + } + + client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel); + return { + old: oldChannel, + updated: channel, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * Emitted whenever a channel is updated - e.g. name change, topic change. + * @event Client#channelUpdate + * @param {Channel} oldChannel The channel before the update + * @param {Channel} newChannel The channel after the update + */ + +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..0276a52 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildBanRemove.js @@ -0,0 +1,13 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class GuildBanRemove extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + const user = client.dataManager.newUser(data.user); + if (guild && user) client.emit(Constants.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..ea184ef --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildChannelsPositionUpdate.js @@ -0,0 +1,19 @@ +const Action = require('./Action'); + +class GuildChannelsPositionUpdate extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + for (const partialChannel of data.channels) { + const channel = guild.channels.get(partialChannel.id); + if (channel) channel.position = 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..8222323 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildDelete.js @@ -0,0 +1,57 @@ +const Action = require('./Action'); +const Constants = 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.get(data.id); + if (guild) { + for (const channel of guild.channels.values()) { + if (channel.type === 'text') channel.stopTyping(true); + } + + if (guild.available && data.unavailable) { + // Guild is unavailable + guild.available = false; + client.emit(Constants.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.values()) this.client.channels.delete(channel.id); + if (guild.voiceConnection) guild.voiceConnection.disconnect(); + + // Delete guild + client.guilds.delete(guild.id); + this.deleted.set(guild.id, guild); + this.scheduleForDeletion(guild.id); + } else { + guild = this.deleted.get(data.id) || null; + } + if (guild) guild.deleted = true; + + return { guild }; + } + + scheduleForDeletion(id) { + this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout); + } +} + +/** + * Emitted whenever a guild becomes unavailable, likely due to a server outage. + * @event Client#guildUnavailable + * @param {Guild} guild The guild that has become unavailable + */ + +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..79f55ba --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js @@ -0,0 +1,17 @@ +const Action = require('./Action'); + +class GuildEmojiCreateAction extends Action { + handle(guild, createdEmoji) { + const client = this.client; + const emoji = client.dataManager.newEmoji(createdEmoji, guild); + return { emoji }; + } +} + +/** + * Emitted whenever a custom emoji is created in a guild. + * @event Client#emojiCreate + * @param {Emoji} emoji The emoji that was created + */ + +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..044679c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiDelete.js @@ -0,0 +1,18 @@ +const Action = require('./Action'); + +class GuildEmojiDeleteAction extends Action { + handle(emoji) { + const client = this.client; + client.dataManager.killEmoji(emoji); + emoji.deleted = true; + return { emoji }; + } +} + +/** + * Emitted whenever a custom guild emoji is deleted. + * @event Client#emojiDelete + * @param {Emoji} emoji The emoji that was deleted + */ + +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..ff6ae65 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js @@ -0,0 +1,17 @@ +const Action = require('./Action'); + +class GuildEmojiUpdateAction extends Action { + handle(oldEmoji, newEmoji) { + const emoji = this.client.dataManager.updateEmoji(oldEmoji, newEmoji); + return { emoji }; + } +} + +/** + * Emitted whenever a custom guild emoji is updated. + * @event Client#emojiUpdate + * @param {Emoji} oldEmoji The old emoji + * @param {Emoji} newEmoji The new emoji + */ + +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..8656a34 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojisUpdate.js @@ -0,0 +1,38 @@ +const Action = require('./Action'); + +function mappify(iterable) { + const map = new Map(); + for (const x of iterable) map.set(...x); + return map; +} + +class GuildEmojisUpdateAction extends Action { + handle(data) { + const guild = this.client.guilds.get(data.guild_id); + if (!guild || !guild.emojis) return; + + const deletions = mappify(guild.emojis.entries()); + + for (const emoji of data.emojis) { + // Determine type of emoji event + const cachedEmoji = guild.emojis.get(emoji.id); + if (cachedEmoji) { + deletions.delete(emoji.id); + if (!cachedEmoji.equals(emoji, true)) { + // 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/GuildMemberGet.js b/node_modules/discord.js/src/client/actions/GuildMemberGet.js new file mode 100644 index 0000000..ecb7a8f --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildMemberGet.js @@ -0,0 +1,10 @@ +const Action = require('./Action'); + +class GuildMemberGetAction extends Action { + handle(guild, data) { + const member = guild._addMember(data, false); + return { member }; + } +} + +module.exports = GuildMemberGetAction; 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..6682b63 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildMemberRemove.js @@ -0,0 +1,41 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class GuildMemberRemoveAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + let member = null; + if (guild) { + member = guild.members.get(data.user.id); + guild.memberCount--; + if (member) { + guild._removeMember(member); + this.deleted.set(guild.id + data.user.id, member); + if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, member); + this.scheduleForDeletion(guild.id, data.user.id); + } else { + member = this.deleted.get(guild.id + data.user.id) || null; + } + if (member) member.deleted = true; + } + return { guild, member }; + } + + scheduleForDeletion(guildID, userID) { + this.client.setTimeout(() => this.deleted.delete(guildID + userID), this.client.options.restWsBridgeTimeout); + } +} + +/** + * 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 + */ + +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..b4f320f --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleCreate.js @@ -0,0 +1,26 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Role = require('../../structures/Role'); + +class GuildRoleCreate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + let role; + if (guild) { + const already = guild.roles.has(data.role.id); + role = new Role(guild, data.role); + guild.roles.set(role.id, role); + if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, role); + } + return { role }; + } +} + +/** + * Emitted whenever a role is created. + * @event Client#roleCreate + * @param {Role} role The role that was created + */ + +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..deafc85 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleDelete.js @@ -0,0 +1,42 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class GuildRoleDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + let role; + + if (guild) { + role = guild.roles.get(data.role_id); + if (role) { + guild.roles.delete(data.role_id); + this.deleted.set(guild.id + data.role_id, role); + this.scheduleForDeletion(guild.id, data.role_id); + client.emit(Constants.Events.GUILD_ROLE_DELETE, role); + } else { + role = this.deleted.get(guild.id + data.role_id) || null; + } + if (role) role.deleted = true; + } + + return { role }; + } + + scheduleForDeletion(guildID, roleID) { + this.client.setTimeout(() => this.deleted.delete(guildID + roleID), this.client.options.restWsBridgeTimeout); + } +} + +/** + * Emitted whenever a guild role is deleted. + * @event Client#roleDelete + * @param {Role} role The role that was deleted + */ + +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..7d127ab --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleUpdate.js @@ -0,0 +1,41 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Util = require('../../util/Util'); + +class GuildRoleUpdateAction extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + + if (guild) { + const roleData = data.role; + let oldRole = null; + + const role = guild.roles.get(roleData.id); + if (role) { + oldRole = Util.cloneObject(role); + role.setup(data.role); + client.emit(Constants.Events.GUILD_ROLE_UPDATE, oldRole, role); + } + + return { + old: oldRole, + updated: role, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * 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 + */ + +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..9094965 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js @@ -0,0 +1,19 @@ +const Action = require('./Action'); + +class GuildRolesPositionUpdate extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + for (const partialRole of data.roles) { + const role = guild.roles.get(partialRole.id); + if (role) role.position = partialRole.position; + } + } + + return { guild }; + } +} + +module.exports = GuildRolesPositionUpdate; diff --git a/node_modules/discord.js/src/client/actions/GuildSync.js b/node_modules/discord.js/src/client/actions/GuildSync.js new file mode 100644 index 0000000..3d3a47b --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildSync.js @@ -0,0 +1,29 @@ +const Action = require('./Action'); + +class GuildSync extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.id); + if (guild) { + if (data.presences) { + for (const presence of data.presences) guild._setPresence(presence.user.id, presence); + } + + if (data.members) { + for (const syncMember of data.members) { + const member = guild.members.get(syncMember.user.id); + if (member) { + guild._updateMember(member, syncMember); + } else { + guild._addMember(syncMember, false); + } + } + } + + if ('large' in data) guild.large = data.large; + } + } +} + +module.exports = GuildSync; 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..6806d1c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildUpdate.js @@ -0,0 +1,34 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Util = require('../../util/Util'); + +class GuildUpdateAction extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.id); + if (guild) { + const oldGuild = Util.cloneObject(guild); + guild.setup(data); + client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild); + return { + old: oldGuild, + updated: guild, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * 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 + */ + +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..36e8cf1 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/InviteCreate.js @@ -0,0 +1,29 @@ +'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 guild = client.guilds.get(data.guild_id); + const channel = client.channels.get(data.channel_id); + if (guild && channel) { + const inviteData = Object.assign(data, { guild, channel }); + 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 }; + } + return { invite: null }; + } +} + +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..a25eb88 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/InviteDelete.js @@ -0,0 +1,25 @@ +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 guild = client.guilds.get(data.guild_id); + const channel = client.channels.get(data.channel_id); + if (guild && channel) { + const inviteData = Object.assign(data, { guild, channel }); + 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); + } + } +} + +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..532f80c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageCreate.js @@ -0,0 +1,53 @@ +const Action = require('./Action'); +const Message = require('../../structures/Message'); + +class MessageCreateAction extends Action { + handle(data) { + const client = this.client; + + const channel = client.channels.get((data instanceof Array ? data[0] : data).channel_id); + const user = client.users.get((data instanceof Array ? data[0] : data).author.id); + if (channel) { + const member = channel.guild ? channel.guild.member(user) : null; + if (data instanceof Array) { + const messages = new Array(data.length); + for (let i = 0; i < data.length; i++) { + messages[i] = channel._cacheMessage(new Message(channel, data[i], client)); + } + const lastMessage = messages[messages.length - 1]; + channel.lastMessageID = lastMessage.id; + if (user) { + user.lastMessageID = lastMessage.id; + user.lastMessage = lastMessage; + } + if (member) { + member.lastMessageID = lastMessage.id; + member.lastMessage = lastMessage; + } + return { + messages, + }; + } else { + const message = channel._cacheMessage(new Message(channel, data, client)); + channel.lastMessageID = data.id; + if (user) { + user.lastMessageID = data.id; + user.lastMessage = message; + } + if (member) { + member.lastMessageID = data.id; + member.lastMessage = message; + } + return { + message, + }; + } + } + + return { + message: null, + }; + } +} + +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..9dc0828 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDelete.js @@ -0,0 +1,35 @@ +const Action = require('./Action'); + +class MessageDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + const channel = client.channels.get(data.channel_id); + let message; + + if (channel) { + message = channel.messages.get(data.id); + if (message) { + channel.messages.delete(message.id); + this.deleted.set(channel.id + message.id, message); + this.scheduleForDeletion(channel.id, message.id); + } else { + message = this.deleted.get(channel.id + data.id) || null; + } + if (message) message.deleted = true; + } + + return { message }; + } + + scheduleForDeletion(channelID, messageID) { + this.client.setTimeout(() => this.deleted.delete(channelID + messageID), + this.client.options.restWsBridgeTimeout); + } +} + +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..7ee2455 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js @@ -0,0 +1,26 @@ +const Action = require('./Action'); +const Collection = require('../../util/Collection'); +const Constants = require('../../util/Constants'); + +class MessageDeleteBulkAction extends Action { + handle(data) { + const messages = new Collection(); + const channel = this.client.channels.get(data.channel_id); + + if (channel) { + for (const id of data.ids) { + const message = channel.messages.get(id); + if (message) { + message.deleted = true; + messages.set(message.id, message); + channel.messages.delete(id); + } + } + } + + if (messages.size > 0) this.client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages); + return { messages }; + } +} + +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..d962953 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionAdd.js @@ -0,0 +1,37 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +/* +{ user_id: 'id', + message_id: 'id', + emoji: { name: '�', id: null }, + channel_id: 'id' } } +*/ + +class MessageReactionAdd extends Action { + handle(data) { + const user = this.client.users.get(data.user_id); + if (!user) return false; + // Verify channel + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + // Verify message + const message = channel.messages.get(data.message_id); + if (!message) return false; + if (!data.emoji) return false; + // Verify reaction + const reaction = message._addReaction(data.emoji, user); + if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_ADD, reaction, user); + + return { message, reaction, 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 emoji or reaction emoji + */ + +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..2403c3b --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemove.js @@ -0,0 +1,37 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +/* +{ user_id: 'id', + message_id: 'id', + emoji: { name: '�', id: null }, + channel_id: 'id' } } +*/ + +class MessageReactionRemove extends Action { + handle(data) { + const user = this.client.users.get(data.user_id); + if (!user) return false; + // Verify channel + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + // Verify message + const message = channel.messages.get(data.message_id); + if (!message) return false; + if (!data.emoji) return false; + // Verify reaction + const reaction = message._removeReaction(data.emoji, user); + if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE, reaction, user); + + return { message, reaction, 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 + */ + +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..e365646 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemoveAll.js @@ -0,0 +1,25 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class MessageReactionRemoveAll extends Action { + handle(data) { + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + + const message = channel.messages.get(data.message_id); + if (!message) return false; + + message._clearReactions(); + this.client.emit(Constants.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..12b4445 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemoveEmoji.js @@ -0,0 +1,27 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class MessageReactionRemoveEmoji extends Action { + handle(data) { + // Verify channel + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + // Verify message + const message = channel.messages.get(data.message_id); + if (!message) return false; + if (!data.emoji) return false; + // Verify reaction + const reaction = message._removeReaction(data.emoji); + if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction); + + return { message, reaction }; + } +} + +/** + * Emitted whenever a reaction emoji is removed from a cached message. + * @event Client#messageReactionRemoveEmoji + * @param {MessageReaction} messageReaction The reaction object + */ + +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..4e0392d --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageUpdate.js @@ -0,0 +1,40 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class MessageUpdateAction extends Action { + handle(data) { + const client = this.client; + + const channel = client.channels.get(data.channel_id); + if (channel) { + const message = channel.messages.get(data.id); + if (message) { + message.patch(data); + client.emit(Constants.Events.MESSAGE_UPDATE, message._edits[0], message); + return { + old: message._edits[0], + updated: message, + }; + } + + return { + old: message, + updated: message, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * 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 + */ + +module.exports = MessageUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/UserGet.js b/node_modules/discord.js/src/client/actions/UserGet.js new file mode 100644 index 0000000..0ee85ae --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserGet.js @@ -0,0 +1,11 @@ +const Action = require('./Action'); + +class UserGetAction extends Action { + handle(data) { + const client = this.client; + const user = client.dataManager.newUser(data); + return { user }; + } +} + +module.exports = UserGetAction; diff --git a/node_modules/discord.js/src/client/actions/UserNoteUpdate.js b/node_modules/discord.js/src/client/actions/UserNoteUpdate.js new file mode 100644 index 0000000..4c2cc21 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserNoteUpdate.js @@ -0,0 +1,30 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class UserNoteUpdateAction extends Action { + handle(data) { + const client = this.client; + + const oldNote = client.user.notes.get(data.id); + const note = data.note.length ? data.note : null; + + client.user.notes.set(data.id, note); + + client.emit(Constants.Events.USER_NOTE_UPDATE, data.id, oldNote, note); + + return { + old: oldNote, + updated: note, + }; + } +} + +/** + * Emitted whenever a note is updated. + * @event Client#userNoteUpdate + * @param {User} user The user the note belongs to + * @param {string} oldNote The note content before the update + * @param {string} newNote The note content after the update + */ + +module.exports = UserNoteUpdateAction; 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..6b917d1 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserUpdate.js @@ -0,0 +1,33 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Util = require('../../util/Util'); + +class UserUpdateAction extends Action { + handle(data) { + const client = this.client; + + if (client.user) { + if (client.user.equals(data)) { + return { + old: client.user, + updated: client.user, + }; + } + + const oldUser = Util.cloneObject(client.user); + client.user.patch(data); + client.emit(Constants.Events.USER_UPDATE, oldUser, client.user); + return { + old: oldUser, + updated: client.user, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +module.exports = UserUpdateAction; diff --git a/node_modules/discord.js/src/client/rest/APIRequest.js b/node_modules/discord.js/src/client/rest/APIRequest.js new file mode 100644 index 0000000..3492fa7 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/APIRequest.js @@ -0,0 +1,52 @@ +const snekfetch = require('snekfetch'); +const Constants = require('../../util/Constants'); + +class APIRequest { + constructor(rest, method, path, auth, data, files, reason) { + this.rest = rest; + this.client = rest.client; + this.method = method; + this.path = path.toString(); + this.auth = auth; + this.data = data; + this.files = files; + this.route = this.getRoute(this.path); + this.reason = reason; + } + + getRoute(url) { + let route = url.split('?')[0]; + if (route.includes('/channels/') || route.includes('/guilds/')) { + const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/'); + const majorID = route.substring(startInd).split('/')[2]; + route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID); + } + return route; + } + + getAuth() { + if (this.client.token && this.client.user && this.client.user.bot) { + return `Bot ${this.client.token}`; + } else if (this.client.token) { + return this.client.token; + } + throw new Error(Constants.Errors.NO_TOKEN); + } + + gen() { + const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`; + const request = snekfetch[this.method](`${API}${this.path}`); + if (this.auth) request.set('Authorization', this.getAuth()); + if (this.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.reason)); + if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent); + if (this.files) { + for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name); + if (typeof this.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.data)); + } else if (this.data) { + request.send(this.data); + } + return request; + } +} + +module.exports = APIRequest; diff --git a/node_modules/discord.js/src/client/rest/DiscordAPIError.js b/node_modules/discord.js/src/client/rest/DiscordAPIError.js new file mode 100644 index 0000000..f5f8336 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/DiscordAPIError.js @@ -0,0 +1,60 @@ +/** + * Represents an error from the Discord API. + * @extends Error + */ +class DiscordAPIError extends Error { + constructor(path, error, method) { + super(); + const flattened = this.constructor.flattenErrors(error.errors || error).join('\n'); + this.name = 'DiscordAPIError'; + this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened; + + /** + * The path of the request relative to the HTTP endpoint + * @type {string} + */ + this.path = path; + + /** + * HTTP error code returned by Discord + * @type {number} + */ + this.code = error.code; + + /** + * The HTTP method used for the request + * @type {string} + */ + this.method = method; + } + + /** + * Flattens an errors object returned from the API into an array. + * @param {Object} obj Discord errors object + * @param {string} [key] Used internally to determine key names of nested fields + * @returns {string[]} + * @private + */ + static flattenErrors(obj, key = '') { + let messages = []; + + for (const k of Object.keys(obj)) { + if (k === 'message') continue; + const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k; + + if (obj[k]._errors) { + messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`); + } else if (obj[k].code || obj[k].message) { + messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}: ${obj[k].message}`.trim()); + } else if (typeof obj[k] === 'string') { + messages.push(obj[k]); + } else { + messages = messages.concat(this.flattenErrors(obj[k], newKey)); + } + } + + return messages; + } +} + +module.exports = DiscordAPIError; diff --git a/node_modules/discord.js/src/client/rest/RESTManager.js b/node_modules/discord.js/src/client/rest/RESTManager.js new file mode 100644 index 0000000..ee64712 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RESTManager.js @@ -0,0 +1,58 @@ +const UserAgentManager = require('./UserAgentManager'); +const RESTMethods = require('./RESTMethods'); +const SequentialRequestHandler = require('./RequestHandlers/Sequential'); +const BurstRequestHandler = require('./RequestHandlers/Burst'); +const APIRequest = require('./APIRequest'); +const Constants = require('../../util/Constants'); + +class RESTManager { + constructor(client) { + this.client = client; + this.handlers = {}; + this.userAgentManager = new UserAgentManager(this); + this.methods = new RESTMethods(this); + this.rateLimitedEndpoints = {}; + this.globallyRateLimited = false; + } + + destroy() { + for (const handlerKey of Object.keys(this.handlers)) { + const handler = this.handlers[handlerKey]; + if (handler.destroy) handler.destroy(); + } + } + + push(handler, apiRequest) { + return new Promise((resolve, reject) => { + handler.push({ + request: apiRequest, + resolve, + reject, + retries: 0, + }); + }); + } + + getRequestHandler() { + switch (this.client.options.apiRequestMethod) { + case 'sequential': + return SequentialRequestHandler; + case 'burst': + return BurstRequestHandler; + default: + throw new Error(Constants.Errors.INVALID_RATE_LIMIT_METHOD); + } + } + + makeRequest(method, url, auth, data, file, reason) { + const apiRequest = new APIRequest(this, method, url, auth, data, file, reason); + if (!this.handlers[apiRequest.route]) { + const RequestHandlerType = this.getRequestHandler(); + this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route); + } + + return this.push(this.handlers[apiRequest.route], apiRequest); + } +} + +module.exports = RESTManager; diff --git a/node_modules/discord.js/src/client/rest/RESTMethods.js b/node_modules/discord.js/src/client/rest/RESTMethods.js new file mode 100644 index 0000000..59f3c65 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RESTMethods.js @@ -0,0 +1,1067 @@ +const querystring = require('querystring'); +const long = require('long'); +const Permissions = require('../../util/Permissions'); +const Constants = require('../../util/Constants'); +const Endpoints = Constants.Endpoints; +const Collection = require('../../util/Collection'); +const Util = require('../../util/Util'); +const resolvePermissions = require('../../structures/shared/resolvePermissions'); + +const RichEmbed = require('../../structures/RichEmbed'); +const User = require('../../structures/User'); +const GuildMember = require('../../structures/GuildMember'); +const Message = require('../../structures/Message'); +const Role = require('../../structures/Role'); +const Invite = require('../../structures/Invite'); +const Webhook = require('../../structures/Webhook'); +const UserProfile = require('../../structures/UserProfile'); +const OAuth2Application = require('../../structures/OAuth2Application'); +const Channel = require('../../structures/Channel'); +const GroupDMChannel = require('../../structures/GroupDMChannel'); +const Guild = require('../../structures/Guild'); +const VoiceRegion = require('../../structures/VoiceRegion'); +const GuildAuditLogs = require('../../structures/GuildAuditLogs'); + +const MessageFlags = require('../../util/MessageFlags'); + +class RESTMethods { + constructor(restManager) { + this.rest = restManager; + this.client = restManager.client; + this._ackToken = null; + } + + login(token = this.client.token) { + return new Promise((resolve, reject) => { + if (!token || typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); + token = token.replace(/^Bot\s*/i, ''); + this.client.manager.connectToWebSocket(token, resolve, reject); + }).catch(e => { + this.client.destroy(); + return Promise.reject(e); + }); + } + + logout() { + return this.rest.makeRequest('post', Endpoints.logout, true, {}); + } + + getGateway(bot = false) { + return this.rest.makeRequest('get', bot ? Endpoints.gateway.bot : Endpoints.gateway, true); + } + + fetchVoiceRegions(guildID) { + let endpoint; + if (guildID) endpoint = Endpoints.Guild(guildID).voiceRegions; + else endpoint = Endpoints.voiceRegions; + return this.rest.makeRequest('get', endpoint, true).then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); + } + + fetchEmbed(guildID) { + return this.rest.makeRequest('get', Endpoints.Guild(guildID).embed, true).then(data => ({ + enabled: data.enabled, + channel: data.channel_id ? this.client.channels.get(data.channel_id) : null, + })); + } + + sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code, reply } = {}, files = null) { + return new Promise((resolve, reject) => { // eslint-disable-line complexity + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + // The nonce has to be a uint64 :< + if (typeof nonce !== 'undefined') { + nonce = parseInt(nonce); + if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.'); + } + + if (content) { + if (split && typeof split !== 'object') split = {}; + + // Wrap everything in a code block + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + if (split) { + split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; + split.append = '\n```'; + } + } + + // Add zero-width spaces to @everyone/@here + if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + // Add the reply prefix + if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + if (split) split.prepend = `${mention}, ${split.prepend || ''}`; + } + + // Split the content + if (split) content = Util.splitMessage(content, split); + } else if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + content = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + } + + const send = chan => { + if (content instanceof Array) { + const messages = []; + (function sendChunk(list, index) { + const options = index === list.length - 1 ? { tts, embed, files } : { tts }; + chan.send(list[index], options).then(message => { + messages.push(message); + if (index >= list.length - 1) return resolve(messages); + return sendChunk(list, ++index); + }).catch(reject); + }(content, 0)); + } else { + this.rest.makeRequest('post', Endpoints.Channel(chan).messages, true, { + content, tts, nonce, embed, + }, files).then(data => resolve(this.client.actions.MessageCreate.handle(data).message), reject); + } + }; + + if (channel instanceof User || channel instanceof GuildMember) this.createDM(channel).then(send, reject); + else send(channel); + }); + } + + updateMessage(message, content, { flags, embed, code, reply } = {}) { + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + if (typeof flags !== 'undefined') flags = MessageFlags.resolve(flags); + + // Wrap everything in a code block + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + } + + // Add the reply prefix + if (reply && message.channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + } + + if (embed instanceof RichEmbed) embed = embed.toJSON(); + + return this.rest.makeRequest('patch', Endpoints.Message(message), true, { + content, embed, flags, + }).then(data => this.client.actions.MessageUpdate.handle(data).updated); + } + + deleteMessage(message) { + return this.rest.makeRequest('delete', Endpoints.Message(message), true) + .then(() => + this.client.actions.MessageDelete.handle({ + id: message.id, + channel_id: message.channel.id, + }).message + ); + } + + ackMessage(message) { + return this.rest.makeRequest('post', Endpoints.Message(message).ack, true, { token: this._ackToken }).then(res => { + if (res.token) this._ackToken = res.token; + return message; + }); + } + + ackTextChannel(channel) { + return this.rest.makeRequest('post', Endpoints.Channel(channel).Message(channel.lastMessageID).ack, true, { + token: this._ackToken, + }).then(res => { + if (res.token) this._ackToken = res.token; + return channel; + }); + } + + ackGuild(guild) { + return this.rest.makeRequest('post', Endpoints.Guild(guild).ack, true).then(() => guild); + } + + bulkDeleteMessages(channel, messages) { + return this.rest.makeRequest('post', Endpoints.Channel(channel).messages.bulkDelete, true, { + messages: messages, + }).then(() => + this.client.actions.MessageDeleteBulk.handle({ + channel_id: channel.id, + ids: messages, + }).messages + ); + } + + search(target, options) { + if (typeof options === 'string') options = { content: options }; + if (options.before) { + if (!(options.before instanceof Date)) options.before = new Date(options.before); + options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString(); + } + if (options.after) { + if (!(options.after instanceof Date)) options.after = new Date(options.after); + options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString(); + } + if (options.during) { + if (!(options.during instanceof Date)) options.during = new Date(options.during); + const t = options.during.getTime() - 14200704e5; + options.minID = long.fromNumber(t).shiftLeft(22).toString(); + options.maxID = long.fromNumber(t + 86400000).shiftLeft(22).toString(); + } + if (options.channel) options.channel = this.client.resolver.resolveChannelID(options.channel); + if (options.author) options.author = this.client.resolver.resolveUserID(options.author); + if (options.mentions) options.mentions = this.client.resolver.resolveUserID(options.options.mentions); + options = { + content: options.content, + max_id: options.maxID, + min_id: options.minID, + has: options.has, + channel_id: options.channel, + author_id: options.author, + author_type: options.authorType, + context_size: options.contextSize, + sort_by: options.sortBy, + sort_order: options.sortOrder, + limit: options.limit, + offset: options.offset, + mentions: options.mentions, + mentions_everyone: options.mentionsEveryone, + link_hostname: options.linkHostname, + embed_provider: options.embedProvider, + embed_type: options.embedType, + attachment_filename: options.attachmentFilename, + attachment_extension: options.attachmentExtension, + include_nsfw: options.nsfw, + }; + + for (const key of Object.keys(options)) if (options[key] === undefined) delete options[key]; + const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + + let endpoint; + if (target instanceof Channel) { + endpoint = Endpoints.Channel(target).search; + } else if (target instanceof Guild) { + endpoint = Endpoints.Guild(target).search; + } else { + throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); + } + return this.rest.makeRequest('get', `${endpoint}?${queryString}`, true).then(body => { + const messages = body.messages.map(x => + x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)) + ); + return { + totalResults: body.total_results, + messages, + }; + }); + } + + createChannel(guild, name, options) { + const { + type, + topic, + nsfw, + bitrate, + userLimit, + parent, + permissionOverwrites, + position, + rateLimitPerUser, + reason, + } = options; + return this.rest.makeRequest('post', Endpoints.Guild(guild).channels, true, { + name, + topic, + type: type ? Constants.ChannelTypes[type.toUpperCase()] : Constants.ChannelTypes.TEXT, + nsfw, + bitrate, + user_limit: userLimit, + parent_id: parent instanceof Channel ? parent.id : parent, + permission_overwrites: resolvePermissions.call(this, permissionOverwrites, guild), + position, + rate_limit_per_user: rateLimitPerUser, + }, + undefined, + reason).then(data => this.client.actions.ChannelCreate.handle(data).channel); + } + + createDM(recipient) { + const dmChannel = this.getExistingDM(recipient); + if (dmChannel) return Promise.resolve(dmChannel); + return this.rest.makeRequest('post', Endpoints.User(this.client.user).channels, true, { + recipient_id: recipient.id, + }).then(data => this.client.actions.ChannelCreate.handle(data).channel); + } + + createGroupDM(options) { + const data = this.client.user.bot ? + { access_tokens: options.accessTokens, nicks: options.nicks } : + { recipients: options.recipients }; + return this.rest.makeRequest('post', Endpoints.User('@me').channels, true, data) + .then(res => new GroupDMChannel(this.client, res)); + } + + addUserToGroupDM(channel, options) { + const data = this.client.user.bot ? + { nick: options.nick, access_token: options.accessToken } : + { recipient: options.id }; + return this.rest.makeRequest('put', Endpoints.Channel(channel).Recipient(options.id), true, data) + .then(() => channel); + } + + removeUserFromGroupDM(channel, userId) { + return this.rest.makeRequest('delete', Endpoints.Channel(channel).Recipient(userId), true) + .then(() => channel); + } + + updateGroupDMChannel(channel, _data) { + const data = {}; + data.name = _data.name; + data.icon = _data.icon; + return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data).then(() => channel); + } + + getExistingDM(recipient) { + return this.client.channels.find(channel => + channel.recipient && channel.recipient.id === recipient.id + ); + } + + deleteChannel(channel, reason) { + if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel); + if (!channel) return Promise.reject(new Error('No channel to delete.')); + return this.rest.makeRequest('delete', Endpoints.Channel(channel), true, undefined, undefined, reason) + .then(data => { + data.id = channel.id; + return this.client.actions.ChannelDelete.handle(data).channel; + }); + } + + updateChannel(channel, _data, reason) { + const data = {}; + data.name = (_data.name || channel.name).trim(); + data.topic = typeof _data.topic === 'undefined' ? channel.topic : _data.topic; + data.nsfw = typeof _data.nsfw === 'undefined' ? channel.nsfw : _data.nsfw; + data.position = _data.position || channel.position; + data.bitrate = _data.bitrate || (channel.bitrate ? channel.bitrate * 1000 : undefined); + data.user_limit = typeof _data.userLimit !== 'undefined' ? _data.userLimit : channel.userLimit; + data.parent_id = _data.parent instanceof Channel ? _data.parent.id : _data.parent; + data.permission_overwrites = _data.permissionOverwrites ? + resolvePermissions.call(this, _data.permissionOverwrites, channel.guild) : undefined; + data.rate_limit_per_user = typeof _data.rateLimitPerUser !== 'undefined' ? + _data.rateLimitPerUser : channel.rateLimitPerUser; + return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data, undefined, reason).then(newData => + this.client.actions.ChannelUpdate.handle(newData).updated + ); + } + + leaveGuild(guild) { + if (guild.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); + return this.rest.makeRequest('delete', Endpoints.User('@me').Guild(guild.id), true).then(() => + this.client.actions.GuildDelete.handle({ id: guild.id }).guild + ); + } + + createGuild(options) { + options.icon = this.client.resolver.resolveBase64(options.icon) || null; + options.region = options.region || 'us-central'; + return new Promise((resolve, reject) => { + this.rest.makeRequest('post', Endpoints.guilds, true, options).then(data => { + if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id)); + + const handleGuild = guild => { + if (guild.id === data.id) { + this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); + this.client.clearTimeout(timeout); + resolve(guild); + } + }; + this.client.on(Constants.Events.GUILD_CREATE, handleGuild); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); + reject(new Error('Took too long to receive guild data.')); + }, 10000); + return undefined; + }, reject); + }); + } + + // Untested but probably will work + deleteGuild(guild) { + return this.rest.makeRequest('delete', Endpoints.Guild(guild), true).then(() => + this.client.actions.GuildDelete.handle({ id: guild.id }).guild + ); + } + + getUser(userID, cache) { + return this.rest.makeRequest('get', Endpoints.User(userID), true).then(data => { + if (cache) return this.client.actions.UserGet.handle(data).user; + else return new User(this.client, data); + }); + } + + updateCurrentUser(_data, password) { + const user = this.client.user; + const data = {}; + data.username = _data.username || user.username; + data.avatar = typeof _data.avatar === 'undefined' ? user.avatar : this.client.resolver.resolveBase64(_data.avatar); + if (!user.bot) { + data.email = _data.email || user.email; + data.password = password; + if (_data.new_password) data.new_password = _data.newPassword; + } + return this.rest.makeRequest('patch', Endpoints.User('@me'), true, data).then(newData => + this.client.actions.UserUpdate.handle(newData).updated + ); + } + + updateGuild(guild, data, reason) { + return this.rest.makeRequest('patch', Endpoints.Guild(guild), true, data, undefined, reason).then(newData => + this.client.actions.GuildUpdate.handle(newData).updated + ); + } + + kickGuildMember(guild, member, reason) { + return this.rest.makeRequest( + 'delete', Endpoints.Guild(guild).Member(member), true, + undefined, undefined, reason) + .then(() => member); + } + + createGuildRole(guild, data, reason) { + if (data.color) data.color = this.client.resolver.resolveColor(data.color); + if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + return this.rest.makeRequest('post', Endpoints.Guild(guild).roles, true, data, undefined, reason).then(r => { + const { role } = this.client.actions.GuildRoleCreate.handle({ + guild_id: guild.id, + role: r, + }); + if (data.position) return role.setPosition(data.position, reason); + return role; + }); + } + + deleteGuildRole(role, reason) { + return this.rest.makeRequest( + 'delete', Endpoints.Guild(role.guild).Role(role.id), true, + undefined, undefined, reason) + .then(() => + this.client.actions.GuildRoleDelete.handle({ + guild_id: role.guild.id, + role_id: role.id, + }).role + ); + } + + setChannelOverwrite(channel, payload) { + return this.rest.makeRequest('put', `${Endpoints.Channel(channel).permissions}/${payload.id}`, true, payload); + } + + deletePermissionOverwrites(overwrite, reason) { + return this.rest.makeRequest( + 'delete', `${Endpoints.Channel(overwrite.channel).permissions}/${overwrite.id}`, + true, undefined, undefined, reason + ).then(() => overwrite); + } + + getChannelMessages(channel, payload = {}) { + const params = []; + if (payload.limit) params.push(`limit=${payload.limit}`); + if (payload.around) params.push(`around=${payload.around}`); + else if (payload.before) params.push(`before=${payload.before}`); + else if (payload.after) params.push(`after=${payload.after}`); + + let endpoint = Endpoints.Channel(channel).messages; + if (params.length > 0) endpoint += `?${params.join('&')}`; + return this.rest.makeRequest('get', endpoint, true); + } + + getChannelMessage(channel, messageID) { + const msg = channel.messages.get(messageID); + if (msg) return Promise.resolve(msg); + return this.rest.makeRequest('get', Endpoints.Channel(channel).Message(messageID), true); + } + + putGuildMember(guild, userID, options) { + options.access_token = options.accessToken; + if (options.roles) { + const roles = options.roles; + if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { + options.roles = roles.map(role => role.id); + } + } + return this.rest.makeRequest('put', Endpoints.Guild(guild).Member(userID), true, options) + .then(data => this.client.actions.GuildMemberGet.handle(guild, data).member); + } + + getGuild(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild), true); + } + + getGuildMember(guild, userID, cache) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).Member(userID), true).then(data => { + if (cache) return this.client.actions.GuildMemberGet.handle(guild, data).member; + else return new GuildMember(guild, data); + }); + } + + updateGuildMember(member, data, reason) { + if (data.channel) { + const channel = this.client.resolver.resolveChannel(data.channel); + if (!channel || channel.guild.id !== member.guild.id || channel.type !== 'voice') { + return Promise.reject(new Error('Could not resolve channel to a guild voice channel.')); + } + data.channel_id = channel.id; + data.channel = undefined; + } else if (data.channel === null) { + data.channel_id = null; + data.channel = undefined; + } + if (data.roles) data.roles = [...new Set(data.roles.map(role => role instanceof Role ? role.id : role))]; + + let endpoint = Endpoints.Member(member); + // Fix your endpoints, discord ;-; + if (member.id === this.client.user.id) { + const keys = Object.keys(data); + if (keys.length === 1 && keys[0] === 'nick') { + endpoint = Endpoints.Member(member).nickname; + } + } + + return this.rest.makeRequest('patch', endpoint, true, data, undefined, reason).then(newData => + member.guild._updateMember(member, newData).mem + ); + } + + addMemberRole(member, role, reason) { + return new Promise((resolve, reject) => { + if (member._roles.includes(role.id)) return resolve(member); + + const listener = (oldMember, newMember) => { + if (newMember.id === member.id && !oldMember._roles.includes(role.id) && newMember._roles.includes(role.id)) { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + resolve(newMember); + } + }; + + this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + reject(new Error('Adding the role timed out.')); + }, 10e3); + + return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason) + .catch(err => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + this.client.clearTimeout(timeout); + reject(err); + }); + }); + } + + removeMemberRole(member, role, reason) { + return new Promise((resolve, reject) => { + if (!member._roles.includes(role.id)) return resolve(member); + + const listener = (oldMember, newMember) => { + if (newMember.id === member.id && oldMember._roles.includes(role.id) && !newMember._roles.includes(role.id)) { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + resolve(newMember); + } + }; + + this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + reject(new Error('Removing the role timed out.')); + }, 10e3); + + return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason) + .catch(err => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + this.client.clearTimeout(timeout); + reject(err); + }); + }); + } + + sendTyping(channelID) { + return this.rest.makeRequest('post', Endpoints.Channel(channelID).typing, true); + } + + banGuildMember(guild, member, options) { + const id = this.client.resolver.resolveUserID(member); + if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); + + const url = `${Endpoints.Guild(guild).bans}/${id}?${querystring.stringify(options)}`; + return this.rest.makeRequest('put', url, true).then(() => { + if (member instanceof GuildMember) return member; + const user = this.client.resolver.resolveUser(id); + if (user) { + member = this.client.resolver.resolveGuildMember(guild, user); + return member || user; + } + return id; + }); + } + + unbanGuildMember(guild, member, reason) { + return new Promise((resolve, reject) => { + const id = this.client.resolver.resolveUserID(member); + if (!id) throw new Error('Couldn\'t resolve the user ID to unban.'); + + const listener = (eGuild, eUser) => { + if (eGuild.id === guild.id && eUser.id === id) { + this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); + this.client.clearTimeout(timeout); + resolve(eUser); + } + }; + this.client.on(Constants.Events.GUILD_BAN_REMOVE, listener); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); + reject(new Error('Took too long to receive the ban remove event.')); + }, 10000); + + this.rest.makeRequest('delete', `${Endpoints.Guild(guild).bans}/${id}`, true, undefined, undefined, reason) + .catch(err => { + this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); + this.client.clearTimeout(timeout); + reject(err); + }); + }); + } + + getGuildBan(guild, user) { + const id = this.client.resolver.resolveUserID(user); + return this.rest.makeRequest('get', `${Endpoints.Guild(guild).bans}/${id}`, true).then(ban => ({ + reason: ban.reason, + user: this.client.dataManager.newUser(ban.user), + })); + } + + getGuildBans(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(bans => + bans.reduce((collection, ban) => { + collection.set(ban.user.id, { + reason: ban.reason, + user: this.client.dataManager.newUser(ban.user), + }); + return collection; + }, new Collection()) + ); + } + + updateGuildRole(role, _data, reason) { + const data = {}; + data.name = _data.name || role.name; + data.position = typeof _data.position !== 'undefined' ? _data.position : role.position; + data.color = _data.color === null ? null : this.client.resolver.resolveColor(_data.color || role.color); + data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist; + data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable; + + if (typeof _data.permissions !== 'undefined') data.permissions = Permissions.resolve(_data.permissions); + else data.permissions = role.permissions; + + return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data, undefined, reason) + .then(_role => + this.client.actions.GuildRoleUpdate.handle({ + role: _role, + guild_id: role.guild.id, + }).updated + ); + } + + pinMessage(message) { + return this.rest.makeRequest('put', Endpoints.Channel(message.channel).Pin(message.id), true) + .then(() => message); + } + + unpinMessage(message) { + return this.rest.makeRequest('delete', Endpoints.Channel(message.channel).Pin(message.id), true) + .then(() => message); + } + + getChannelPinnedMessages(channel) { + return this.rest.makeRequest('get', Endpoints.Channel(channel).pins, true); + } + + createChannelInvite(channel, options, reason) { + const payload = {}; + payload.temporary = options.temporary; + payload.max_age = options.maxAge; + payload.max_uses = options.maxUses; + payload.unique = options.unique; + return this.rest.makeRequest('post', Endpoints.Channel(channel).invites, true, payload, undefined, reason) + .then(invite => new Invite(this.client, invite)); + } + + deleteInvite(invite, reason) { + return this.rest.makeRequest('delete', Endpoints.Invite(invite.code), true, undefined, undefined, reason) + .then(() => invite); + } + + getInvite(code) { + return this.rest.makeRequest('get', Endpoints.Invite(code), true).then(invite => + new Invite(this.client, invite) + ); + } + + getGuildInvites(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).invites, true).then(inviteItems => { + const invites = new Collection(); + for (const inviteItem of inviteItems) { + const invite = new Invite(this.client, inviteItem); + invites.set(invite.code, invite); + } + return invites; + }); + } + + getGuildVanityCode(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).vanityURL, true) + .then(res => res.code); + } + + pruneGuildMembers(guild, days, dry, reason) { + return this.rest.makeRequest(dry ? + 'get' : + 'post', + `${Endpoints.Guild(guild).prune}?days=${days}`, true, undefined, undefined, reason) + .then(data => data.pruned); + } + + createEmoji(guild, image, name, roles, reason) { + const data = { image, name }; + if (roles) data.roles = roles.map(r => r.id ? r.id : r); + return this.rest.makeRequest('post', Endpoints.Guild(guild).emojis, true, data, undefined, reason) + .then(emoji => this.client.actions.GuildEmojiCreate.handle(guild, emoji).emoji); + } + + updateEmoji(emoji, _data, reason) { + const data = {}; + if (_data.name) data.name = _data.name; + if (_data.roles) data.roles = _data.roles.map(r => r.id ? r.id : r); + return this.rest.makeRequest('patch', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, data, undefined, reason) + .then(newEmoji => this.client.actions.GuildEmojiUpdate.handle(emoji, newEmoji).emoji); + } + + deleteEmoji(emoji, reason) { + return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, undefined, reason) + .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).emoji); + } + + getGuildAuditLogs(guild, options = {}) { + if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; + if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; + if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; + + const queryString = (querystring.stringify({ + before: options.before, + after: options.after, + limit: options.limit, + user_id: this.client.resolver.resolveUserID(options.user), + action_type: options.type, + }).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + + return this.rest.makeRequest('get', `${Endpoints.Guild(guild).auditLogs}?${queryString}`, true) + .then(data => GuildAuditLogs.build(guild, data)); + } + + getWebhook(id, token) { + return this.rest.makeRequest('get', Endpoints.Webhook(id, token), !token).then(data => + new Webhook(this.client, data) + ); + } + + getGuildWebhooks(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).webhooks, true).then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); + } + + getChannelWebhooks(channel) { + return this.rest.makeRequest('get', Endpoints.Channel(channel).webhooks, true).then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); + } + + createWebhook(channel, name, avatar, reason) { + return this.rest.makeRequest('post', Endpoints.Channel(channel).webhooks, true, { name, avatar }, undefined, reason) + .then(data => new Webhook(this.client, data)); + } + + editWebhook(webhook, options, reason) { + let endpoint; + let auth; + + // Changing the channel of a webhook or specifying a reason requires a bot token + if (options.channel_id || reason) { + endpoint = Endpoints.Webhook(webhook.id); + auth = true; + } else { + endpoint = Endpoints.Webhook(webhook.id, webhook.token); + auth = false; + } + + return this.rest.makeRequest('patch', endpoint, auth, options, undefined, reason).then(data => { + webhook.name = data.name; + webhook.avatar = data.avatar; + webhook.channelID = data.channel_id; + return webhook; + }); + } + + deleteWebhook(webhook, reason) { + return this.rest.makeRequest( + 'delete', Endpoints.Webhook(webhook.id, webhook.token), + false, undefined, undefined, reason); + } + + sendWebhookMessage(webhook, content, { avatarURL, tts, embeds, username } = {}, files = null) { + return new Promise((resolve, reject) => { + username = username || webhook.name; + + if (content instanceof Array) { + const messages = []; + (function sendChunk(list, index) { + const options = index === list.length - 1 ? { tts, embeds, files } : { tts }; + webhook.send(list[index], options).then(message => { + messages.push(message); + if (index >= list.length - 1) return resolve(messages); + return sendChunk(list, ++index); + }).catch(reject); + }(content, 0)); + } else { + this.rest.makeRequest('post', `${Endpoints.Webhook(webhook.id, webhook.token)}?wait=true`, false, { + username, + avatar_url: avatarURL, + content, + tts, + embeds, + }, files).then(data => { + if (!this.client.channels) resolve(data); + else resolve(this.client.actions.MessageCreate.handle(data).message); + }, reject); + } + }); + } + + sendSlackWebhookMessage(webhook, body) { + return this.rest.makeRequest( + 'post', `${Endpoints.Webhook(webhook.id, webhook.token)}/slack?wait=true`, false, body + ); + } + + fetchUserProfile(user) { + return this.rest.makeRequest('get', Endpoints.User(user).profile, true).then(data => + new UserProfile(user, data) + ); + } + + fetchMentions(options) { + if (options.guild instanceof Guild) options.guild = options.guild.id; + Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); + + return this.rest.makeRequest( + 'get', Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild), true + ).then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); + } + + addFriend(user) { + return this.rest.makeRequest('post', Endpoints.User('@me'), true, { + username: user.username, + discriminator: user.discriminator, + }).then(() => user); + } + + removeFriend(user) { + return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) + .then(() => user); + } + + blockUser(user) { + return this.rest.makeRequest('put', Endpoints.User('@me').Relationship(user.id), true, { type: 2 }) + .then(() => user); + } + + unblockUser(user) { + return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) + .then(() => user); + } + + updateEmbed(guildID, embed, reason) { + return this.rest.makeRequest('patch', Endpoints.Guild(guildID).embed, true, { + enabled: embed.enabled, + channel_id: this.client.resolver.resolveChannelID(embed.channel), + }, undefined, reason); + } + + setRolePositions(guildID, roles) { + return this.rest.makeRequest('patch', Endpoints.Guild(guildID).roles, true, roles).then(() => + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: guildID, + roles, + }).guild + ); + } + + setChannelPositions(guildID, channels) { + return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, channels).then(() => + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: guildID, + channels, + }).guild + ); + } + + addMessageReaction(message, emoji) { + return this.rest.makeRequest( + 'put', Endpoints.Message(message).Reaction(emoji).User('@me'), true + ).then(() => + message._addReaction(Util.parseEmoji(emoji), message.client.user) + ); + } + + removeMessageReaction(message, emoji, userID) { + const endpoint = Endpoints.Message(message).Reaction(emoji).User(userID === this.client.user.id ? '@me' : userID); + return this.rest.makeRequest('delete', endpoint, true).then(() => + this.client.actions.MessageReactionRemove.handle({ + user_id: userID, + message_id: message.id, + emoji: Util.parseEmoji(emoji), + channel_id: message.channel.id, + }).reaction + ); + } + + removeMessageReactionEmoji(message, emoji) { + const endpoint = Endpoints.Message(message).Reaction(emoji); + return this.rest.makeRequest('delete', endpoint, true).then(() => + this.client.actions.MessageReactionRemoveEmoji.handle({ + message_id: message.id, + emoji: Util.parseEmoji(emoji), + channel_id: message.channel.id, + }).reaction + ); + } + + removeMessageReactions(message) { + return this.rest.makeRequest('delete', Endpoints.Message(message).reactions, true) + .then(() => message); + } + + getMessageReactionUsers(message, emoji, options) { + const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + + return this.rest.makeRequest('get', `${Endpoints.Message(message).Reaction(emoji)}?${queryString}`, true); + } + + getApplication(id) { + return this.rest.makeRequest('get', Endpoints.OAUTH2.Application(id), true).then(app => + new OAuth2Application(this.client, app) + ); + } + + resetApplication(id) { + return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetToken, true) + .then(() => this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetSecret, true)) + .then(app => new OAuth2Application(this.client, app)); + } + + setNote(user, note) { + return this.rest.makeRequest('put', Endpoints.User(user).note, true, { note }).then(() => user); + } + + acceptInvite(code) { + if (code.id) code = code.id; + return new Promise((resolve, reject) => + this.rest.makeRequest('post', Endpoints.Invite(code), true).then(res => { + const handler = guild => { + if (guild.id === res.id) { + resolve(guild); + this.client.removeListener(Constants.Events.GUILD_CREATE, handler); + } + }; + this.client.on(Constants.Events.GUILD_CREATE, handler); + this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_CREATE, handler); + reject(new Error('Accepting invite timed out')); + }, 120e3); + }) + ); + } + + patchUserSettings(data) { + return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data); + } + + patchClientUserGuildSettings(guildID, data) { + return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').Guild(guildID).settings, true, data); + } + + getIntegrations(guild) { + return this.rest.makeRequest( + 'get', + Constants.Endpoints.Guild(guild.id).integrations, + true + ); + } + + createIntegration(guild, data, reason) { + return this.rest.makeRequest( + 'post', + Constants.Endpoints.Guild(guild.id).integrations, + true, + data, + undefined, + reason + ); + } + + syncIntegration(integration) { + return this.rest.makeRequest( + 'post', + Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id), + true + ); + } + + editIntegration(integration, data, reason) { + return this.rest.makeRequest( + 'patch', + Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id), + true, + data, + undefined, + reason + ); + } + + deleteIntegration(integration, reason) { + return this.rest.makeRequest( + 'delete', + Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id), + true, + undefined, + undefined, + reason + ); + } +} + +module.exports = RESTMethods; diff --git a/node_modules/discord.js/src/client/rest/RequestHandlers/Burst.js b/node_modules/discord.js/src/client/rest/RequestHandlers/Burst.js new file mode 100644 index 0000000..eceee65 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/Burst.js @@ -0,0 +1,90 @@ +const RequestHandler = require('./RequestHandler'); +const DiscordAPIError = require('../DiscordAPIError'); +const { Events: { RATE_LIMIT } } = require('../../../util/Constants'); + +class BurstRequestHandler extends RequestHandler { + constructor(restManager, endpoint) { + super(restManager, endpoint); + + this.client = restManager.client; + + this.limit = Infinity; + this.resetTime = null; + this.remaining = 1; + this.timeDifference = 0; + + this.resetTimeout = null; + } + + push(request) { + super.push(request); + this.handle(); + } + + execute(item) { + if (!item) return; + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.limit = Number(res.headers['x-ratelimit-limit']); + this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.remaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + if (this.resetTimeout) return; + this.resetTimeout = this.client.setTimeout(() => { + this.remaining = this.limit; + this.globalLimit = false; + this.handle(); + this.resetTimeout = null; + }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else if (err.status >= 500 && err.status < 600) { + if (item.retries === this.client.options.retryLimit) { + item.reject(err); + this.handle(); + } else { + item.retries++; + this.queue.unshift(item); + this.resetTimeout = this.client.setTimeout(() => { + this.handle(); + this.resetTimeout = null; + }, 1e3 + this.client.options.restTimeOffset); + } + } else { + item.reject(err.status >= 400 && err.status < 500 ? + new DiscordAPIError(res.request.path, res.body, res.request.method) : err); + this.handle(); + } + } else { + if (this.remaining === 0) { + if (this.client.listenerCount(RATE_LIMIT)) { + this.client.emit(RATE_LIMIT, { + limit: this.limit, + timeDifference: this.timeDifference, + path: item.request.path, + method: item.request.method, + }); + } + } + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + this.handle(); + } + }); + } + + handle() { + super.handle(); + if (this.queue.length === 0) return; + if ((this.remaining <= 0 || this.globalLimit) && Date.now() - this.timeDifference < this.resetTime) return; + this.execute(this.queue.shift()); + this.remaining--; + this.handle(); + } +} + +module.exports = BurstRequestHandler; diff --git a/node_modules/discord.js/src/client/rest/RequestHandlers/RequestHandler.js b/node_modules/discord.js/src/client/rest/RequestHandlers/RequestHandler.js new file mode 100644 index 0000000..c5bad20 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/RequestHandler.js @@ -0,0 +1,54 @@ +/** + * A base class for different types of rate limiting handlers for the REST API. + * @private + */ +class RequestHandler { + /** + * @param {RESTManager} restManager The REST manager to use + */ + constructor(restManager) { + /** + * The RESTManager that instantiated this RequestHandler + * @type {RESTManager} + */ + this.restManager = restManager; + + /** + * A list of requests that have yet to be processed + * @type {APIRequest[]} + */ + this.queue = []; + } + + /** + * Whether or not the client is being rate limited on every endpoint + * @type {boolean} + * @readonly + */ + get globalLimit() { + return this.restManager.globallyRateLimited; + } + + set globalLimit(value) { + this.restManager.globallyRateLimited = value; + } + + /** + * Push a new API request into this bucket. + * @param {APIRequest} request The new request to push into the queue + */ + push(request) { + this.queue.push(request); + } + + /** + * Attempts to get this RequestHandler to process its current queue. + */ + handle() {} // eslint-disable-line no-empty-function + + destroy() { + this.queue = []; + } +} + +module.exports = RequestHandler; diff --git a/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js b/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js new file mode 100644 index 0000000..9d25bdb --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js @@ -0,0 +1,132 @@ +const RequestHandler = require('./RequestHandler'); +const DiscordAPIError = require('../DiscordAPIError'); +const { Events: { RATE_LIMIT } } = require('../../../util/Constants'); + +/** + * Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto + * the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account, + * but it can be slower. + * @extends {RequestHandler} + * @private + */ +class SequentialRequestHandler extends RequestHandler { + /** + * @param {RESTManager} restManager The REST manager to use + * @param {string} endpoint The endpoint to handle + */ + constructor(restManager, endpoint) { + super(restManager, endpoint); + + /** + * The client that instantiated this handler + * @type {Client} + */ + this.client = restManager.client; + + /** + * The endpoint that this handler is handling + * @type {string} + */ + this.endpoint = endpoint; + + /** + * The time difference between Discord's Dates and the local computer's Dates. A positive number means the local + * computer's time is ahead of Discord's + * @type {number} + */ + this.timeDifference = 0; + + /** + * Whether the queue is being processed or not + * @type {boolean} + */ + this.busy = false; + } + + push(request) { + super.push(request); + this.handle(); + } + + /** + * Performs a request then resolves a promise to indicate its readiness for a new request. + * @param {APIRequest} item The item to execute + * @returns {Promise<?Object|Error>} + */ + execute(item) { + this.busy = true; + return new Promise(resolve => { + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.requestLimit = Number(res.headers['x-ratelimit-limit']); + this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.requestRemaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + this.client.setTimeout(() => { + this.globalLimit = false; + resolve(); + }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + } else if (err.status >= 500 && err.status < 600) { + if (item.retries === this.client.options.retryLimit) { + item.reject(err); + resolve(); + } else { + item.retries++; + this.queue.unshift(item); + this.client.setTimeout(resolve, 1e3 + this.client.options.restTimeOffset); + } + } else { + item.reject(err.status >= 400 && err.status < 500 ? + new DiscordAPIError(res.request.path, res.body, res.request.method) : err); + resolve(err); + } + } else { + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + if (this.requestRemaining === 0) { + if (this.client.listenerCount(RATE_LIMIT)) { + /** + * Emitted when the client hits a rate limit while making a request + * @event Client#rateLimit + * @param {Object} rateLimitInfo Object containing the rate limit info + * @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint + * @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers + * @param {string} rateLimitInfo.path Path used for request that triggered this event + * @param {string} rateLimitInfo.method HTTP method used for request that triggered this event + */ + this.client.emit(RATE_LIMIT, { + limit: this.requestLimit, + timeDifference: this.timeDifference, + path: item.request.path, + method: item.request.method, + }); + } + this.client.setTimeout( + () => resolve(data), + this.requestResetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset + ); + } else { + resolve(data); + } + } + }); + }); + } + + handle() { + super.handle(); + if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return; + this.execute(this.queue.shift()).then(() => { + this.busy = false; + this.handle(); + }); + } +} + +module.exports = SequentialRequestHandler; diff --git a/node_modules/discord.js/src/client/rest/UserAgentManager.js b/node_modules/discord.js/src/client/rest/UserAgentManager.js new file mode 100644 index 0000000..13e5528 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/UserAgentManager.js @@ -0,0 +1,25 @@ +const Constants = require('../../util/Constants'); + +class UserAgentManager { + constructor() { + this.build(this.constructor.DEFAULT); + } + + set({ url, version } = {}) { + this.build({ + url: url || this.constructor.DFEAULT.url, + version: version || this.constructor.DEFAULT.version, + }); + } + + build(ua) { + this.userAgent = `DiscordBot (${ua.url}, ${ua.version}) Node.js/${process.version}`; + } +} + +UserAgentManager.DEFAULT = { + url: Constants.Package.homepage.split('#')[0], + version: Constants.Package.version, +}; + +module.exports = UserAgentManager; 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..d8d3f9f --- /dev/null +++ b/node_modules/discord.js/src/client/voice/ClientVoiceManager.js @@ -0,0 +1,86 @@ +const Collection = require('../../util/Collection'); +const VoiceConnection = require('./VoiceConnection'); + +/** + * Manages all the voice stuff for the client. + * @private + */ +class ClientVoiceManager { + constructor(client) { + /** + * The client that instantiated this voice manager + * @type {Client} + */ + this.client = client; + + /** + * A collection mapping connection IDs to the Connection objects + * @type {Collection<Snowflake, VoiceConnection>} + */ + this.connections = new Collection(); + + this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); + this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); + } + + onVoiceServer({ guild_id, token, 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); + if (!connection) return; + if (!channel_id) { + connection._disconnect(); + this.connections.delete(guild_id); + return; + } + + connection.channel = this.client.channels.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>} + */ + joinChannel(channel) { + return new Promise((resolve, reject) => { + if (!channel.joinable) { + if (channel.full) { + throw new Error('You do not have permission to join this voice channel; it is full.'); + } else { + throw new Error('You do not have permission to join this voice channel.'); + } + } + + 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); + this.connections.set(channel.guild.id, connection); + } + + connection.once('failed', reason => { + this.connections.delete(channel.guild.id); + reject(reason); + }); + + connection.once('authenticated', () => { + connection.once('ready', () => resolve(connection)); + connection.once('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..3a78d87 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceBroadcast.js @@ -0,0 +1,366 @@ +const VolumeInterface = require('./util/VolumeInterface'); +const Prism = require('prism-media'); +const OpusEncoders = require('./opus/OpusEngineList'); +const Collection = require('../../util/Collection'); + +const ffmpegArguments = [ + '-analyzeduration', '0', + '-loglevel', '0', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', +]; + +/** + * A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency. + * + * Example usage: + * ```js + * const broadcast = client.createVoiceBroadcast(); + * broadcast.playFile('./music.mp3'); + * // Play "music.mp3" in all voice connections that the client is in + * for (const connection of client.voiceConnections.values()) { + * connection.playBroadcast(broadcast); + * } + * ``` + * @implements {VolumeInterface} + */ +class VoiceBroadcast extends VolumeInterface { + constructor(client) { + super(); + /** + * The client that created the broadcast + * @type {Client} + */ + this.client = client; + this._dispatchers = new Collection(); + this._encoders = new Collection(); + /** + * The audio transcoder that this broadcast uses + * @type {Prism} + */ + this.prism = new Prism(); + /** + * The current audio transcoder that is being used + * @type {Object} + */ + this.currentTranscoder = null; + this.tickInterval = null; + this._volume = 1; + } + + /** + * An array of subscribed dispatchers + * @type {StreamDispatcher[]} + * @readonly + */ + get dispatchers() { + let d = []; + for (const container of this._dispatchers.values()) { + d = d.concat(Array.from(container.values())); + } + return d; + } + + get _playableStream() { + const currentTranscoder = this.currentTranscoder; + if (!currentTranscoder) return null; + const transcoder = currentTranscoder.transcoder; + const options = currentTranscoder.options; + return (transcoder && transcoder.output) || options.stream; + } + + unregisterDispatcher(dispatcher, old) { + const volume = old || dispatcher.volume; + + /** + * Emitted whenever a stream dispatcher unsubscribes from the broadcast. + * @event VoiceBroadcast#unsubscribe + * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher + */ + this.emit('unsubscribe', dispatcher); + for (const container of this._dispatchers.values()) { + container.delete(dispatcher); + + if (!container.size) { + this._encoders.get(volume).destroy(); + this._dispatchers.delete(volume); + this._encoders.delete(volume); + } + } + } + + registerDispatcher(dispatcher) { + if (!this._dispatchers.has(dispatcher.volume)) { + this._dispatchers.set(dispatcher.volume, new Set()); + this._encoders.set(dispatcher.volume, OpusEncoders.fetch()); + } + const container = this._dispatchers.get(dispatcher.volume); + if (!container.has(dispatcher)) { + container.add(dispatcher); + dispatcher.once('end', () => this.unregisterDispatcher(dispatcher)); + dispatcher.on('volumeChange', (o, n) => { + this.unregisterDispatcher(dispatcher, o); + if (!this._dispatchers.has(n)) { + this._dispatchers.set(n, new Set()); + this._encoders.set(n, OpusEncoders.fetch()); + } + this._dispatchers.get(n).add(dispatcher); + }); + /** + * Emitted whenever a stream dispatcher subscribes to the broadcast. + * @event VoiceBroadcast#subscribe + * @param {StreamDispatcher} dispatcher The subscribed dispatcher + */ + this.emit('subscribe', dispatcher); + } + } + + killCurrentTranscoder() { + if (this.currentTranscoder) { + if (this.currentTranscoder.transcoder) this.currentTranscoder.transcoder.kill(); + this.currentTranscoder = null; + this.emit('end'); + } + } + + /** + * Plays any audio stream across the broadcast. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {VoiceBroadcast} + * @example + * // Play streams using ytdl-core + * const ytdl = require('ytdl-core'); + * const streamOptions = { seek: 0, volume: 1 }; + * const broadcast = client.createVoiceBroadcast(); + * + * voiceChannel.join() + * .then(connection => { + * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); + * broadcast.playStream(stream); + * const dispatcher = connection.playBroadcast(broadcast); + * }) + * .catch(console.error); + */ + playStream(stream, options = {}) { + this.setVolume(options.volume || 1); + return this._playTranscodable(stream, options); + } + + /** + * Play the given file in the voice connection. + * @param {string} file The absolute path to the file + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play files natively + * const broadcast = client.createVoiceBroadcast(); + * + * voiceChannel.join() + * .then(connection => { + * broadcast.playFile('C:/Users/Discord/Desktop/music.mp3'); + * const dispatcher = connection.playBroadcast(broadcast); + * }) + * .catch(console.error); + */ + playFile(file, options = {}) { + this.setVolume(options.volume || 1); + return this._playTranscodable(`file:${file}`, options); + } + + _playTranscodable(media, options) { + this.killCurrentTranscoder(); + const transcoder = this.prism.transcode({ + type: 'ffmpeg', + media, + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), + }); + /** + * Emitted whenever an error occurs. + * @event VoiceBroadcast#error + * @param {Error} error The error that occurred + */ + transcoder.once('error', e => { + if (this.listenerCount('error') > 0) this.emit('error', e); + /** + * Emitted whenever the VoiceBroadcast has any warnings. + * @event VoiceBroadcast#warn + * @param {string|Error} warning The warning that was raised + */ + else this.emit('warn', e); + }); + /** + * Emitted once the broadcast (the audio stream) ends. + * @event VoiceBroadcast#end + */ + transcoder.once('end', () => this.killCurrentTranscoder()); + this.currentTranscoder = { + transcoder, + options, + }; + transcoder.output.once('readable', () => this._startPlaying()); + return this; + } + + /** + * Plays a stream of 16-bit signed stereo PCM. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {VoiceBroadcast} + */ + playConvertedStream(stream, options = {}) { + this.killCurrentTranscoder(); + this.setVolume(options.volume || 1); + this.currentTranscoder = { options: { stream } }; + stream.once('readable', () => this._startPlaying()); + return this; + } + + /** + * Plays an Opus encoded stream. + * <warn>Note that inline volume is not compatible with this method.</warn> + * @param {ReadableStream} stream The Opus audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playOpusStream(stream) { + this.currentTranscoder = { options: { stream }, opus: true }; + stream.once('readable', () => this._startPlaying()); + return this; + } + + /** + * Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) + * @param {string} input The arbitrary input + * @param {StreamOptions} [options] Options for playing the stream + * @returns {VoiceBroadcast} + */ + playArbitraryInput(input, options = {}) { + this.setVolume(options.volume || 1); + options.input = input; + return this._playTranscodable(input, options); + } + + /** + * Pauses the entire broadcast - all dispatchers also pause. + */ + pause() { + this.paused = true; + for (const container of this._dispatchers.values()) { + for (const dispatcher of container.values()) { + dispatcher.pause(); + } + } + } + + /** + * Resumes the entire broadcast - all dispatchers also resume. + */ + resume() { + this.paused = false; + for (const container of this._dispatchers.values()) { + for (const dispatcher of container.values()) { + dispatcher.resume(); + } + } + } + + _startPlaying() { + if (this.tickInterval) clearInterval(this.tickInterval); + // Old code? + // this.tickInterval = this.client.setInterval(this.tick.bind(this), 20); + this._startTime = Date.now(); + this._count = 0; + this._pausedTime = 0; + this._missed = 0; + this.tick(); + } + + tick() { + if (!this._playableStream) return; + if (this.paused) { + this._pausedTime += 20; + setTimeout(() => this.tick(), 20); + return; + } + + const opus = this.currentTranscoder.opus; + const buffer = this.readStreamBuffer(); + + if (!buffer) { + this._missed++; + if (this._missed < 5) { + this._pausedTime += 200; + setTimeout(() => this.tick(), 200); + } else { + this.killCurrentTranscoder(); + } + return; + } + + this._missed = 0; + + let packetMatrix = {}; + + const getOpusPacket = volume => { + if (packetMatrix[volume]) return packetMatrix[volume]; + + const opusEncoder = this._encoders.get(volume); + const opusPacket = opusEncoder.encode(this.applyVolume(buffer, this._volume * volume)); + packetMatrix[volume] = opusPacket; + return opusPacket; + }; + + for (const dispatcher of this.dispatchers) { + if (opus) { + dispatcher.processPacket(buffer); + continue; + } + + const volume = dispatcher.volume; + dispatcher.processPacket(getOpusPacket(volume)); + } + + const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now()); + this._count++; + setTimeout(() => this.tick(), next); + } + + readStreamBuffer() { + const opus = this.currentTranscoder.opus; + const bufferLength = (opus ? 80 : 1920) * 2; + let buffer = this._playableStream.read(bufferLength); + if (opus) return buffer; + if (!buffer) return null; + + if (buffer.length !== bufferLength) { + const newBuffer = Buffer.alloc(bufferLength).fill(0); + buffer.copy(newBuffer); + buffer = newBuffer; + } + + return buffer; + } + + /** + * Stop the current stream from playing without unsubscribing dispatchers. + */ + end() { + this.killCurrentTranscoder(); + } + + /** + * End the current broadcast, all subscribed dispatchers will also end. + */ + destroy() { + this.end(); + for (const container of this._dispatchers.values()) { + for (const dispatcher of container.values()) { + dispatcher.destroy('end', 'broadcast ended'); + } + } + } +} + +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..2fc6338 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceConnection.js @@ -0,0 +1,598 @@ +const VoiceWebSocket = require('./VoiceWebSocket'); +const VoiceUDP = require('./VoiceUDPClient'); +const Util = require('../../util/Util'); +const Constants = require('../../util/Constants'); +const AudioPlayer = require('./player/AudioPlayer'); +const VoiceReceiver = require('./receiver/VoiceReceiver'); +const SingleSilence = require('./util/SingleSilence'); +const EventEmitter = require('events').EventEmitter; +const Prism = require('prism-media'); + +// 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; + +/** + * Represents a connection to a guild's voice server. + * ```js + * // Obtained using: + * voiceChannel.join() + * .then(connection => { + * + * }); + * ``` + * @extends {EventEmitter} + */ +class VoiceConnection extends EventEmitter { + constructor(voiceManager, channel) { + super(); + + /** + * The voice manager that instantiated this connection + * @type {ClientVoiceManager} + */ + this.voiceManager = voiceManager; + + /** + * The client that instantiated this connection + * @type {Client} + */ + this.client = voiceManager.client; + + /** + * @external Prism + * @see {@link https://github.com/hydrabolt/prism-media} + */ + + /** + * The audio transcoder for this connection + * @type {Prism} + */ + this.prism = new Prism(); + + /** + * The voice channel this connection is currently serving + * @type {VoiceChannel} + */ + this.channel = channel; + + /** + * The current status of the voice connection + * @type {VoiceStatus} + */ + this.status = Constants.VoiceStatus.AUTHENTICATING; + + /** + * Whether we're currently transmitting audio + * @type {boolean} + */ + this.speaking = false; + + /** + * An array of Voice Receivers that have been created for this connection + * @type {VoiceReceiver[]} + */ + this.receivers = []; + + /** + * 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); + }); + + /** + * Map SSRC to user id + * @type {Map<number, Snowflake>} + * @private + */ + this.ssrcMap = new Map(); + + /** + * Map user id to speaking timeout + * @type {Map<Snowflake, Timeout>} + * @private + */ + this.speakingTimeouts = new Map(); + + /** + * Object that wraps contains the `ws` and `udp` sockets of this voice connection + * @type {Object} + * @private + */ + this.sockets = {}; + + this.authenticate(); + } + + /** + * The current stream dispatcher (if any) + * @type {?StreamDispatcher} + * @readonly + */ + get dispatcher() { + return this.player.dispatcher; + } + + /** + * Sets whether the voice connection should display as "speaking" or not. + * @param {boolean} value Whether or not to speak + * @private + */ + setSpeaking(value) { + if (this.speaking === value) return; + if (this.status !== Constants.VoiceStatus.CONNECTED) return; + this.speaking = value; + this.sockets.ws.sendPacket({ + op: Constants.VoiceOPCodes.SPEAKING, + d: { + speaking: true, + delay: 0, + }, + }).catch(e => { + this.emit('debug', e); + }); + } + + /** + * Sends a request to the main gateway to join a voice channel. + * @param {Object} [options] The options to provide + */ + sendVoiceStateUpdate(options = {}) { + options = Util.mergeDefault({ + guild_id: this.channel.guild.id, + channel_id: this.channel.id, + self_mute: false, + self_deaf: false, + }, options); + + this.client.ws.send({ + op: Constants.OPCodes.VOICE_STATE_UPDATE, + d: options, + }); + } + + /** + * Set the token and endpoint required to connect to the voice servers. + * @param {string} token The voice token + * @param {string} endpoint The voice endpoint + * @returns {void} + */ + setTokenAndEndpoint(token, endpoint) { + if (!endpoint) { + // Signifies awaiting endpoint stage + return; + } + + if (!token) { + this.authenticateFailed('Token not provided from voice server packet.'); + return; + } + + endpoint = endpoint.match(/([^:]*)/)[0]; + + if (!endpoint) { + this.authenticateFailed('Invalid endpoint received.'); + return; + } + + if (this.status === Constants.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 + */ + setSessionID(sessionID) { + if (!sessionID) { + this.authenticateFailed('Session ID not supplied.'); + return; + } + + if (this.status === Constants.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; + + if (token && endpoint && sessionID) { + this.client.clearTimeout(this.connectTimeout); + this.status = Constants.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); + if (this.status === Constants.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 = Constants.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(new Error('Connection not established within 15 seconds.')), 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.status = Constants.VoiceStatus.RECONNECTING; + /** + * Emitted when the voice connection is reconnecting (typically after a region change). + * @event VoiceConnection#reconnecting + */ + this.emit('reconnecting'); + this.connect(); + } + + /** + * Disconnect the voice connection, causing a disconnect and closing event to be emitted. + */ + disconnect() { + this.emit('closing'); + this.sendVoiceStateUpdate({ + channel_id: null, + }); + + this._disconnect(); + } + + /** + * Internally disconnects (doesn't send disconnect packet). + * @private + */ + _disconnect() { + this.player.destroy(); + this.cleanup(); + this.status = Constants.VoiceStatus.DISCONNECTED; + /** + * Emitted when the voice connection disconnects. + * @event VoiceConnection#disconnect + */ + this.emit('disconnect'); + } + + /** + * Cleans up after disconnect. + * @private + */ + cleanup() { + const { ws, udp } = this.sockets; + + if (ws) { + ws.removeAllListeners('error'); + ws.removeAllListeners('ready'); + ws.removeAllListeners('sessionDescription'); + ws.removeAllListeners('startSpeaking'); + ws.shutdown(); + } + + if (udp) udp.removeAllListeners('error'); + + this.sockets.ws = null; + this.sockets.udp = null; + } + + /** + * Connect the voice connection. + * @private + */ + connect() { + if (this.status !== Constants.VoiceStatus.RECONNECTING) { + if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.'); + if (this.sockets.udp) throw new Error('There is already an existing UDP connection.'); + } + + 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('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)); + } + + /** + * Invoked when the voice websocket is ready. + * @param {Object} data The received data + * @private + */ + onReady({ port, ssrc, ip }) { + this.authentication.port = port; + this.authentication.ssrc = ssrc; + this.sockets.udp.createUDPSocket(ip); + this.sockets.udp.socket.on('message', this.onUDPMessage.bind(this)); + } + + /** + * Invoked when a session description is received. + * @param {string} mode The encryption mode + * @param {string} secret The secret key + * @private + */ + onSessionDescription(mode, secret) { + this.authentication.encryptionMode = mode; + this.authentication.secretKey = secret; + + this.status = Constants.VoiceStatus.CONNECTED; + const ready = () => { + /** + * 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. + this.playOpusStream(new SingleSilence()).once('end', ready); + } + } + + /** + * Invoked whenever a user initially starts speaking. + * @param {Object} data The speaking data + * @private + */ + onStartSpeaking({ user_id, ssrc }) { + this.ssrcMap.set(+ssrc, user_id); + } + + /** + * Invoked when a speaking event is received. + * @param {Object} data The received data + * @private + */ + onSpeaking({ user_id, speaking }) { + const guild = this.channel.guild; + const user = this.client.users.get(user_id); + if (!speaking) { + for (const receiver of this.receivers) { + receiver.stoppedSpeaking(user); + } + } + /** + * Emitted whenever a user starts/stops speaking. + * @event VoiceConnection#speaking + * @param {User} user The user that has started/stopped speaking + * @param {boolean} speaking Whether or not the user is speaking + */ + if (this.status === Constants.VoiceStatus.CONNECTED) this.emit('speaking', user, speaking); + guild._memberSpeakUpdate(user_id, speaking); + } + + /** + * Handles synthesizing of the speaking event. + * @param {Buffer} buffer Received packet from the UDP socket + * @private + */ + onUDPMessage(buffer) { + const ssrc = +buffer.readUInt32BE(8).toString(10); + const user = this.client.users.get(this.ssrcMap.get(ssrc)); + if (!user) return; + + let speakingTimeout = this.speakingTimeouts.get(ssrc); + if (typeof speakingTimeout === 'undefined') { + this.onSpeaking({ user_id: user.id, ssrc, speaking: true }); + } else { + this.client.clearTimeout(speakingTimeout); + } + + speakingTimeout = this.client.setTimeout(() => { + try { + this.onSpeaking({ user_id: user.id, ssrc, speaking: false }); + this.client.clearTimeout(speakingTimeout); + this.speakingTimeouts.delete(ssrc); + } catch (ex) { + // Connection already closed, ignore + } + }, DISCORD_SPEAKING_DELAY); + this.speakingTimeouts.set(ssrc, speakingTimeout); + } + + /** + * Options that can be passed to stream-playing methods: + * @typedef {Object} StreamOptions + * @property {number} [seek=0] The time to seek to + * @property {number} [volume=1] The volume to play at + * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss + * @property {number|string} [bitrate=48000] The bitrate (quality) of the audio. + * If set to 'auto', the voice channel's bitrate will be used + */ + + /** + * Play the given file in the voice connection. + * @param {string} file The absolute path to the file + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play files natively + * voiceChannel.join() + * .then(connection => { + * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); + * }) + * .catch(console.error); + */ + playFile(file, options) { + return this.player.playUnknownStream(`file:${file}`, options); + } + + /** + * Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) + * @param {string} input the arbitrary input + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playArbitraryInput(input, options) { + return this.player.playUnknownStream(input, options); + } + + /** + * Plays and converts an audio stream in the voice connection. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play streams using ytdl-core + * const ytdl = require('ytdl-core'); + * const streamOptions = { seek: 0, volume: 1 }; + * voiceChannel.join() + * .then(connection => { + * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); + * const dispatcher = connection.playStream(stream, streamOptions); + * }) + * .catch(console.error); + */ + playStream(stream, options) { + return this.player.playUnknownStream(stream, options); + } + + /** + * Plays a stream of 16-bit signed stereo PCM. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playConvertedStream(stream, options) { + return this.player.playPCMStream(stream, options); + } + + /** + * Plays an Opus encoded stream. + * <warn>Note that inline volume is not compatible with this method.</warn> + * @param {ReadableStream} stream The Opus audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playOpusStream(stream, options) { + return this.player.playOpusStream(stream, options); + } + + /** + * Plays a voice broadcast. + * @param {VoiceBroadcast} broadcast The broadcast to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play a broadcast + * const broadcast = client + * .createVoiceBroadcast() + * .playFile('./test.mp3'); + * const dispatcher = voiceConnection.playBroadcast(broadcast); + */ + playBroadcast(broadcast, options) { + return this.player.playBroadcast(broadcast, options); + } + + /** + * Creates a VoiceReceiver so you can start listening to voice data. + * It's recommended to only create one of these. + * @returns {VoiceReceiver} + */ + createReceiver() { + const receiver = new VoiceReceiver(this); + this.receivers.push(receiver); + return receiver; + } +} + +module.exports = VoiceConnection; diff --git a/node_modules/discord.js/src/client/voice/VoiceUDPClient.js b/node_modules/discord.js/src/client/voice/VoiceUDPClient.js new file mode 100644 index 0000000..cf1e388 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceUDPClient.js @@ -0,0 +1,127 @@ +const udp = require('dgram'); +const Constants = require('../../util/Constants'); +const EventEmitter = require('events').EventEmitter; + +/** + * 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() { + 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('Tried to send a UDP packet, but there is no socket available.'); + if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.'); + this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { + if (error) reject(error); else resolve(packet); + }); + }); + } + + createUDPSocket(address) { + this.discordAddress = address; + const socket = this.socket = udp.createSocket('udp4'); + + socket.once('message', message => { + const packet = parseLocalPacket(message); + if (packet.error) { + this.emit('error', packet.error); + return; + } + + this.localAddress = packet.address; + this.localPort = packet.port; + + this.voiceConnection.sockets.ws.sendPacket({ + op: Constants.VoiceOPCodes.SELECT_PROTOCOL, + d: { + protocol: 'udp', + data: { + address: packet.address, + port: packet.port, + mode: 'xsalsa20_poly1305', + }, + }, + }); + }); + + const blankMessage = Buffer.alloc(70); + blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4); + this.send(blankMessage); + } +} + +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/VoiceWebSocket.js b/node_modules/discord.js/src/client/voice/VoiceWebSocket.js new file mode 100644 index 0000000..2269007 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceWebSocket.js @@ -0,0 +1,246 @@ +const Constants = require('../../util/Constants'); +const SecretKey = require('./util/SecretKey'); +const EventEmitter = require('events').EventEmitter; + +let WebSocket; +try { + WebSocket = require('@discordjs/uws'); +} catch (err) { + WebSocket = require('ws'); +} + +/** + * Represents a Voice Connection's WebSocket. + * @extends {EventEmitter} + * @private + */ +class VoiceWebSocket extends EventEmitter { + constructor(voiceConnection) { + super(); + + /** + * The client of this voice WebSocket + * @type {Client} + */ + this.client = voiceConnection.voiceManager.client; + + /** + * The Voice Connection that this WebSocket serves + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + + /** + * How many connection attempts have been made + * @type {number} + */ + this.attempts = 0; + + this.connect(); + this.dead = false; + this.voiceConnection.on('closing', this.shutdown.bind(this)); + } + + shutdown() { + this.dead = true; + this.reset(); + } + + /** + * Resets the current WebSocket. + */ + reset() { + 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() { + if (this.dead) return; + if (this.ws) this.reset(); + if (this.attempts >= 5) { + this.emit('debug', new Error(`Too many connection attempts (${this.attempts}).`)); + return; + } + + this.attempts++; + + /** + * The actual WebSocket used to connect to the Voice WebSocket Server. + * @type {WebSocket} + */ + this.ws = new WebSocket(`wss://${this.voiceConnection.authentication.endpoint}`); + 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) { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error(`Voice websocket not open to send ${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.sendPacket({ + op: Constants.OPCodes.DISPATCH, + d: { + server_id: this.voiceConnection.channel.guild.id, + user_id: this.client.user.id, + token: this.voiceConnection.authentication.token, + session_id: this.voiceConnection.authentication.sessionID, + }, + }).catch(() => { + this.emit('error', new Error('Tried to send join packet, but the WebSocket is not open.')); + }); + } + + /** + * 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(JSON.parse(event.data)); + } catch (error) { + return this.onError(error); + } + } + + /** + * Called whenever the connection to the WebSocket server is lost. + */ + onClose() { + 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('error', error); + } + + /** + * Called whenever a valid packet is received from the WebSocket. + * @param {Object} packet The received packet + */ + onPacket(packet) { + switch (packet.op) { + case Constants.VoiceOPCodes.READY: + this.setHeartbeat(packet.d.heartbeat_interval); + /** + * Emitted once the voice WebSocket receives the ready packet. + * @param {Object} packet The received packet + * @event VoiceWebSocket#ready + */ + this.emit('ready', packet.d); + break; + case Constants.VoiceOPCodes.SESSION_DESCRIPTION: + /** + * Emitted once the Voice Websocket receives a description of this voice session. + * @param {string} encryptionMode The type of encryption being used + * @param {SecretKey} secretKey The secret key used for encryption + * @event VoiceWebSocket#sessionDescription + */ + this.emit('sessionDescription', packet.d.mode, new SecretKey(packet.d.secret_key)); + break; + case Constants.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('Tried to set voice heartbeat but no valid interval was specified.')); + return; + } + if (this.heartbeatInterval) { + /** + * Emitted whenver 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'); + 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; + } + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + + /** + * Sends a heartbeat packet. + */ + sendHeartbeat() { + this.sendPacket({ op: Constants.VoiceOPCodes.HEARTBEAT, d: null }).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/dispatcher/StreamDispatcher.js b/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js new file mode 100644 index 0000000..77734ca --- /dev/null +++ b/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js @@ -0,0 +1,331 @@ +const VolumeInterface = require('../util/VolumeInterface'); +const VoiceBroadcast = require('../VoiceBroadcast'); +const Constants = require('../../../util/Constants'); + +const secretbox = require('../util/Secretbox'); + +const nonce = Buffer.alloc(24); +nonce.fill(0); + +/** + * 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.playFile('./file.mp3'); + * }); + * ``` + * @implements {VolumeInterface} + */ +class StreamDispatcher extends VolumeInterface { + constructor(player, stream, streamOptions) { + super(streamOptions); + /** + * The Audio Player that controls this dispatcher + * @type {AudioPlayer} + */ + this.player = player; + /** + * The stream that the dispatcher plays + * @type {ReadableStream|VoiceBroadcast} + */ + this.stream = stream; + if (!(this.stream instanceof VoiceBroadcast)) this.startStreaming(); + this.streamOptions = streamOptions; + + const data = this.streamingData; + data.length = 20; + data.missed = 0; + + /** + * Whether playing is paused + * @type {boolean} + */ + this.paused = false; + /** + * Whether this dispatcher has been destroyed + * @type {boolean} + */ + this.destroyed = false; + + this._opus = streamOptions.opus; + } + + /** + * How many passes the dispatcher should take when sending packets to reduce packet loss. Values over 5 + * aren't recommended, as it means you are using 5x more bandwidth. You _can_ edit this at runtime + * @type {number} + * @readonly + */ + get passes() { + return this.streamOptions.passes || 1; + } + + set passes(n) { + this.streamOptions.passes = n; + } + + get streamingData() { + return this.player.streamingData; + } + + /** + * How long the stream dispatcher has been "speaking" for + * @type {number} + * @readonly + */ + get time() { + return this.streamingData.count * (this.streamingData.length || 0); + } + + /** + * The total time, taking into account pauses and skips, that the dispatcher has been streaming for + * @type {number} + * @readonly + */ + get totalStreamTime() { + return this.time + this.streamingData.pausedTime; + } + + /** + * Stops sending voice packets to the voice connection (stream may still progress however). + */ + pause() { this.setPaused(true); } + + /** + * Resumes sending voice packets to the voice connection (may be further on in the stream than when paused). + */ + resume() { this.setPaused(false); } + + + /** + * Stops the current stream permanently and emits an `end` event. + * @param {string} [reason='user'] An optional reason for stopping the dispatcher + */ + end(reason = 'user') { + this.destroy('end', reason); + } + + setSpeaking(value) { + if (this.speaking === value) return; + if (this.player.voiceConnection.status !== Constants.VoiceStatus.CONNECTED) return; + this.speaking = 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); + } + + + /** + * Set the bitrate of the current Opus encoder. + * @param {number} bitrate New bitrate, in kbps + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(bitrate) { + this.player.setBitrate(bitrate); + } + + sendBuffer(buffer, sequence, timestamp, opusPacket) { + opusPacket = opusPacket || this.player.opusEncoder.encode(buffer); + const packet = this.createPacket(sequence, timestamp, opusPacket); + this.sendPacket(packet); + } + + sendPacket(packet) { + let repeats = this.passes; + /** + * Emitted whenever the dispatcher has debug information. + * @event StreamDispatcher#debug + * @param {string} info The debug info + */ + this.setSpeaking(true); + while (repeats-- && this.player.voiceConnection.sockets.udp) { + this.player.voiceConnection.sockets.udp.send(packet) + .catch(e => { + this.setSpeaking(false); + this.emit('debug', `Failed to send a packet ${e}`); + }); + } + } + + createPacket(sequence, timestamp, buffer) { + const packetBuffer = Buffer.alloc(buffer.length + 28); + packetBuffer.fill(0); + 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); + buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key); + for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i]; + + return packetBuffer; + } + + processPacket(packet) { + try { + if (this.destroyed || !this.player.voiceConnection.authentication.secretKey) { + this.setSpeaking(false); + return; + } + + const data = this.streamingData; + + if (this.paused) { + this.setSpeaking(false); + data.pausedTime = data.length * 10; + return; + } + + if (!packet) { + data.missed++; + data.pausedTime += data.length * 10; + return; + } + + this.started(); + this.missed = 0; + + this.stepStreamingData(); + this.sendBuffer(null, data.sequence, data.timestamp, packet); + } catch (e) { + this.destroy('error', e); + } + } + + process() { + try { + if (this.destroyed) { + this.setSpeaking(false); + return; + } + + const data = this.streamingData; + + if (data.missed >= 5) { + this.destroy('end', 'Stream is not generating quickly enough.'); + return; + } + + if (this.paused) { + this.setSpeaking(false); + // Old code? + // data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0; + data.pausedTime += data.length * 10; + this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10); + return; + } + + this.started(); + + const buffer = this.readStreamBuffer(); + if (!buffer) { + data.missed++; + data.pausedTime += data.length * 10; + this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10); + return; + } + + data.missed = 0; + + this.stepStreamingData(); + + if (this._opus) { + this.sendBuffer(null, data.sequence, data.timestamp, buffer); + } else { + this.sendBuffer(buffer, data.sequence, data.timestamp); + } + + const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now()); + this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), nextTime); + } catch (e) { + this.destroy('error', e); + } + } + + readStreamBuffer() { + const data = this.streamingData; + const bufferLength = (this._opus ? 80 : 1920) * data.channels; + let buffer = this.stream.read(bufferLength); + if (this._opus) return buffer; + if (!buffer) return null; + + if (buffer.length !== bufferLength) { + const newBuffer = Buffer.alloc(bufferLength).fill(0); + buffer.copy(newBuffer); + buffer = newBuffer; + } + + buffer = this.applyVolume(buffer); + return buffer; + } + + started() { + const data = this.streamingData; + + if (!data.startTime) { + /** + * Emitted once the dispatcher starts streaming. + * @event StreamDispatcher#start + */ + this.emit('start'); + data.startTime = Date.now(); + } + } + + stepStreamingData() { + const data = this.streamingData; + data.count++; + data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0; + data.timestamp = (data.timestamp + 960) < 4294967295 ? data.timestamp + 960 : 0; + } + + destroy(type, reason) { + if (this.destroyed) return; + this.destroyed = true; + this.setSpeaking(false); + this.emit(type, reason); + /** + * Emitted once the dispatcher ends. + * @param {string} [reason] The reason the dispatcher ended + * @event StreamDispatcher#end + */ + if (type !== 'end') this.emit('end', `destroyed due to ${type} - ${reason}`); + } + + startStreaming() { + if (!this.stream) { + /** + * Emitted if the dispatcher encounters an error. + * @event StreamDispatcher#error + * @param {string} error The error message + */ + this.emit('error', 'No stream'); + return; + } + + this.stream.on('end', err => this.destroy('end', err || 'stream')); + this.stream.on('error', err => this.destroy('error', err)); + + const data = this.streamingData; + data.length = 20; + data.missed = 0; + + this.stream.once('readable', () => { + data.startTime = null; + data.count = 0; + this.process(); + }); + } + + setPaused(paused) { this.setSpeaking(!(this.paused = paused)); } +} + +module.exports = StreamDispatcher; diff --git a/node_modules/discord.js/src/client/voice/opus/BaseOpusEngine.js b/node_modules/discord.js/src/client/voice/opus/BaseOpusEngine.js new file mode 100644 index 0000000..a510449 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/BaseOpusEngine.js @@ -0,0 +1,60 @@ +/** + * The base opus encoding engine. + * @private + */ +class BaseOpus { + /** + * @param {Object} [options] The options to apply to the Opus engine + * @param {number} [options.bitrate=48] The desired bitrate (kbps) + * @param {boolean} [options.fec=false] Whether to enable forward error correction + * @param {number} [options.plp=0] The expected packet loss percentage + */ + constructor({ bitrate = 48, fec = false, plp = 0 } = {}) { + this.ctl = { + BITRATE: 4002, + FEC: 4012, + PLP: 4014, + }; + + this.samplingRate = 48000; + this.channels = 2; + + /** + * The desired bitrate (kbps) + * @type {number} + */ + this.bitrate = bitrate; + + /** + * Miscellaneous Opus options + * @type {Object} + */ + this.options = { fec, plp }; + } + + init() { + try { + this.setBitrate(this.bitrate); + + // Set FEC (forward error correction) + if (this.options.fec) this.setFEC(this.options.fec); + + // Set PLP (expected packet loss percentage) + if (this.options.plp) this.setPLP(this.options.plp); + } catch (err) { + // Opus engine likely has no support for libopus CTL + } + } + + encode(buffer) { + return buffer; + } + + decode(buffer) { + return buffer; + } + + destroy() {} // eslint-disable-line no-empty-function +} + +module.exports = BaseOpus; diff --git a/node_modules/discord.js/src/client/voice/opus/DiscordJsOpusEngine.js b/node_modules/discord.js/src/client/voice/opus/DiscordJsOpusEngine.js new file mode 100644 index 0000000..a3759c2 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/DiscordJsOpusEngine.js @@ -0,0 +1,34 @@ +const OpusEngine = require('./BaseOpusEngine'); + +class DiscordJsOpusEngine extends OpusEngine { + constructor(player) { + super(player); + const opus = require('@discordjs/opus'); + this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels); + super.init(); + } + + setBitrate(bitrate) { + this.encoder.setBitrate(Math.min(128, Math.max(16, bitrate)) * 1000); + } + + setFEC(enabled) { + this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0); + } + + setPLP(percent) { + this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100))); + } + + encode(buffer) { + super.encode(buffer); + return this.encoder.encode(buffer, 1920); + } + + decode(buffer) { + super.decode(buffer); + return this.encoder.decode(buffer, 1920); + } +} + +module.exports = DiscordJsOpusEngine; diff --git a/node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js b/node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js new file mode 100644 index 0000000..a9ffc55 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js @@ -0,0 +1,34 @@ +const OpusEngine = require('./BaseOpusEngine'); + +class NodeOpusEngine extends OpusEngine { + constructor(player) { + super(player); + const opus = require('node-opus'); + this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels); + super.init(); + } + + setBitrate(bitrate) { + this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + + setFEC(enabled) { + this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0); + } + + setPLP(percent) { + this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100))); + } + + encode(buffer) { + super.encode(buffer); + return this.encoder.encode(buffer, 1920); + } + + decode(buffer) { + super.decode(buffer); + return this.encoder.decode(buffer, 1920); + } +} + +module.exports = NodeOpusEngine; diff --git a/node_modules/discord.js/src/client/voice/opus/OpusEngineList.js b/node_modules/discord.js/src/client/voice/opus/OpusEngineList.js new file mode 100644 index 0000000..cf63fbc --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/OpusEngineList.js @@ -0,0 +1,29 @@ +const list = [ + require('./DiscordJsOpusEngine'), + require('./NodeOpusEngine'), + require('./OpusScriptEngine'), +]; + +function fetch(Encoder, engineOptions) { + try { + return new Encoder(engineOptions); + } catch (err) { + if (err.message.includes('Cannot find module')) return null; + + // The Opus engine exists, but another error occurred. + throw err; + } +} + +exports.add = encoder => { + list.push(encoder); +}; + +exports.fetch = engineOptions => { + for (const encoder of list) { + const fetched = fetch(encoder, engineOptions); + if (fetched) return fetched; + } + + throw new Error('Couldn\'t find an Opus engine.'); +}; diff --git a/node_modules/discord.js/src/client/voice/opus/OpusScriptEngine.js b/node_modules/discord.js/src/client/voice/opus/OpusScriptEngine.js new file mode 100644 index 0000000..271a068 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/OpusScriptEngine.js @@ -0,0 +1,39 @@ +const OpusEngine = require('./BaseOpusEngine'); + +class OpusScriptEngine extends OpusEngine { + constructor(player) { + super(player); + const OpusScript = require('opusscript'); + this.encoder = new OpusScript(this.samplingRate, this.channels); + super.init(); + } + + setBitrate(bitrate) { + this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + + setFEC(enabled) { + this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0); + } + + setPLP(percent) { + this.encoder.encoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100))); + } + + encode(buffer) { + super.encode(buffer); + return this.encoder.encode(buffer, 960); + } + + decode(buffer) { + super.decode(buffer); + return this.encoder.decode(buffer); + } + + destroy() { + super.destroy(); + this.encoder.delete(); + } +} + +module.exports = OpusScriptEngine; 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..9b30c85 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/AudioPlayer.js @@ -0,0 +1,170 @@ +const EventEmitter = require('events').EventEmitter; +const Prism = require('prism-media'); +const StreamDispatcher = require('../dispatcher/StreamDispatcher'); +const Collection = require('../../../util/Collection'); +const OpusEncoders = require('../opus/OpusEngineList'); + +const ffmpegArguments = [ + '-analyzeduration', '0', + '-loglevel', '0', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', +]; + +/** + * An Audio Player for a Voice Connection. + * @private + * @extends {EventEmitter} + */ +class AudioPlayer extends EventEmitter { + constructor(voiceConnection) { + super(); + /** + * The voice connection that the player serves + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + /** + * The prism transcoder that the player uses + * @type {Prism} + */ + this.prism = new Prism(); + this.streams = new Collection(); + this.currentStream = {}; + this.streamingData = { + channels: 2, + count: 0, + sequence: 0, + timestamp: 0, + pausedTime: 0, + }; + this.voiceConnection.once('closing', () => this.destroyCurrentStream()); + } + + /** + * The current transcoder + * @type {?Object} + * @readonly + */ + get transcoder() { + return this.currentStream.transcoder; + } + + /** + * The current dispatcher + * @type {?StreamDispatcher} + * @readonly + */ + get dispatcher() { + return this.currentStream.dispatcher; + } + + destroy() { + if (this.opusEncoder) this.opusEncoder.destroy(); + this.opusEncoder = null; + } + + destroyCurrentStream() { + const transcoder = this.transcoder; + const dispatcher = this.dispatcher; + if (transcoder) transcoder.kill(); + if (dispatcher) { + const end = dispatcher.listeners('end')[0]; + const error = dispatcher.listeners('error')[0]; + if (end) dispatcher.removeListener('end', end); + if (error) dispatcher.removeListener('error', error); + dispatcher.destroy('end'); + } + this.currentStream = {}; + } + + /** + * Set the bitrate of the current Opus encoder. + * @param {number} value New bitrate, in kbps + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(value) { + if (!value) return; + if (!this.opusEncoder) return; + const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value; + this.opusEncoder.setBitrate(bitrate); + } + + playUnknownStream(stream, options = {}) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); + const transcoder = this.prism.transcode({ + type: 'ffmpeg', + media: stream, + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), + }); + this.destroyCurrentStream(); + this.currentStream = { + transcoder: transcoder, + output: transcoder.output, + input: stream, + }; + transcoder.on('error', e => { + this.destroyCurrentStream(); + if (this.listenerCount('error') > 0) this.emit('error', e); + this.emit('warn', `prism transcoder error - ${e}`); + }); + return this.playPCMStream(transcoder.output, options, true); + } + + playPCMStream(stream, options = {}, fromUnknown = false) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); + this.setBitrate(options.bitrate); + const dispatcher = this.createDispatcher(stream, options); + if (fromUnknown) { + this.currentStream.dispatcher = dispatcher; + } else { + this.destroyCurrentStream(); + this.currentStream = { + dispatcher, + input: stream, + output: stream, + }; + } + return dispatcher; + } + + playOpusStream(stream, options = {}) { + options.opus = true; + this.destroyCurrentStream(); + const dispatcher = this.createDispatcher(stream, options); + this.currentStream = { + dispatcher, + input: stream, + output: stream, + }; + return dispatcher; + } + + playBroadcast(broadcast, options) { + this.destroyCurrentStream(); + const dispatcher = this.createDispatcher(broadcast, options); + this.currentStream = { + dispatcher, + broadcast, + input: broadcast, + output: broadcast, + }; + broadcast.registerDispatcher(dispatcher); + return dispatcher; + } + + createDispatcher(stream, { seek = 0, volume = 1, passes = 1, opus } = {}) { + const options = { seek, volume, passes, opus }; + + const dispatcher = new StreamDispatcher(this, stream, options); + dispatcher.on('end', () => this.destroyCurrentStream()); + dispatcher.on('error', () => this.destroyCurrentStream()); + dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); + return dispatcher; + } +} + +module.exports = AudioPlayer; diff --git a/node_modules/discord.js/src/client/voice/receiver/VoiceReadable.js b/node_modules/discord.js/src/client/voice/receiver/VoiceReadable.js new file mode 100644 index 0000000..d349428 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/VoiceReadable.js @@ -0,0 +1,17 @@ +const Readable = require('stream').Readable; + +class VoiceReadable extends Readable { + constructor() { + super(); + this._packets = []; + this.open = true; + } + + _read() {} // eslint-disable-line no-empty-function + + _push(d) { + if (this.open) this.push(d); + } +} + +module.exports = VoiceReadable; diff --git a/node_modules/discord.js/src/client/voice/receiver/VoiceReceiver.js b/node_modules/discord.js/src/client/voice/receiver/VoiceReceiver.js new file mode 100644 index 0000000..e37b273 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/VoiceReceiver.js @@ -0,0 +1,220 @@ +const EventEmitter = require('events').EventEmitter; +const secretbox = require('../util/Secretbox'); +const Readable = require('./VoiceReadable'); +const OpusEncoders = require('../opus/OpusEngineList'); + +const nonce = Buffer.alloc(24); +nonce.fill(0); + +/** + * Receives voice data from a voice connection. + * ```js + * // Obtained using: + * voiceChannel.join() + * .then(connection => { + * const receiver = connection.createReceiver(); + * }); + * ``` + * @extends {EventEmitter} + */ +class VoiceReceiver extends EventEmitter { + constructor(connection) { + super(); + /* + Need a queue because we don't get the ssrc of the user speaking until after the first few packets, + so we queue up unknown SSRCs until they become known, then empty the queue + */ + this.queues = new Map(); + this.pcmStreams = new Map(); + this.opusStreams = new Map(); + this.opusEncoders = new Map(); + + /** + * Whether or not this receiver has been destroyed + * @type {boolean} + */ + this.destroyed = false; + + /** + * The VoiceConnection that instantiated this + * @type {VoiceConnection} + */ + this.voiceConnection = connection; + + this._listener = msg => { + const ssrc = +msg.readUInt32BE(8).toString(10); + const user = connection.client.users.get(connection.ssrcMap.get(ssrc)); + if (!user) { + if (!this.queues.has(ssrc)) this.queues.set(ssrc, []); + this.queues.get(ssrc).push(msg); + } else { + if (this.queues.get(ssrc)) { + this.queues.get(ssrc).push(msg); + this.queues.get(ssrc).map(m => this.handlePacket(m, user)); + this.queues.delete(ssrc); + return; + } + this.handlePacket(msg, user); + } + }; + this.voiceConnection.sockets.udp.socket.on('message', this._listener); + } + + /** + * If this VoiceReceiver has been destroyed, running `recreate()` will recreate the listener. + * This avoids you having to create a new receiver. + * <info>Any streams that you had prior to destroying the receiver will not be recreated.</info> + */ + recreate() { + if (!this.destroyed) return; + this.voiceConnection.sockets.udp.socket.on('message', this._listener); + this.destroyed = false; + } + + /** + * Destroy this VoiceReceiver, also ending any streams that it may be controlling. + */ + destroy() { + this.voiceConnection.sockets.udp.socket.removeListener('message', this._listener); + for (const [id, stream] of this.pcmStreams) { + stream._push(null); + this.pcmStreams.delete(id); + } + for (const [id, stream] of this.opusStreams) { + stream._push(null); + this.opusStreams.delete(id); + } + for (const [id, encoder] of this.opusEncoders) { + encoder.destroy(); + this.opusEncoders.delete(id); + } + this.destroyed = true; + } + + /** + * Invoked when a user stops speaking. + * @param {User} user The user that stopped speaking + * @private + */ + stoppedSpeaking(user) { + const opusStream = this.opusStreams.get(user.id); + const pcmStream = this.pcmStreams.get(user.id); + const opusEncoder = this.opusEncoders.get(user.id); + if (opusStream) { + opusStream.push(null); + opusStream.open = false; + this.opusStreams.delete(user.id); + } + if (pcmStream) { + pcmStream.push(null); + pcmStream.open = false; + this.pcmStreams.delete(user.id); + } + if (opusEncoder) { + opusEncoder.destroy(); + this.opusEncoders.delete(user.id); + } + } + + /** + * Creates a readable stream for a user that provides opus data while the user is speaking. When the user + * stops speaking, the stream is destroyed. + * @param {UserResolvable} user The user to create the stream for + * @returns {ReadableStream} + */ + createOpusStream(user) { + user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); + if (!user) throw new Error('Couldn\'t resolve the user to create Opus stream.'); + if (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.'); + const stream = new Readable(); + this.opusStreams.set(user.id, stream); + return stream; + } + + /** + * Creates a readable stream for a user that provides PCM data while the user is speaking. When the user + * stops speaking, the stream is destroyed. The stream is 16-bit signed stereo PCM at 48KHz. + * @param {UserResolvable} user The user to create the stream for + * @returns {ReadableStream} + */ + createPCMStream(user) { + user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); + if (!user) throw new Error('Couldn\'t resolve the user to create PCM stream.'); + if (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.'); + const stream = new Readable(); + this.pcmStreams.set(user.id, stream); + return stream; + } + + handlePacket(msg, user) { + msg.copy(nonce, 0, 0, 12); + let data = secretbox.methods.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key); + if (!data) { + /** + * Emitted whenever a voice packet experiences a problem. + * @event VoiceReceiver#warn + * @param {string} reason The reason for the warning. If it happened because the voice packet could not be + * decrypted, this would be `decrypt`. If it happened because the voice packet could not be decoded into + * PCM, this would be `decode` + * @param {string} message The warning message + */ + this.emit('warn', 'decrypt', 'Failed to decrypt voice packet'); + return; + } + data = Buffer.from(data); + + // Strip RTP Header Extensions (one-byte only) + if (data[0] === 0xBE && data[1] === 0xDE && data.length > 4) { + const headerExtensionLength = data.readUInt16BE(2); + let offset = 4; + for (let i = 0; i < headerExtensionLength; i++) { + const byte = data[offset]; + offset++; + if (byte === 0) { + continue; + } + offset += 1 + (0b1111 & (byte >> 4)); + } + // Skip over undocumented Discord byte + offset++; + + data = data.slice(offset); + } + + if (this.opusStreams.get(user.id)) this.opusStreams.get(user.id)._push(data); + /** + * Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM). + * @event VoiceReceiver#opus + * @param {User} user The user that is sending the buffer (is speaking) + * @param {Buffer} buffer The opus buffer + */ + this.emit('opus', user, data); + if (this.listenerCount('pcm') > 0 || this.pcmStreams.size > 0) { + if (!this.opusEncoders.get(user.id)) this.opusEncoders.set(user.id, OpusEncoders.fetch()); + const { pcm, error } = VoiceReceiver._tryDecode(this.opusEncoders.get(user.id), data); + if (error) { + this.emit('warn', 'decode', `Failed to decode packet voice to PCM because: ${error.message}`); + return; + } + if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm); + /** + * Emits decoded voice data when it's received. For performance reasons, the decoding will only + * happen if there is at least one `pcm` listener on this receiver. + * @event VoiceReceiver#pcm + * @param {User} user The user that is sending the buffer (is speaking) + * @param {Buffer} buffer The decoded buffer + */ + this.emit('pcm', user, pcm); + } + } + + static _tryDecode(encoder, data) { + try { + return { pcm: encoder.decode(data) }; + } catch (error) { + return { error }; + } + } +} + +module.exports = VoiceReceiver; diff --git a/node_modules/discord.js/src/client/voice/util/SecretKey.js b/node_modules/discord.js/src/client/voice/util/SecretKey.js new file mode 100644 index 0000000..670e9e5 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/SecretKey.js @@ -0,0 +1,16 @@ +/** + * Represents a Secret Key used in encryption over voice. + * @private + */ +class SecretKey { + constructor(key) { + /** + * The key used for encryption + * @type {Uint8Array} + */ + this.key = new Uint8Array(new ArrayBuffer(key.length)); + for (const index of Object.keys(key)) this.key[index] = key[index]; + } +} + +module.exports = SecretKey; 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..a833d51 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/Secretbox.js @@ -0,0 +1,33 @@ +const libs = { + sodium: sodium => ({ + open: sodium.api.crypto_secretbox_open_easy, + close: sodium.api.crypto_secretbox_easy, + }), + 'libsodium-wrappers': sodium => ({ + open: sodium.crypto_secretbox_open_easy, + close: sodium.crypto_secretbox_easy, + }), + tweetnacl: tweetnacl => ({ + open: tweetnacl.secretbox.open, + close: tweetnacl.secretbox, + }), +}; + +exports.methods = {}; + +for (const libName of Object.keys(libs)) { + try { + const lib = require(libName); + if (libName === 'libsodium-wrappers' && lib.ready) { + lib.ready.then(() => { + exports.methods = libs[libName](lib); + }).catch(() => { + const tweetnacl = require('tweetnacl'); + exports.methods = libs.tweetnacl(tweetnacl); + }).catch(() => undefined); + } else { + exports.methods = libs[libName](lib); + } + break; + } catch (err) {} // 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..239ceb4 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/Silence.js @@ -0,0 +1,16 @@ +const { Readable } = require('stream'); + +const SILENCE_FRAME = Buffer.from([0xF8, 0xFF, 0xFE]); + +/** + * A readable emitting silent opus frames. + * @extends {Readable} + * @private + */ +class Silence extends Readable { + _read() { + this.push(SILENCE_FRAME); + } +} + +module.exports = Silence; diff --git a/node_modules/discord.js/src/client/voice/util/SingleSilence.js b/node_modules/discord.js/src/client/voice/util/SingleSilence.js new file mode 100644 index 0000000..b59341e --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/SingleSilence.js @@ -0,0 +1,17 @@ +const Silence = require('./Silence'); + +/** + * Only emits a single silent opus frame. + * This is used as a workaround for Discord now requiring + * silence to be sent before being able to receive audio. + * @extends {Silence} + * @private + */ +class SingleSilence extends Silence { + _read() { + super._read(); + this.push(null); + } +} + +module.exports = SingleSilence; 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..62e6da5 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/VolumeInterface.js @@ -0,0 +1,86 @@ +const EventEmitter = require('events'); + +/** + * An interface class for volume transformation. + * @extends {EventEmitter} + */ +class VolumeInterface extends EventEmitter { + constructor({ volume = 1 } = {}) { + super(); + this.setVolume(volume); + } + + /** + * The current volume of the broadcast + * @readonly + * @type {number} + */ + get volume() { + return this._volume; + } + + /** + * The current volume of the broadcast in decibels + * @readonly + * @type {number} + */ + get volumeDecibels() { + return Math.log10(this._volume) * 20; + } + + /** + * The current volume of the broadcast from a logarithmic scale + * @readonly + * @type {number} + */ + 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; + } + + /** + * Set the volume in decibels. + * @param {number} db The decibels + */ + setVolumeDecibels(db) { + this.setVolume(Math.pow(10, db / 20)); + } + + /** + * Set 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)); + } +} + +module.exports = VolumeInterface; diff --git a/node_modules/discord.js/src/client/websocket/WebSocketConnection.js b/node_modules/discord.js/src/client/websocket/WebSocketConnection.js new file mode 100644 index 0000000..4ea078b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/WebSocketConnection.js @@ -0,0 +1,509 @@ +const browser = typeof window !== 'undefined'; +const EventEmitter = require('events'); +const Constants = require('../../util/Constants'); +const zlib = require('zlib'); +const PacketManager = require('./packets/WebSocketPacketManager'); +const erlpack = (function findErlpack() { + try { + const e = require('erlpack'); + if (!e.pack) return null; + return e; + } catch (e) { + return null; + } +}()); + +const WebSocket = (function findWebSocket() { + if (browser) return window.WebSocket; // eslint-disable-line no-undef + try { + const uws = require('@discordjs/uws'); + process.emitWarning('uws support is being removed in the next version of discord.js', + 'DeprecationWarning', findWebSocket); + return uws; + } catch (e) { + return require('ws'); + } +}()); + +/** + * Abstracts a WebSocket connection with decoding/encoding for the Discord gateway. + * @private + */ +class WebSocketConnection extends EventEmitter { + /** + * @param {WebSocketManager} manager The WebSocket manager + * @param {string} gateway The WebSocket gateway to connect to + */ + constructor(manager, gateway) { + super(); + /** + * The WebSocket Manager of this connection + * @type {WebSocketManager} + */ + this.manager = manager; + + /** + * The client this belongs to + * @type {Client} + */ + this.client = manager.client; + + /** + * The WebSocket connection itself + * @type {WebSocket} + */ + this.ws = null; + + /** + * The current sequence of the WebSocket + * @type {number} + */ + this.sequence = -1; + + /** + * The current status of the client + * @type {Status} + */ + this.status = Constants.Status.IDLE; + + /** + * The Packet Manager of the connection + * @type {WebSocketPacketManager} + */ + this.packetManager = new PacketManager(this); + + /** + * The last time a ping was sent (a timestamp) + * @type {number} + */ + this.lastPingTimestamp = 0; + + /** + * Contains the rate limit queue and metadata + * @type {Object} + */ + this.ratelimit = { + queue: [], + remaining: 120, + total: 120, + time: 60e3, + resetTimer: null, + }; + this.connect(gateway); + + /** + * Events that are disabled (will not be processed) + * @type {Object} + */ + this.disabledEvents = {}; + + /** + * The sequence on WebSocket close + * @type {number} + */ + this.closeSequence = 0; + + /** + * Whether or not the WebSocket is expecting to be closed + * @type {boolean} + */ + this.expectingClose = false; + for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true; + } + + /** + * Causes the client to be marked as ready and emits the ready event. + * @returns {void} + */ + triggerReady() { + if (this.status === Constants.Status.READY) { + this.debug('Tried to mark self as ready, but already ready'); + return; + } + /** + * Emitted when the client becomes ready to start working. + * @event Client#ready + */ + this.status = Constants.Status.READY; + this.client.emit(Constants.Events.READY); + this.packetManager.handleQueue(); + } + + /** + * Checks whether the client is ready to be marked as ready. + * @returns {void} + */ + checkIfReady() { + if (this.status === Constants.Status.READY || this.status === Constants.Status.NEARLY) return false; + let unavailableGuilds = 0; + for (const guild of this.client.guilds.values()) { + if (!guild.available) unavailableGuilds++; + } + if (unavailableGuilds === 0) { + this.status = Constants.Status.NEARLY; + if (!this.client.options.fetchAllMembers) return this.triggerReady(); + // Fetch all members before marking self as ready + const promises = this.client.guilds.map(g => g.fetchMembers()); + Promise.all(promises) + .then(() => this.triggerReady()) + .catch(e => { + this.debug(`Failed to fetch all members before ready! ${e}`); + this.triggerReady(); + }); + } + return true; + } + + // Util + /** + * Emits a debug message. + * @param {string} message Debug message + * @returns {void} + */ + debug(message) { + if (message instanceof Error) message = message.stack; + return this.manager.debug(`[connection] ${message}`); + } + + /** + * Attempts to serialise data from the WebSocket. + * @param {string|Object} data Data to unpack + * @returns {Object} + */ + unpack(data) { + if (data instanceof ArrayBuffer) data = Buffer.from(new Uint8Array(data)); + + if (erlpack && typeof data !== 'string') return erlpack.unpack(data); + else if (data instanceof Buffer) data = zlib.inflateSync(data).toString(); + + return JSON.parse(data); + } + + /** + * Packs an object ready to be sent. + * @param {Object} data Data to pack + * @returns {string|Buffer} + */ + pack(data) { + return erlpack ? erlpack.pack(data) : JSON.stringify(data); + } + + /** + * Processes the current WebSocket queue. + */ + processQueue() { + if (this.ratelimit.remaining === 0) return; + if (this.ratelimit.queue.length === 0) return; + if (this.ratelimit.remaining === this.ratelimit.total) { + this.ratelimit.resetTimer = this.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--; + } + } + + /** + * Sends data, bypassing the queue. + * @param {Object} data Packet to send + * @returns {void} + */ + _send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); + return; + } + this.ws.send(this.pack(data)); + } + + /** + * Adds data to the queue to be sent. + * @param {Object} data Packet to send + * @returns {void} + */ + send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); + return; + } + this.ratelimit.queue.push(data); + this.processQueue(); + } + + /** + * Creates a connection to a gateway. + * @param {string} gateway The gateway to connect to + * @param {number} [after=0] How long to wait before connecting + * @param {boolean} [force=false] Whether or not to force a new connection even if one already exists + * @returns {boolean} + */ + connect(gateway = this.gateway, after = 0, force = false) { + if (after) return this.client.setTimeout(() => this.connect(gateway, 0, force), after); // eslint-disable-line + if (this.ws && !force) { + this.debug('WebSocket connection already exists'); + return false; + } else if (typeof gateway !== 'string') { + this.debug(`Tried to connect to an invalid gateway: ${gateway}`); + return false; + } + this.expectingClose = false; + this.gateway = gateway; + this.debug(`Connecting to ${gateway}`); + const ws = this.ws = new WebSocket(gateway); + if (browser) ws.binaryType = 'arraybuffer'; + ws.onmessage = this.onMessage.bind(this); + ws.onopen = this.onOpen.bind(this); + ws.onerror = this.onError.bind(this); + ws.onclose = this.onClose.bind(this); + this.status = Constants.Status.CONNECTING; + return true; + } + + /** + * Destroys the connection. + * @returns {boolean} + */ + destroy() { + const ws = this.ws; + if (!ws) { + this.debug('Attempted to destroy WebSocket but no connection exists!'); + return false; + } + this.heartbeat(-1); + this.expectingClose = true; + ws.close(1000); + this.packetManager.handleQueue(); + this.ws = null; + this.status = Constants.Status.DISCONNECTED; + this.ratelimit.remaining = this.ratelimit.total; + return true; + } + + /** + * Called whenever a message is received. + * @param {Event} event Event received + * @returns {boolean} + */ + onMessage(event) { + let data; + try { + data = this.unpack(event.data); + } catch (err) { + this.emit('debug', err); + } + return this.onPacket(data); + } + + /** + * Sets the current sequence of the connection. + * @param {number} s New sequence + */ + setSequence(s) { + this.sequence = s > this.sequence ? s : this.sequence; + } + + /** + * Called whenever a packet is received. + * @param {Object} packet Received packet + * @returns {boolean} + */ + onPacket(packet) { + if (!packet) { + this.debug('Received null packet'); + return false; + } + this.client.emit('raw', packet); + switch (packet.op) { + case Constants.OPCodes.HELLO: + return this.heartbeat(packet.d.heartbeat_interval); + case Constants.OPCodes.RECONNECT: + return this.reconnect(); + case Constants.OPCodes.INVALID_SESSION: + if (!packet.d) this.sessionID = null; + this.sequence = -1; + this.debug('Session invalidated -- will identify with a new session'); + return this.identify(packet.d ? 2500 : 0); + case Constants.OPCodes.HEARTBEAT_ACK: + return this.ackHeartbeat(); + case Constants.OPCodes.HEARTBEAT: + return this.heartbeat(); + default: + return this.packetManager.handle(packet); + } + } + + /** + * Called whenever a connection is opened to the gateway. + * @param {Event} event Received open event + */ + onOpen(event) { + if (event && event.target && event.target.url) this.gateway = event.target.url; + this.debug(`Connected to gateway ${this.gateway}`); + this.identify(); + } + + /** + * Causes a reconnection to the gateway. + */ + reconnect() { + this.debug('Attemping to reconnect in 5500ms...'); + /** + * Emitted whenever the client tries to reconnect to the WebSocket. + * @event Client#reconnecting + */ + this.client.emit(Constants.Events.RECONNECTING); + this.connect(this.gateway, 5500, true); + } + + /** + * Called whenever an error occurs with the WebSocket. + * @param {Error} error The error that occurred + */ + onError(error) { + if (error && error.message === 'uWs client connection error') { + this.reconnect(); + return; + } + /** + * Emitted whenever the client's WebSocket encounters a connection error. + * @event Client#error + * @param {Error} error The encountered error + */ + this.client.emit(Constants.Events.ERROR, error); + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * Called whenever a connection to the gateway is closed. + * @param {CloseEvent} event Close event that was received + */ + onClose(event) { + this.debug(`${this.expectingClose ? 'Client' : 'Server'} closed the WebSocket connection: ${event.code}`); + this.closeSequence = this.sequence; + // Reset the state before trying to fix anything + this.emit('close', event); + this.heartbeat(-1); + // Should we reconnect? + if (event.code === 1000 ? this.expectingClose : Constants.WSCodes[event.code]) { + this.expectingClose = false; + /** + * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. + * @event Client#disconnect + * @param {CloseEvent} event The WebSocket close event + */ + this.client.emit(Constants.Events.DISCONNECT, event); + this.debug(Constants.WSCodes[event.code]); + this.destroy(); + return; + } + this.expectingClose = false; + this.reconnect(); + } + + // Heartbeat + /** + * Acknowledges a heartbeat. + */ + ackHeartbeat() { + this.debug(`Heartbeat acknowledged, latency of ${Date.now() - this.lastPingTimestamp}ms`); + this.client._pong(this.lastPingTimestamp); + } + + /** + * Sends a heartbeat or sets an interval for sending heartbeats. + * @param {number} [time] If -1, clears the interval, any other number sets an interval + * If no value is given, a heartbeat will be sent instantly + */ + heartbeat(time) { + if (!isNaN(time)) { + if (time === -1) { + this.debug('Clearing heartbeat interval'); + this.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } else { + this.debug(`Setting a heartbeat interval for ${time}ms`); + this.heartbeatInterval = this.client.setInterval(() => this.heartbeat(), time); + } + return; + } + this.debug('Sending a heartbeat'); + this.lastPingTimestamp = Date.now(); + this.send({ + op: Constants.OPCodes.HEARTBEAT, + d: this.sequence, + }); + } + + // Identification + /** + * Identifies the client on a connection. + * @param {number} [after] How long to wait before identifying + * @returns {void} + */ + identify(after) { + if (after) return this.client.setTimeout(this.identify.bind(this), after); + return this.sessionID ? this.identifyResume() : this.identifyNew(); + } + + /** + * Identifies as a new connection on the gateway. + * @returns {void} + */ + identifyNew() { + if (!this.client.token) { + this.debug('No token available to identify a new session with'); + return; + } + // Clone the generic payload and assign the token + const d = Object.assign({ token: this.client.token }, this.client.options.ws); + + // Sharding stuff + const { shardId, shardCount } = this.client.options; + if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)]; + + // Send the payload + this.debug('Identifying as a new session'); + this.send({ op: Constants.OPCodes.IDENTIFY, d }); + } + + /** + * Resumes a session on the gateway. + * @returns {void} + */ + identifyResume() { + if (!this.sessionID) { + this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead'); + return this.identifyNew(); + } + this.debug(`Attempting to resume session ${this.sessionID}`); + + const d = { + token: this.client.token, + session_id: this.sessionID, + seq: this.sequence, + }; + + return this.send({ + op: Constants.OPCodes.RESUME, + d, + }); + } +} + +/** + * Encoding the WebSocket connections will use. + * @type {string} + */ +WebSocketConnection.ENCODING = erlpack ? 'etf' : 'json'; +WebSocketConnection.WebSocket = WebSocket; + +module.exports = WebSocketConnection; 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..9ef073f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/WebSocketManager.js @@ -0,0 +1,90 @@ +const EventEmitter = require('events').EventEmitter; +const Constants = require('../../util/Constants'); +const WebSocketConnection = require('./WebSocketConnection'); + +/** + * WebSocket Manager of the client. + * @private + */ +class WebSocketManager extends EventEmitter { + constructor(client) { + super(); + /** + * The client that instantiated this WebSocketManager + * @type {Client} + */ + this.client = client; + + /** + * The WebSocket connection of this manager + * @type {?WebSocketConnection} + */ + this.connection = null; + } + + /** + * Sends a heartbeat on the available connection. + * @returns {void} + */ + heartbeat() { + if (!this.connection) return this.debug('No connection to heartbeat'); + return this.connection.heartbeat(); + } + + /** + * Emits a debug event. + * @param {string} message Debug message + * @returns {void} + */ + debug(message) { + return this.client.emit('debug', `[ws] ${message}`); + } + + /** + * Destroy the client. + * @returns {void} Whether or not destruction was successful + */ + destroy() { + if (!this.connection) { + this.debug('Attempted to destroy WebSocket but no connection exists!'); + return false; + } + return this.connection.destroy(); + } + + /** + * Send a packet on the available WebSocket. + * @param {Object} packet Packet to send + * @returns {void} + */ + send(packet) { + if (!this.connection) { + this.debug('No connection to websocket'); + return; + } + this.connection.send(packet); + } + + /** + * Connects the client to a gateway. + * @param {string} gateway The gateway to connect to + * @returns {boolean} + */ + connect(gateway) { + if (!this.connection) { + this.connection = new WebSocketConnection(this, gateway); + return true; + } + switch (this.connection.status) { + case Constants.Status.IDLE: + case Constants.Status.DISCONNECTED: + this.connection.connect(gateway, 5500); + return true; + default: + this.debug(`Couldn't connect to ${gateway} as the websocket is at state ${this.connection.status}`); + return false; + } + } +} + +module.exports = WebSocketManager; diff --git a/node_modules/discord.js/src/client/websocket/packets/WebSocketPacketManager.js b/node_modules/discord.js/src/client/websocket/packets/WebSocketPacketManager.js new file mode 100644 index 0000000..c84b16b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/WebSocketPacketManager.js @@ -0,0 +1,113 @@ +const Constants = require('../../../util/Constants'); + +const BeforeReadyWhitelist = [ + Constants.WSEvents.READY, + Constants.WSEvents.RESUMED, + Constants.WSEvents.GUILD_CREATE, + Constants.WSEvents.GUILD_DELETE, + Constants.WSEvents.GUILD_MEMBERS_CHUNK, + Constants.WSEvents.GUILD_MEMBER_ADD, + Constants.WSEvents.GUILD_MEMBER_REMOVE, +]; + +class WebSocketPacketManager { + constructor(connection) { + this.ws = connection; + this.handlers = {}; + this.queue = []; + + this.register(Constants.WSEvents.READY, require('./handlers/Ready')); + this.register(Constants.WSEvents.RESUMED, require('./handlers/Resumed')); + this.register(Constants.WSEvents.GUILD_CREATE, require('./handlers/GuildCreate')); + this.register(Constants.WSEvents.GUILD_DELETE, require('./handlers/GuildDelete')); + this.register(Constants.WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate')); + this.register(Constants.WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd')); + this.register(Constants.WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove')); + this.register(Constants.WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd')); + this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove')); + this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate')); + this.register(Constants.WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate')); + this.register(Constants.WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete')); + this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate')); + this.register(Constants.WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate')); + this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk')); + this.register(Constants.WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate')); + this.register(Constants.WSEvents.INVITE_CREATE, require('./handlers/InviteCreate')); + this.register(Constants.WSEvents.INVITE_DELETE, require('./handlers/InviteDelete')); + this.register(Constants.WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate')); + this.register(Constants.WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete')); + this.register(Constants.WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate')); + this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate')); + this.register(Constants.WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate')); + this.register(Constants.WSEvents.USER_UPDATE, require('./handlers/UserUpdate')); + this.register(Constants.WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate')); + this.register(Constants.WSEvents.USER_SETTINGS_UPDATE, require('./handlers/UserSettingsUpdate')); + this.register(Constants.WSEvents.USER_GUILD_SETTINGS_UPDATE, require('./handlers/UserGuildSettingsUpdate')); + this.register(Constants.WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate')); + this.register(Constants.WSEvents.TYPING_START, require('./handlers/TypingStart')); + this.register(Constants.WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate')); + this.register(Constants.WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete')); + this.register(Constants.WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate')); + this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk')); + this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate')); + this.register(Constants.WSEvents.GUILD_SYNC, require('./handlers/GuildSync')); + this.register(Constants.WSEvents.RELATIONSHIP_ADD, require('./handlers/RelationshipAdd')); + this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, require('./handlers/RelationshipRemove')); + this.register(Constants.WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd')); + this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove')); + this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE_EMOJI, require('./handlers/MessageReactionRemoveEmoji')); + this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll')); + this.register(Constants.WSEvents.WEBHOOKS_UPDATE, require('./handlers/WebhooksUpdate')); + } + + get client() { + return this.ws.client; + } + + register(event, Handler) { + this.handlers[event] = new Handler(this); + } + + handleQueue() { + this.queue.forEach((element, index) => { + this.handle(this.queue[index], true); + this.queue.splice(index, 1); + }); + } + + handle(packet, queue = false) { + if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) { + this.ws.client._pong(this.ws.client._pingTimestamp); + this.ws.lastHeartbeatAck = true; + this.ws.client.emit('debug', 'Heartbeat acknowledged'); + } else if (packet.op === Constants.OPCodes.HEARTBEAT) { + this.client.ws.send({ + op: Constants.OPCodes.HEARTBEAT, + d: this.client.ws.sequence, + }); + this.ws.client.emit('debug', 'Received gateway heartbeat'); + } + + if (this.ws.status === Constants.Status.RECONNECTING) { + this.ws.reconnecting = false; + this.ws.checkIfReady(); + } + + this.ws.setSequence(packet.s); + + if (this.ws.disabledEvents[packet.t] !== undefined) return false; + + if (this.ws.status !== Constants.Status.READY) { + if (BeforeReadyWhitelist.indexOf(packet.t) === -1) { + this.queue.push(packet); + return false; + } + } + + if (!queue && this.queue.length > 0) this.handleQueue(); + if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet); + return false; + } +} + +module.exports = WebSocketPacketManager; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/AbstractHandler.js b/node_modules/discord.js/src/client/websocket/packets/handlers/AbstractHandler.js new file mode 100644 index 0000000..c1c2a5a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/AbstractHandler.js @@ -0,0 +1,11 @@ +class AbstractHandler { + constructor(packetManager) { + this.packetManager = packetManager; + } + + handle(packet) { + return packet; + } +} + +module.exports = AbstractHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelCreate.js new file mode 100644 index 0000000..04cb298 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelCreate.js @@ -0,0 +1,17 @@ +const AbstractHandler = require('./AbstractHandler'); + +class ChannelCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.ChannelCreate.handle(data); + } +} + +/** + * Emitted whenever a channel is created. + * @event Client#channelCreate + * @param {Channel} channel The channel that was created + */ + +module.exports = ChannelCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelDelete.js new file mode 100644 index 0000000..b25f585 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelDelete.js @@ -0,0 +1,20 @@ +const AbstractHandler = require('./AbstractHandler'); + +const Constants = require('../../../../util/Constants'); + +class ChannelDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.ChannelDelete.handle(data); + if (response.channel) client.emit(Constants.Events.CHANNEL_DELETE, response.channel); + } +} + +/** + * Emitted whenever a channel is deleted. + * @event Client#channelDelete + * @param {Channel} channel The channel that was deleted + */ + +module.exports = ChannelDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelPinsUpdate.js new file mode 100644 index 0000000..16ffe1c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelPinsUpdate.js @@ -0,0 +1,37 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +/* +{ t: 'CHANNEL_PINS_UPDATE', + s: 666, + op: 0, + d: + { last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00', + channel_id: '314866471639044027' } } +*/ + +class ChannelPinsUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const channel = client.channels.get(data.channel_id); + const time = new Date(data.last_pin_timestamp); + if (channel && time) { + // Discord sends null for last_pin_timestamp if the last pinned message was removed + channel.lastPinTimestamp = time.getTime() || null; + + client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time); + } + } +} + +/** + * 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. + * <warn>The `time` parameter will be a Unix Epoch Date object when there are no pins left.</warn> + * @event Client#channelPinsUpdate + * @param {Channel} channel The channel that the pins update occured in + * @param {Date} time The time when the last pinned message was pinned + */ + +module.exports = ChannelPinsUpdate; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelUpdate.js new file mode 100644 index 0000000..fa535b1 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class ChannelUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.ChannelUpdate.handle(data); + } +} + +module.exports = ChannelUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanAdd.js new file mode 100644 index 0000000..60ce72d --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanAdd.js @@ -0,0 +1,23 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class GuildBanAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + const user = client.users.get(data.user.id); + if (guild && user) client.emit(Constants.Events.GUILD_BAN_ADD, guild, 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 + */ + +module.exports = GuildBanAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanRemove.js new file mode 100644 index 0000000..c4edbde --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanRemove.js @@ -0,0 +1,20 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildBanRemoveHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildBanRemove.handle(data); + } +} + +/** + * 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 + */ + +module.exports = GuildBanRemoveHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildCreate.js new file mode 100644 index 0000000..d7c1803 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildCreate.js @@ -0,0 +1,22 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + const guild = client.guilds.get(data.id); + if (guild) { + if (!guild.available && !data.unavailable) { + // A newly available guild + guild.setup(data); + this.packetManager.ws.checkIfReady(); + } + } else { + // A new guild + client.dataManager.newGuild(data); + } + } +} + +module.exports = GuildCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildDelete.js new file mode 100644 index 0000000..35e3c53 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildDelete.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class GuildDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.GuildDelete.handle(data); + if (response.guild) client.emit(Constants.Events.GUILD_DELETE, response.guild); + } +} + +/** + * Emitted whenever a guild is deleted/left. + * @event Client#guildDelete + * @param {Guild} guild The guild that was deleted + */ + +module.exports = GuildDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildEmojisUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildEmojisUpdate.js new file mode 100644 index 0000000..2906e74 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildEmojisUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildEmojisUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildEmojisUpdate.handle(data); + } +} + +module.exports = GuildEmojisUpdate; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js new file mode 100644 index 0000000..5adfb5b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const { Events } = require('../../../../util/Constants'); + +class GuildIntegrationsHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsHandler; + +/** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberAdd.js new file mode 100644 index 0000000..d4d122f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberAdd.js @@ -0,0 +1,17 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildMemberAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (guild) { + guild.memberCount++; + guild._addMember(data); + } + } +} + +module.exports = GuildMemberAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberRemove.js new file mode 100644 index 0000000..6ec1bfe --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberRemove.js @@ -0,0 +1,13 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildMemberRemoveHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildMemberRemove.handle(data); + } +} + +module.exports = GuildMemberRemoveHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberUpdate.js new file mode 100644 index 0000000..94ac71f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberUpdate.js @@ -0,0 +1,18 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildMemberUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + const member = guild.members.get(data.user.id); + if (member) guild._updateMember(member, data); + } + } +} + +module.exports = GuildMemberUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMembersChunk.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMembersChunk.js new file mode 100644 index 0000000..4458644 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMembersChunk.js @@ -0,0 +1,33 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); +// Uncomment in v12 +// const Collection = require('../../../../util/Collection'); + +class GuildMembersChunkHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (!guild) return; + + // Uncomment in v12 + // const members = new Collection(); + // + // for (const member of data.members) members.set(member.id, guild._addMember(member, false)); + + const members = data.members.map(member => guild._addMember(member, false)); + + client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members, guild); + + client.ws.lastHeartbeatAck = true; + } +} + +/** + * Emitted whenever a chunk of guild members is received (all members come from the same guild). + * @event Client#guildMembersChunk + * @param {GuildMember[]} members The members in the chunk + * @param {Guild} guild The guild related to the member chunk + */ + +module.exports = GuildMembersChunkHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleCreate.js new file mode 100644 index 0000000..8581d53 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleCreate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildRoleCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildRoleCreate.handle(data); + } +} + +module.exports = GuildRoleCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleDelete.js new file mode 100644 index 0000000..63439b0 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleDelete.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildRoleDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildRoleDelete.handle(data); + } +} + +module.exports = GuildRoleDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleUpdate.js new file mode 100644 index 0000000..6fbdc10 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildRoleUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildRoleUpdate.handle(data); + } +} + +module.exports = GuildRoleUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildSync.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildSync.js new file mode 100644 index 0000000..0b9f5aa --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildSync.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildSyncHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildSync.handle(data); + } +} + +module.exports = GuildSyncHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildUpdate.js new file mode 100644 index 0000000..70eff52 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildUpdate.handle(data); + } +} + +module.exports = GuildUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/InviteCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteCreate.js new file mode 100644 index 0000000..00efb67 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteCreate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class InviteCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.InviteCreate.handle(data); + } +} + +module.exports = InviteCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/InviteDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteDelete.js new file mode 100644 index 0000000..0b8da42 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteDelete.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class InviteDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.InviteDelete.handle(data); + } +} + +module.exports = InviteDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageCreate.js new file mode 100644 index 0000000..beed34d --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageCreate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class MessageCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.MessageCreate.handle(data); + if (response.message) client.emit(Constants.Events.MESSAGE_CREATE, response.message); + } +} + +/** + * Emitted whenever a message is created. + * @event Client#message + * @param {Message} message The created message + */ + +module.exports = MessageCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDelete.js new file mode 100644 index 0000000..043c70a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDelete.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class MessageDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.MessageDelete.handle(data); + if (response.message) client.emit(Constants.Events.MESSAGE_DELETE, response.message); + } +} + +/** + * Emitted whenever a message is deleted. + * @event Client#messageDelete + * @param {Message} message The deleted message + */ + +module.exports = MessageDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDeleteBulk.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDeleteBulk.js new file mode 100644 index 0000000..db02df0 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDeleteBulk.js @@ -0,0 +1,17 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageDeleteBulkHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageDeleteBulk.handle(data); + } +} + +/** + * Emitted whenever messages are deleted in bulk. + * @event Client#messageDeleteBulk + * @param {Collection<Snowflake, Message>} messages The deleted messages, mapped by their ID + */ + +module.exports = MessageDeleteBulkHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionAdd.js new file mode 100644 index 0000000..a58db70 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionAdd.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionAdd.handle(data); + } +} + +module.exports = MessageReactionAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemove.js new file mode 100644 index 0000000..cddde70 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemove.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionRemove extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionRemove.handle(data); + } +} + +module.exports = MessageReactionRemove; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js new file mode 100644 index 0000000..303da9c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionRemoveAll extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionRemoveAll.handle(data); + } +} + +module.exports = MessageReactionRemoveAll; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveEmoji.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveEmoji.js new file mode 100644 index 0000000..c16af74 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveEmoji.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionRemoveEmoji extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionRemoveEmoji.handle(data); + } +} + +module.exports = MessageReactionRemoveEmoji; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageUpdate.js new file mode 100644 index 0000000..527632d --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageUpdate.handle(data); + } +} + +module.exports = MessageUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/PresenceUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/PresenceUpdate.js new file mode 100644 index 0000000..8bcf659 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/PresenceUpdate.js @@ -0,0 +1,76 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); +const Util = require('../../../../util/Util'); + +class PresenceUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + let user = client.users.get(data.user.id); + const guild = client.guilds.get(data.guild_id); + + // Step 1 + if (!user) { + if (data.user.username) { + user = client.dataManager.newUser(data.user); + } else { + return; + } + } + + const oldUser = Util.cloneObject(user); + user.patch(data.user); + if (!user.equals(oldUser)) { + client.emit(Constants.Events.USER_UPDATE, oldUser, user); + } + + if (guild) { + let member = guild.members.get(user.id); + if (!member && data.status !== 'offline') { + member = guild._addMember({ + user, + roles: data.roles, + deaf: false, + mute: false, + }, false); + client.emit(Constants.Events.GUILD_MEMBER_AVAILABLE, member); + } + if (member) { + if (client.listenerCount(Constants.Events.PRESENCE_UPDATE) === 0) { + guild._setPresence(user.id, data); + return; + } + const oldMember = Util.cloneObject(member); + if (member.presence) { + oldMember.frozenPresence = Util.cloneObject(member.presence); + } + guild._setPresence(user.id, data); + client.emit(Constants.Events.PRESENCE_UPDATE, oldMember, member); + } else { + guild._setPresence(user.id, data); + } + } + } +} + +/** + * Emitted whenever a guild member's presence changes, or they change one of their details. + * @event Client#presenceUpdate + * @param {GuildMember} oldMember The member before the presence update + * @param {GuildMember} newMember The member after the presence update + */ + +/** + * 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 + */ + +/** + * Emitted whenever a member becomes available in a large guild. + * @event Client#guildMemberAvailable + * @param {GuildMember} member The member that became available + */ + +module.exports = PresenceUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/Ready.js b/node_modules/discord.js/src/client/websocket/packets/handlers/Ready.js new file mode 100644 index 0000000..3eb80a2 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/Ready.js @@ -0,0 +1,84 @@ +const AbstractHandler = require('./AbstractHandler'); + +const ClientUser = require('../../../../structures/ClientUser'); + +class ReadyHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + client.ws.heartbeat(); + + data.user.user_settings = data.user_settings; + data.user.user_guild_settings = data.user_guild_settings; + + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.readyAt = new Date(); + client.users.set(clientUser.id, clientUser); + + for (const guild of data.guilds) if (!client.guilds.has(guild.id)) client.dataManager.newGuild(guild); + for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM); + + for (const relation of data.relationships) { + const user = client.dataManager.newUser(relation.user); + if (relation.type === 1) { + client.user.friends.set(user.id, user); + } else if (relation.type === 2) { + client.user.blocked.set(user.id, user); + } + } + + data.presences = data.presences || []; + for (const presence of data.presences) { + client.dataManager.newUser(presence.user); + client._setPresence(presence.user.id, presence); + } + + if (data.notes) { + for (const user of Object.keys(data.notes)) { + let note = data.notes[user]; + if (!note.length) note = null; + + client.user.notes.set(user, note); + } + } + + if (!client.user.bot && client.options.sync) client.setInterval(client.syncGuilds.bind(client), 30000); + + if (!client.users.has('1')) { + client.dataManager.newUser({ + id: '1', + username: 'Clyde', + discriminator: '0000', + avatar: 'https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png', + bot: true, + status: 'online', + game: null, + verified: true, + }); + } + + const t = client.setTimeout(() => { + client.ws.connection.triggerReady(); + }, 1200 * data.guilds.length); + + const guildCount = data.guilds.length; + + if (client.getMaxListeners() !== 0) client.setMaxListeners(client.getMaxListeners() + guildCount); + + client.once('ready', () => { + client.syncGuilds(); + if (client.getMaxListeners() !== 0) client.setMaxListeners(client.getMaxListeners() - guildCount); + client.clearTimeout(t); + }); + + const ws = this.packetManager.ws; + + ws.sessionID = data.session_id; + client.emit('debug', `READY ${ws.sessionID}`); + ws.checkIfReady(); + } +} + +module.exports = ReadyHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipAdd.js new file mode 100644 index 0000000..122b4c5 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipAdd.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); + +class RelationshipAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + if (data.type === 1) { + client.fetchUser(data.id).then(user => { + client.user.friends.set(user.id, user); + }); + } else if (data.type === 2) { + client.fetchUser(data.id).then(user => { + client.user.blocked.set(user.id, user); + }); + } + } +} + +module.exports = RelationshipAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipRemove.js new file mode 100644 index 0000000..b57326a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipRemove.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); + +class RelationshipRemoveHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + if (data.type === 2) { + if (client.user.blocked.has(data.id)) { + client.user.blocked.delete(data.id); + } + } else if (data.type === 1) { + if (client.user.friends.has(data.id)) { + client.user.friends.delete(data.id); + } + } + } +} + +module.exports = RelationshipRemoveHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/Resumed.js b/node_modules/discord.js/src/client/websocket/packets/handlers/Resumed.js new file mode 100644 index 0000000..a0a51c0 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/Resumed.js @@ -0,0 +1,26 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class ResumedHandler extends AbstractHandler { + handle() { + const client = this.packetManager.client; + const ws = client.ws.connection; + + ws.status = Constants.Status.READY; + this.packetManager.handleQueue(); + + const replayed = ws.sequence - ws.closeSequence; + + ws.debug(`RESUMED | replayed ${replayed} events.`); + client.emit(Constants.Events.RESUME, replayed); + ws.heartbeat(); + } +} + +/** + * Emitted whenever a WebSocket resumes. + * @event Client#resume + * @param {number} replayed The number of events that were replayed + */ + +module.exports = ResumedHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/TypingStart.js b/node_modules/discord.js/src/client/websocket/packets/handlers/TypingStart.js new file mode 100644 index 0000000..a7f5a36 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/TypingStart.js @@ -0,0 +1,68 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class TypingStartHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const channel = client.channels.get(data.channel_id); + const user = client.users.get(data.user_id); + const timestamp = new Date(data.timestamp * 1000); + + if (channel && user) { + if (channel.type === 'voice') { + client.emit(Constants.Events.WARN, `Discord sent a typing packet to voice channel ${channel.id}`); + return; + } + if (channel._typing.has(user.id)) { + const typing = channel._typing.get(user.id); + typing.lastTimestamp = timestamp; + typing.resetTimeout(tooLate(channel, user)); + } else { + channel._typing.set(user.id, new TypingData(client, timestamp, timestamp, tooLate(channel, user))); + client.emit(Constants.Events.TYPING_START, channel, user); + } + } + } +} + +class TypingData { + constructor(client, since, lastTimestamp, _timeout) { + this.client = client; + this.since = since; + this.lastTimestamp = lastTimestamp; + this._timeout = _timeout; + } + + resetTimeout(_timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = _timeout; + } + + get elapsedTime() { + return Date.now() - this.since; + } +} + +function tooLate(channel, user) { + return channel.client.setTimeout(() => { + channel.client.emit(Constants.Events.TYPING_STOP, channel, user, channel._typing.get(user.id)); + channel._typing.delete(user.id); + }, 6000); +} + +/** + * 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 + */ + +/** + * Emitted whenever a user stops typing in a channel. + * @event Client#typingStop + * @param {Channel} channel The channel the user stopped typing in + * @param {User} user The user that stopped typing + */ + +module.exports = TypingStartHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js new file mode 100644 index 0000000..90bca4c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js @@ -0,0 +1,21 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); +const ClientUserGuildSettings = require('../../../../structures/ClientUserGuildSettings'); + +class UserGuildSettingsUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const settings = client.user.guildSettings.get(packet.d.guild_id); + if (settings) settings.patch(packet.d); + else client.user.guildSettings.set(packet.d.guild_id, new ClientUserGuildSettings(packet.d, client)); + client.emit(Constants.Events.USER_GUILD_SETTINGS_UPDATE, client.user.guildSettings.get(packet.d.guild_id)); + } +} + +/** + * Emitted whenever the client user's settings update. + * @event Client#clientUserGuildSettingsUpdate + * @param {ClientUserGuildSettings} clientUserGuildSettings The new client user guild settings + */ + +module.exports = UserGuildSettingsUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserNoteUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserNoteUpdate.js new file mode 100644 index 0000000..1e4777a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserNoteUpdate.js @@ -0,0 +1,12 @@ +const AbstractHandler = require('./AbstractHandler'); + +class UserNoteUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + client.actions.UserNoteUpdate.handle(data); + } +} + +module.exports = UserNoteUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserSettingsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserSettingsUpdate.js new file mode 100644 index 0000000..903b64b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserSettingsUpdate.js @@ -0,0 +1,18 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class UserSettingsUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + client.user.settings.patch(packet.d); + client.emit(Constants.Events.USER_SETTINGS_UPDATE, client.user.settings); + } +} + +/** + * Emitted when the client user's settings update. + * @event Client#clientUserSettingsUpdate + * @param {ClientUserSettings} clientUserSettings The new client user settings + */ + +module.exports = UserSettingsUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserUpdate.js new file mode 100644 index 0000000..bc34f34 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class UserUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.UserUpdate.handle(data); + } +} + +module.exports = UserUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceServerUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceServerUpdate.js new file mode 100644 index 0000000..97885d6 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceServerUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); + +/* +{ + "token": "my_token", + "guild_id": "41771983423143937", + "endpoint": "smart.loyal.discord.gg" +} +*/ + +class VoiceServerUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.emit('self.voiceServer', data); + } +} + +module.exports = VoiceServerUpdate; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceStateUpdate.js new file mode 100644 index 0000000..2b0b40f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -0,0 +1,53 @@ +const AbstractHandler = require('./AbstractHandler'); + +const Constants = require('../../../../util/Constants'); +const Util = require('../../../../util/Util'); + +class VoiceStateUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + const member = guild.members.get(data.user_id); + if (member) { + const oldVoiceChannelMember = Util.cloneObject(member); + if (member.voiceChannel && member.voiceChannel.id !== data.channel_id) { + member.voiceChannel.members.delete(oldVoiceChannelMember.id); + } + + // If the member left the voice channel, unset their speaking property + if (!data.channel_id) member.speaking = null; + + if (member.user.id === client.user.id) { + client.emit('self.voiceStateUpdate', data); + } + + const newChannel = client.channels.get(data.channel_id); + if (newChannel) { + newChannel.members.set(member.id, member); + member.guild.channels.set(data.channel_id, newChannel); + } + + member.serverMute = data.mute; + member.serverDeaf = data.deaf; + member.selfMute = data.self_mute; + member.selfDeaf = data.self_deaf; + member.selfStream = data.self_stream || false; + member.voiceSessionID = data.session_id; + member.voiceChannelID = data.channel_id; + client.emit(Constants.Events.VOICE_STATE_UPDATE, oldVoiceChannelMember, member); + } + } + } +} + +/** + * Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * @event Client#voiceStateUpdate + * @param {GuildMember} oldMember The member before the voice state update + * @param {GuildMember} newMember The member after the voice state update + */ + +module.exports = VoiceStateUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/WebhooksUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/WebhooksUpdate.js new file mode 100644 index 0000000..7ed2721 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/WebhooksUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const { Events } = require('../../../../util/Constants'); + +class WebhooksUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const channel = client.channels.get(data.channel_id); + if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); + } +} + +/** + * Emitted whenever a guild text channel has its webhooks changed. + * @event Client#webhookUpdate + * @param {TextChannel} channel The channel that had a webhook update + */ + +module.exports = WebhooksUpdate; |