diff options
Diffstat (limited to 'node_modules/discord.js/src')
142 files changed, 13166 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..11cd5f4 --- /dev/null +++ b/node_modules/discord.js/src/client/Client.js @@ -0,0 +1,478 @@ +const EventEmitter = require('events').EventEmitter; +const mergeDefault = require('../util/MergeDefault'); +const Constants = require('../util/Constants'); +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'); + +/** + * The starting point for making a Discord 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 = 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 ShardingManager) + * @type {?ShardClientUtil} + */ + this.shard = process.send ? ShardClientUtil.singleton(this) : null; + + /** + * A collection of the Client's stored users + * @type {Collection<string, User>} + */ + this.users = new Collection(); + + /** + * A collection of the Client's stored guilds + * @type {Collection<string, Guild>} + */ + this.guilds = new Collection(); + + /** + * A collection of the Client's stored channels + * @type {Collection<string, Channel>} + */ + this.channels = new Collection(); + + /** + * A collection of presences for friends of the logged in user. + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<string, Presence>} + */ + this.presences = new Collection(); + + if (!this.token && 'CLIENT_TOKEN' in process.env) { + /** + * The authorization token for the logged in user/bot. + * @type {?string} + */ + this.token = process.env.CLIENT_TOKEN; + } else { + this.token = null; + } + + /** + * The ClientUser representing the logged in Client + * @type {?ClientUser} + */ + this.user = null; + + /** + * The date at which the Client was regarded as being in the `READY` state. + * @type {?Date} + */ + this.readyAt = null; + + /** + * The previous heartbeat pings of the websocket (most recent first, limited to three elements) + * @type {number[]} + */ + this.pings = []; + + this._pingTimestamp = 0; + this._timeouts = new Set(); + this._intervals = new Set(); + + if (this.options.messageSweepInterval > 0) { + this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); + } + } + + /** + * The status for the logged in Client. + * @type {?number} + * @readonly + */ + get status() { + return this.ws.status; + } + + /** + * The uptime for the logged in Client. + * @type {?number} + * @readonly + */ + get uptime() { + return this.readyAt ? Date.now() - this.readyAt : null; + } + + /** + * The average heartbeat ping of the websocket + * @type {number} + * @readonly + */ + get ping() { + return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; + } + + /** + * Returns a collection, mapping guild ID to voice connections. + * @type {Collection<string, VoiceConnection>} + * @readonly + */ + get voiceConnections() { + if (this.browser) return new Collection(); + return this.voice.connections; + } + + /** + * The emojis that the client can use. Mapped by emoji ID. + * @type {Collection<string, 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; + } + + /** + * The timestamp that 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'; + } + + /** + * Logs the client in. If successful, resolves with the account's token. <warn>If you're making a bot, it's + * much better to use a bot account rather than a user account. + * Bot accounts have higher rate limits and have access to some features user accounts don't have. User bots + * that are making a lot of API requests can even be banned.</warn> + * @param {string} token The token used for the account. + * @returns {Promise<string>} + * @example + * // log the client in using a token + * const token = 'my token'; + * client.login(token); + * @example + * // log the client in using email and password + * const email = 'user@email.com'; + * const password = 'supersecret123'; + * client.login(email, password); + */ + login(token) { + return this.rest.methods.login(token); + } + + /** + * Destroys the client and logs out. + * @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(); + } + + /** + * This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however + * if you wish to force a sync of guild data, you can use this. + * <warn>This is only available when using a user account.</warn> + * @param {Guild[]|Collection<string, Guild>} [guilds=this.guilds] An array or collection of guilds to sync + */ + 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), + }); + } + + /** + * Caches a user, or obtains it from the cache if it's already cached. + * <warn>This is only available when using a bot account.</warn> + * @param {string} id The ID of the user to obtain + * @returns {Promise<User>} + */ + fetchUser(id) { + if (this.users.has(id)) return Promise.resolve(this.users.get(id)); + return this.rest.methods.getUser(id); + } + + /** + * Fetches an invite object from an invite code. + * @param {InviteResolvable} invite An invite code or URL + * @returns {Promise<Invite>} + */ + fetchInvite(invite) { + const code = this.resolver.resolveInviteCode(invite); + return this.rest.methods.getInvite(code); + } + + /** + * Fetch a webhook by ID. + * @param {string} id ID of the webhook + * @param {string} [token] Token for the webhook + * @returns {Promise<Webhook>} + */ + fetchWebhook(id, token) { + return this.rest.methods.getWebhook(id, token); + } + + /** + * Sweeps all 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 the client's `messageCacheLifetime` option. + * @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++; + + for (const message of channel.messages.values()) { + if (now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs) { + channel.messages.delete(message.id); + messages++; + } + } + } + + this.emit('debug', `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`); + return messages; + } + + /** + * Gets the bot's OAuth2 application. + * <warn>This is only available when using a bot account.</warn> + * @returns {Promise<ClientOAuth2Application>} + */ + fetchApplication() { + if (!this.user.bot) throw new Error(Constants.Errors.NO_BOT_ACCOUNT); + return this.rest.methods.getMyApplication(); + } + + /** + * Generate an invite link for your bot + * @param {PermissionResolvable[]|number} [permissions] An array of permissions to request + * @returns {Promise<string>} The invite link + * @example + * client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE']) + * .then(link => { + * console.log(`Generated bot invite link: ${link}`); + * }); + */ + generateInvite(permissions) { + if (permissions) { + if (permissions instanceof Array) permissions = this.resolver.resolvePermissions(permissions); + } else { + permissions = 0; + } + 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(); + this._timeouts.delete(timeout); + }, delay, ...args); + 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); + } + + _pong(startTime) { + this.pings.unshift(Date.now() - startTime); + if (this.pings.length > 3) this.pings.length = 3; + this.ws.lastHeartbeatAck = true; + } + + _setPresence(id, presence) { + if (this.presences.get(id)) { + this.presences.get(id).update(presence); + return; + } + this.presences.set(id, new Presence(presence)); + } + + _eval(script) { + return eval(script); + } + + _validateOptions(options = this.options) { + 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.'); + } +} + +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..32b442b --- /dev/null +++ b/node_modules/discord.js/src/client/ClientDataManager.js @@ -0,0 +1,129 @@ +const Constants = require('../util/Constants'); +const cloneObject = require('../util/CloneObject'); +const Guild = require('../structures/Guild'); +const User = require('../structures/User'); +const DMChannel = require('../structures/DMChannel'); +const Emoji = require('../structures/Emoji'); +const TextChannel = require('../structures/TextChannel'); +const VoiceChannel = require('../structures/VoiceChannel'); +const GuildChannel = require('../structures/GuildChannel'); +const GroupDMChannel = require('../structures/GroupDMChannel'); + +class ClientDataManager { + constructor(client) { + this.client = client; + } + + get pastReady() { + return this.client.ws.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) { + if (this.client.users.has(data.id)) return this.client.users.get(data.id); + const user = new User(this.client, data); + 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.groupDM) { + channel = new GroupDMChannel(this.client, data); + } else { + guild = guild || this.client.guilds.get(data.guild_id); + if (guild) { + if (data.type === Constants.ChannelTypes.text) { + channel = new TextChannel(guild, data); + guild.channels.set(channel.id, channel); + } else if (data.type === Constants.ChannelTypes.voice) { + channel = new VoiceChannel(guild, data); + guild.channels.set(channel.id, channel); + } + } + } + + if (channel) { + if (this.pastReady && !already) this.client.emit(Constants.Events.CHANNEL_CREATE, channel); + this.client.channels.set(channel.id, channel); + 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 = 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 = cloneObject(currentEmoji); + currentEmoji.setup(newData); + this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, 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..d38fb7c --- /dev/null +++ b/node_modules/discord.js/src/client/ClientDataResolver.js @@ -0,0 +1,309 @@ +const path = require('path'); +const fs = require('fs'); +const request = require('superagent'); + +const Constants = require('../util/Constants'); +const convertArrayBuffer = require('../util/ConvertArrayBuffer'); +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'); + +/** + * 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 user ID + * * A Message object (resolves to the message author) + * * A Guild object (owner of the guild) + * * A GuildMember object + * @typedef {User|string|Message|Guild|GuildMember} UserResolvable + */ + + /** + * 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 user.owner; + return null; + } + + /** + * Resolves a UserResolvable to a user ID string + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?string} + */ + 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 Guild ID + * @typedef {Guild|string} 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 {Guild} 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 give a Channel. This can be: + * * A Channel object + * * A Message object (the channel the message was sent in) + * * A Guild object (the #general channel) + * * A channel ID + * @typedef {Channel|Guild|Message|string} ChannelResolvable + */ + + /** + * 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 (channel instanceof Message) return channel.channel; + if (channel instanceof Guild) return channel.channels.get(channel.id) || null; + if (typeof channel === 'string') return this.client.channels.get(channel) || null; + 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)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i; + const match = inviteRegex.exec(data); + if (match && match[1]) return match[1]; + return data; + } + + /** + * Data that can be resolved to give a permission number. This can be: + * * A string + * * A permission number + * + * Possible strings: + * ```js + * [ + * "CREATE_INSTANT_INVITE", + * "KICK_MEMBERS", + * "BAN_MEMBERS", + * "ADMINISTRATOR", + * "MANAGE_CHANNELS", + * "MANAGE_GUILD", + * "ADD_REACTIONS", // add reactions to messages + * "READ_MESSAGES", + * "SEND_MESSAGES", + * "SEND_TTS_MESSAGES", + * "MANAGE_MESSAGES", + * "EMBED_LINKS", + * "ATTACH_FILES", + * "READ_MESSAGE_HISTORY", + * "MENTION_EVERYONE", + * "EXTERNAL_EMOJIS", // use external emojis + * "CONNECT", // connect to voice + * "SPEAK", // speak on voice + * "MUTE_MEMBERS", // globally mute members on voice + * "DEAFEN_MEMBERS", // globally deafen members on voice + * "MOVE_MEMBERS", // move member's voice channels + * "USE_VAD", // use voice activity detection + * "CHANGE_NICKNAME", + * "MANAGE_NICKNAMES", // change nicknames of others + * "MANAGE_ROLES_OR_PERMISSIONS", + * "MANAGE_WEBHOOKS", + * "MANAGE_EMOJIS" + * ] + * ``` + * @typedef {string|number} PermissionResolvable + */ + + /** + * Resolves a PermissionResolvable to a permission number + * @param {PermissionResolvable} permission The permission resolvable to resolve + * @returns {number} + */ + resolvePermission(permission) { + if (typeof permission === 'string') permission = Constants.PermissionFlags[permission]; + if (typeof permission !== 'number' || permission < 1) throw new Error(Constants.Errors.NOT_A_PERMISSION); + return permission; + } + + /** + * Turn an array of permissions into a valid Discord permission bitfield + * @param {PermissionResolvable[]} permissions Permissions to resolve together + * @returns {number} + */ + resolvePermissions(permissions) { + let bitfield = 0; + for (const permission of permissions) bitfield |= this.resolvePermission(permission); + return bitfield; + } + + /** + * 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); + } + + /** + * 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 + * @typedef {string|Buffer} BufferResolvable + */ + + /** + * Resolves a BufferResolvable to a Buffer + * @param {BufferResolvable} resource The buffer resolvable to resolve + * @returns {Promise<Buffer>} + */ + resolveBuffer(resource) { + if (resource instanceof Buffer) return Promise.resolve(resource); + if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertArrayBuffer(resource)); + + if (typeof resource === 'string') { + return new Promise((resolve, reject) => { + if (/^https?:\/\//.test(resource)) { + const req = request.get(resource).set('Content-Type', 'blob'); + if (this.client.browser) req.responseType('arraybuffer'); + req.end((err, res) => { + if (err) return reject(err); + if (this.client.browser) return resolve(convertArrayBuffer(res.xhr.response)); + if (!(res.body instanceof Buffer)) return reject(new TypeError('The response body isn\'t a Buffer.')); + return resolve(res.body); + }); + } else { + const file = path.resolve(resource); + fs.stat(file, (err, stats) => { + if (err) reject(err); + if (!stats || !stats.isFile()) throw new Error(`The file could not be found: ${file}`); + fs.readFile(file, (err2, data) => { + if (err2) reject(err2); else resolve(data); + }); + }); + } + }); + } + + 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: + * * A string + * * An Emoji + * * A ReactionEmoji + * @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 (!emoji.includes('%')) return encodeURIComponent(emoji); + } + return null; + } +} + +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..0cfbbfd --- /dev/null +++ b/node_modules/discord.js/src/client/ClientManager.js @@ -0,0 +1,67 @@ +const Constants = require('../util/Constants'); + +/** + * 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, null if not yet set + * @type {?number} + */ + this.heartbeatInterval = null; + } + + /** + * 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(gateway => { + this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`); + this.client.ws.connect(gateway); + this.client.ws.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)); + }); + this.client.once(Constants.Events.READY, () => { + resolve(token); + this.client.clearTimeout(timeout); + }); + }, reject); + } + + /** + * Sets up a keep-alive interval to keep the Client's connection valid + * @param {number} time The interval in milliseconds at which heartbeat packets should be sent + */ + setupKeepAlive(time) { + this.heartbeatInterval = this.client.setInterval(() => this.client.ws.heartbeat(true), time); + } + + destroy() { + this.client.ws.destroy(); + 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..68a8a94 --- /dev/null +++ b/node_modules/discord.js/src/client/WebhookClient.js @@ -0,0 +1,46 @@ +const Webhook = require('../structures/Webhook'); +const RESTManager = require('./rest/RESTManager'); +const ClientDataResolver = require('./ClientDataResolver'); +const mergeDefault = require('../util/MergeDefault'); +const Constants = require('../util/Constants'); + +/** + * The Webhook Client + * @extends {Webhook} + */ +class WebhookClient extends Webhook { + /** + * @param {string} id The id of the webhook. + * @param {string} token the token of the webhook. + * @param {ClientOptions} [options] Options for the client + * @example + * // create a new webhook and send a message + * let 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 = 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); + } +} + +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..ac95aa7 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ActionsManager.js @@ -0,0 +1,38 @@ +class ActionsManager { + constructor(client) { + this.client = client; + + this.register(require('./MessageCreate')); + this.register(require('./MessageDelete')); + this.register(require('./MessageDeleteBulk')); + this.register(require('./MessageUpdate')); + this.register(require('./MessageReactionAdd')); + this.register(require('./MessageReactionRemove')); + this.register(require('./MessageReactionRemoveAll')); + this.register(require('./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('./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('./GuildRolesPositionUpdate')); + } + + 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..dc47041 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelCreate.js @@ -0,0 +1,13 @@ +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..7b847ef --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelDelete.js @@ -0,0 +1,31 @@ +const Action = require('./Action'); + +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; + } + + 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..df50ed4 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelUpdate.js @@ -0,0 +1,34 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const cloneObject = require('../../util/CloneObject'); + +class ChannelUpdateAction extends Action { + handle(data) { + const client = this.client; + + const channel = client.channels.get(data.id); + if (channel) { + const oldChannel = cloneObject(channel); + 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/GuildDelete.js b/node_modules/discord.js/src/client/actions/GuildDelete.js new file mode 100644 index 0000000..1214263 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildDelete.js @@ -0,0 +1,51 @@ +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) { + 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, + }; + } + + // 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; + } + + 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..5df1ced --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js @@ -0,0 +1,18 @@ +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..8cfa205 --- /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); + 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..94bfa24 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js @@ -0,0 +1,15 @@ +const Action = require('./Action'); + +class GuildEmojiUpdateAction extends Action { + handle(oldEmoji, newEmoji) { + this.client.dataManager.updateEmoji(oldEmoji, newEmoji); + } +} + +/** + * 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/GuildMemberGet.js b/node_modules/discord.js/src/client/actions/GuildMemberGet.js new file mode 100644 index 0000000..b00fa0f --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildMemberGet.js @@ -0,0 +1,12 @@ +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..d68b8b5 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildMemberRemove.js @@ -0,0 +1,49 @@ +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); + if (guild) { + let member = guild.members.get(data.user.id); + if (member) { + guild.memberCount--; + 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; + } + + return { + guild, + member, + }; + } + + return { + guild, + member: null, + }; + } + + 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..82ea19a --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleCreate.js @@ -0,0 +1,32 @@ +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); + if (guild) { + const already = guild.roles.has(data.role.id); + const role = new Role(guild, data.role); + guild.roles.set(role.id, role); + if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, role); + return { + role, + }; + } + + return { + role: null, + }; + } +} + +/** + * 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..eeaa1e9 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleDelete.js @@ -0,0 +1,46 @@ +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); + if (guild) { + let 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; + } + + return { + role, + }; + } + + return { + role: null, + }; + } + + 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..8270517 --- /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 cloneObject = require('../../util/CloneObject'); + +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 = 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..a95c923 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js @@ -0,0 +1,23 @@ +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..7b94ec8 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildSync.js @@ -0,0 +1,27 @@ +const Action = require('./Action'); + +class GuildSync extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.id); + if (guild) { + data.presences = data.presences || []; + for (const presence of data.presences) { + guild._setPresence(presence.user.id, presence); + } + + data.members = 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); + } + } + } + } +} + +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..efda7f7 --- /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 cloneObject = require('../../util/CloneObject'); + +class GuildUpdateAction extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.id); + if (guild) { + const oldGuild = 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/MessageCreate.js b/node_modules/discord.js/src/client/actions/MessageCreate.js new file mode 100644 index 0000000..00fc1e9 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageCreate.js @@ -0,0 +1,40 @@ +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)); + } + channel.lastMessageID = messages[messages.length - 1].id; + if (user) user.lastMessageID = messages[messages.length - 1].id; + if (member) member.lastMessageID = messages[messages.length - 1].id; + return { + messages, + }; + } else { + const message = channel._cacheMessage(new Message(channel, data, client)); + channel.lastMessageID = data.id; + if (user) user.lastMessageID = data.id; + if (member) member.lastMessageID = data.id; + 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..beb8050 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDelete.js @@ -0,0 +1,40 @@ +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); + if (channel) { + let 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; + } + + return { + message, + }; + } + + return { + message: null, + }; + } + + 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..6a12ef1 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js @@ -0,0 +1,24 @@ +const Action = require('./Action'); +const Collection = require('../../util/Collection'); +const Constants = require('../../util/Constants'); + +class MessageDeleteBulkAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.get(data.channel_id); + + const ids = data.ids; + const messages = new Collection(); + for (const id of ids) { + const message = channel.messages.get(id); + if (message) messages.set(message.id, message); + } + + if (messages.size > 0) 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..f57ec2e --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionAdd.js @@ -0,0 +1,43 @@ +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; + + 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; + + if (!data.emoji) return false; + + 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 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..98a958d --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemove.js @@ -0,0 +1,43 @@ +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; + + 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; + + if (!data.emoji) return false; + + 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 message. + * @event Client#messageReactionRemove + * @param {MessageReaction} messageReaction The reaction object. + * @param {User} user The user that removed the emoji or reaction emoji. + */ +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..f35b785 --- /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 message. + * @event Client#messageReactionRemoveAll + * @param {MessageReaction} messageReaction The reaction object. + */ +module.exports = MessageReactionRemoveAll; 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..a62c332 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageUpdate.js @@ -0,0 +1,43 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const cloneObject = require('../../util/CloneObject'); + +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) { + const oldMessage = cloneObject(message); + message.patch(data); + message._edits.unshift(oldMessage); + client.emit(Constants.Events.MESSAGE_UPDATE, oldMessage, message); + return { + old: oldMessage, + 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..65e7c95 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserGet.js @@ -0,0 +1,13 @@ +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..b361eca --- /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 cloneObject = require('../../util/CloneObject'); + +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 = 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..36c2d8f --- /dev/null +++ b/node_modules/discord.js/src/client/rest/APIRequest.js @@ -0,0 +1,49 @@ +const request = require('superagent'); +const Constants = require('../../util/Constants'); + +function 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; +} + +class APIRequest { + constructor(rest, method, url, auth, data, file) { + this.rest = rest; + this.method = method; + this.url = url; + this.auth = auth; + this.data = data; + this.file = file; + this.route = getRoute(this.url); + } + + getAuth() { + if (this.rest.client.token && this.rest.client.user && this.rest.client.user.bot) { + return `Bot ${this.rest.client.token}`; + } else if (this.rest.client.token) { + return this.rest.client.token; + } + throw new Error(Constants.Errors.NO_TOKEN); + } + + gen() { + const apiRequest = request[this.method](this.url); + if (this.auth) apiRequest.set('authorization', this.getAuth()); + if (this.file && this.file.file) { + apiRequest.attach('file', this.file.file, this.file.name); + this.data = this.data || {}; + apiRequest.field('payload_json', JSON.stringify(this.data)); + } else if (this.data) { + apiRequest.send(this.data); + } + if (!this.rest.client.browser) apiRequest.set('User-Agent', this.rest.userAgentManager.userAgent); + return apiRequest; + } +} + +module.exports = APIRequest; 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..ac1ce6e --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RESTManager.js @@ -0,0 +1,51 @@ +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; + } + + push(handler, apiRequest) { + return new Promise((resolve, reject) => { + handler.push({ + request: apiRequest, + resolve, + reject, + }); + }); + } + + 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) { + const apiRequest = new APIRequest(this, method, url, auth, data, file); + + 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..7006248 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RESTMethods.js @@ -0,0 +1,653 @@ +const Constants = require('../../util/Constants'); +const Collection = require('../../util/Collection'); +const splitMessage = require('../../util/SplitMessage'); +const parseEmoji = require('../../util/ParseEmoji'); +const escapeMarkdown = require('../../util/EscapeMarkdown'); + +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 ClientOAuth2Application = require('../../structures/ClientOAuth2Application'); + +class RESTMethods { + constructor(restManager) { + this.rest = restManager; + this.client = restManager.client; + } + + login(token = this.client.token) { + return new Promise((resolve, reject) => { + if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); + token = token.replace(/^Bot\s*/i, ''); + this.client.manager.connectToWebSocket(token, resolve, reject); + }); + } + + logout() { + return this.rest.makeRequest('post', Constants.Endpoints.logout, true, {}); + } + + getGateway() { + return this.rest.makeRequest('get', Constants.Endpoints.gateway, true).then(res => { + this.client.ws.gateway = `${res.url}/?v=${Constants.PROTOCOL_VERSION}`; + return this.client.ws.gateway; + }); + } + + getBotGateway() { + return this.rest.makeRequest('get', Constants.Endpoints.botGateway, true); + } + + sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code } = {}, file = null) { + return new Promise((resolve, reject) => { + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + if (content) { + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = escapeMarkdown(this.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + } + + if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + if (split) content = splitMessage(content, typeof split === 'object' ? split : {}); + } + + const send = chan => { + if (content instanceof Array) { + const messages = []; + (function sendChunk(list, index) { + const options = index === list.length ? { tts, embed } : { tts }; + chan.send(list[index], options, index === list.length ? file : null).then((message) => { + messages.push(message); + if (index >= list.length) return resolve(messages); + return sendChunk(list, ++index); + }); + }(content, 0)); + } else { + this.rest.makeRequest('post', Constants.Endpoints.channelMessages(chan.id), true, { + content, tts, nonce, embed, + }, file).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, { embed, code } = {}) { + content = this.client.resolver.resolveString(content); + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = escapeMarkdown(this.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + } + return this.rest.makeRequest('patch', Constants.Endpoints.channelMessage(message.channel.id, message.id), true, { + content, embed, + }).then(data => this.client.actions.MessageUpdate.handle(data).updated); + } + + deleteMessage(message) { + return this.rest.makeRequest('del', Constants.Endpoints.channelMessage(message.channel.id, message.id), true) + .then(() => + this.client.actions.MessageDelete.handle({ + id: message.id, + channel_id: message.channel.id, + }).message + ); + } + + bulkDeleteMessages(channel, messages) { + return this.rest.makeRequest('post', `${Constants.Endpoints.channelMessages(channel.id)}/bulk_delete`, true, { + messages, + }).then(() => + this.client.actions.MessageDeleteBulk.handle({ + channel_id: channel.id, + ids: messages, + }).messages + ); + } + + createChannel(guild, channelName, channelType, overwrites) { + if (overwrites instanceof Collection) overwrites = overwrites.array(); + return this.rest.makeRequest('post', Constants.Endpoints.guildChannels(guild.id), true, { + name: channelName, + type: channelType, + permission_overwrites: overwrites, + }).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', Constants.Endpoints.userChannels(this.client.user.id), true, { + recipient_id: recipient.id, + }).then(data => this.client.actions.ChannelCreate.handle(data).channel); + } + + getExistingDM(recipient) { + return this.client.channels.find(channel => + channel.recipient && channel.recipient.id === recipient.id + ); + } + + deleteChannel(channel) { + 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('del', Constants.Endpoints.channel(channel.id), true).then(data => { + data.id = channel.id; + return this.client.actions.ChannelDelete.handle(data).channel; + }); + } + + updateChannel(channel, _data) { + const data = {}; + data.name = (_data.name || channel.name).trim(); + data.topic = _data.topic || channel.topic; + data.position = _data.position || channel.position; + data.bitrate = _data.bitrate || channel.bitrate; + data.user_limit = _data.userLimit || channel.userLimit; + return this.rest.makeRequest('patch', Constants.Endpoints.channel(channel.id), true, data).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('del', Constants.Endpoints.meGuild(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', Constants.Endpoints.guilds, true, options).then(data => { + if (this.client.guilds.has(data.id)) { + resolve(this.client.guilds.get(data.id)); + return; + } + + const handleGuild = guild => { + if (guild.id === data.id) { + this.client.removeListener('guildCreate', handleGuild); + this.client.clearTimeout(timeout); + resolve(guild); + } + }; + this.client.on('guildCreate', handleGuild); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener('guildCreate', handleGuild); + reject(new Error('Took too long to receive guild data.')); + }, 10000); + }, reject); + }); + } + + // untested but probably will work + deleteGuild(guild) { + return this.rest.makeRequest('del', Constants.Endpoints.guild(guild.id), true).then(() => + this.client.actions.GuildDelete.handle({ id: guild.id }).guild + ); + } + + getUser(userID) { + return this.rest.makeRequest('get', Constants.Endpoints.user(userID), true).then(data => + this.client.actions.UserGet.handle(data).user + ); + } + + updateCurrentUser(_data, password) { + const user = this.client.user; + const data = {}; + data.username = _data.username || user.username; + data.avatar = this.client.resolver.resolveBase64(_data.avatar) || user.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', Constants.Endpoints.me, true, data).then(newData => + this.client.actions.UserUpdate.handle(newData).updated + ); + } + + updateGuild(guild, _data) { + const data = {}; + if (_data.name) data.name = _data.name; + if (_data.region) data.region = _data.region; + if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel); + if (_data.afkChannel) data.afk_channel_id = this.client.resolver.resolveChannel(_data.afkChannel).id; + if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout); + if (_data.icon) data.icon = this.client.resolver.resolveBase64(_data.icon); + if (_data.owner) data.owner_id = this.client.resolver.resolveUser(_data.owner).id; + if (_data.splash) data.splash = this.client.resolver.resolveBase64(_data.splash); + return this.rest.makeRequest('patch', Constants.Endpoints.guild(guild.id), true, data).then(newData => + this.client.actions.GuildUpdate.handle(newData).updated + ); + } + + kickGuildMember(guild, member) { + return this.rest.makeRequest('del', Constants.Endpoints.guildMember(guild.id, member.id), true).then(() => + this.client.actions.GuildMemberRemove.handle({ + guild_id: guild.id, + user: member.user, + }).member + ); + } + + createGuildRole(guild) { + return this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true).then(role => + this.client.actions.GuildRoleCreate.handle({ + guild_id: guild.id, + role, + }).role + ); + } + + deleteGuildRole(role) { + return this.rest.makeRequest('del', Constants.Endpoints.guildRole(role.guild.id, role.id), true).then(() => + this.client.actions.GuildRoleDelete.handle({ + guild_id: role.guild.id, + role_id: role.id, + }).role + ); + } + + setChannelOverwrite(channel, payload) { + return this.rest.makeRequest( + 'put', `${Constants.Endpoints.channelPermissions(channel.id)}/${payload.id}`, true, payload + ); + } + + deletePermissionOverwrites(overwrite) { + return this.rest.makeRequest( + 'del', `${Constants.Endpoints.channelPermissions(overwrite.channel.id)}/${overwrite.id}`, true + ).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 = Constants.Endpoints.channelMessages(channel.id); + 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', Constants.Endpoints.channelMessage(channel.id, messageID), true); + } + + getGuildMember(guild, user) { + return this.rest.makeRequest('get', Constants.Endpoints.guildMember(guild.id, user.id), true).then(data => + this.client.actions.GuildMemberGet.handle(guild, data).member + ); + } + + updateGuildMember(member, data) { + if (data.channel) data.channel_id = this.client.resolver.resolveChannel(data.channel).id; + if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); + + let endpoint = Constants.Endpoints.guildMember(member.guild.id, member.id); + // fix your endpoints, discord ;-; + if (member.id === this.client.user.id) { + const keys = Object.keys(data); + if (keys.length === 1 && keys[0] === 'nick') { + endpoint = Constants.Endpoints.guildMemberNickname(member.guild.id); + } + } + + return this.rest.makeRequest('patch', endpoint, true, data).then(newData => + member.guild._updateMember(member, newData).mem + ); + } + + addMemberRole(member, role) { + return this.rest.makeRequest('put', Constants.Endpoints.guildMemberRole(member.guild.id, member.id, role.id), true) + .then(() => { + if (!member._roles.includes(role.id)) member._roles.push(role.id); + return member; + }); + } + + removeMemberRole(member, role) { + return this.rest.makeRequest( + 'delete', + Constants.Endpoints.guildMemberRole(member.guild.id, member.id, role.id), + true + ).then(() => { + const index = member._roles.indexOf(role.id); + if (index >= 0) member._roles.splice(index, 1); + return member; + }); + } + + sendTyping(channelID) { + return this.rest.makeRequest('post', `${Constants.Endpoints.channel(channelID)}/typing`, true); + } + + banGuildMember(guild, member, deleteDays = 0) { + const id = this.client.resolver.resolveUserID(member); + if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); + return this.rest.makeRequest( + 'put', `${Constants.Endpoints.guildBans(guild.id)}/${id}?delete-message-days=${deleteDays}`, true, { + 'delete-message-days': deleteDays, + } + ).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) { + 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('del', `${Constants.Endpoints.guildBans(guild.id)}/${id}`, true).catch(err => { + this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); + this.client.clearTimeout(timeout); + reject(err); + }); + }); + } + + getGuildBans(guild) { + return this.rest.makeRequest('get', Constants.Endpoints.guildBans(guild.id), true).then(banItems => { + const bannedUsers = new Collection(); + for (const banItem of banItems) { + const user = this.client.dataManager.newUser(banItem.user); + bannedUsers.set(user.id, user); + } + return bannedUsers; + }); + } + + updateGuildRole(role, _data) { + const data = {}; + data.name = _data.name || role.name; + data.position = typeof _data.position !== 'undefined' ? _data.position : role.position; + data.color = _data.color || role.color; + if (typeof data.color === 'string' && data.color.startsWith('#')) { + data.color = parseInt(data.color.replace('#', ''), 16); + } + data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist; + data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable; + + if (_data.permissions) { + let perms = 0; + for (let perm of _data.permissions) { + if (typeof perm === 'string') perm = Constants.PermissionFlags[perm]; + perms |= perm; + } + data.permissions = perms; + } else { + data.permissions = role.permissions; + } + + return this.rest.makeRequest( + 'patch', Constants.Endpoints.guildRole(role.guild.id, role.id), true, data + ).then(_role => + this.client.actions.GuildRoleUpdate.handle({ + role: _role, + guild_id: role.guild.id, + }).updated + ); + } + + pinMessage(message) { + return this.rest.makeRequest('put', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true) + .then(() => message); + } + + unpinMessage(message) { + return this.rest.makeRequest('del', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true) + .then(() => message); + } + + getChannelPinnedMessages(channel) { + return this.rest.makeRequest('get', `${Constants.Endpoints.channel(channel.id)}/pins`, true); + } + + createChannelInvite(channel, options) { + const payload = {}; + payload.temporary = options.temporary; + payload.max_age = options.maxAge; + payload.max_uses = options.maxUses; + return this.rest.makeRequest('post', `${Constants.Endpoints.channelInvites(channel.id)}`, true, payload) + .then(invite => new Invite(this.client, invite)); + } + + deleteInvite(invite) { + return this.rest.makeRequest('del', Constants.Endpoints.invite(invite.code), true).then(() => invite); + } + + getInvite(code) { + return this.rest.makeRequest('get', Constants.Endpoints.invite(code), true).then(invite => + new Invite(this.client, invite) + ); + } + + getGuildInvites(guild) { + return this.rest.makeRequest('get', Constants.Endpoints.guildInvites(guild.id), 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; + }); + } + + pruneGuildMembers(guild, days, dry) { + return this.rest.makeRequest(dry ? 'get' : 'post', `${Constants.Endpoints.guildPrune(guild.id)}?days=${days}`, true) + .then(data => data.pruned); + } + + createEmoji(guild, image, name) { + return this.rest.makeRequest('post', `${Constants.Endpoints.guildEmojis(guild.id)}`, true, { name, image }) + .then(data => this.client.actions.EmojiCreate.handle(data, guild).emoji); + } + + deleteEmoji(emoji) { + return this.rest.makeRequest('delete', `${Constants.Endpoints.guildEmojis(emoji.guild.id)}/${emoji.id}`, true) + .then(() => this.client.actions.EmojiDelete.handle(emoji).data); + } + + getWebhook(id, token) { + return this.rest.makeRequest('get', Constants.Endpoints.webhook(id, token), !token).then(data => + new Webhook(this.client, data) + ); + } + + getGuildWebhooks(guild) { + return this.rest.makeRequest('get', Constants.Endpoints.guildWebhooks(guild.id), 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', Constants.Endpoints.channelWebhooks(channel.id), 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) { + return this.rest.makeRequest('post', Constants.Endpoints.channelWebhooks(channel.id), true, { name, avatar }) + .then(data => new Webhook(this.client, data)); + } + + editWebhook(webhook, name, avatar) { + return this.rest.makeRequest('patch', Constants.Endpoints.webhook(webhook.id, webhook.token), false, { + name, + avatar, + }).then(data => { + webhook.name = data.name; + webhook.avatar = data.avatar; + return webhook; + }); + } + + deleteWebhook(webhook) { + return this.rest.makeRequest('delete', Constants.Endpoints.webhook(webhook.id, webhook.token), false); + } + + sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds } = {}, file = null) { + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + if (content) { + if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + } + return this.rest.makeRequest('post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}?wait=true`, false, { + username: webhook.name, + avatar_url: avatarURL, + content, + tts, + file, + embeds, + }); + } + + sendSlackWebhookMessage(webhook, body) { + return this.rest.makeRequest( + 'post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}/slack?wait=true`, false, body + ); + } + + fetchUserProfile(user) { + return this.rest.makeRequest('get', Constants.Endpoints.userProfile(user.id), true).then(data => + new UserProfile(user, data) + ); + } + + fetchMeMentions(options) { + if (options.guild) options.guild = options.guild.id ? options.guild.id : options.guild; + return this.rest.makeRequest( + 'get', + Constants.Endpoints.meMentions(options.limit, options.roles, options.everyone, options.guild) + ).then(res => res.body.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); + } + + addFriend(user) { + return this.rest.makeRequest('post', Constants.Endpoints.relationships('@me'), true, { + username: user.username, + discriminator: user.discriminator, + }).then(() => user); + } + + removeFriend(user) { + return this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true) + .then(() => user); + } + + blockUser(user) { + return this.rest.makeRequest('put', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true, { type: 2 }) + .then(() => user); + } + + unblockUser(user) { + return this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true) + .then(() => user); + } + + setRolePositions(guildID, roles) { + return this.rest.makeRequest('patch', Constants.Endpoints.guildRoles(guildID), true, roles).then(() => + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: guildID, + roles, + }).guild + ); + } + + addMessageReaction(message, emoji) { + return this.rest.makeRequest( + 'put', Constants.Endpoints.selfMessageReaction(message.channel.id, message.id, emoji), true + ).then(() => + this.client.actions.MessageReactionAdd.handle({ + user_id: this.client.user.id, + message_id: message.id, + emoji: parseEmoji(emoji), + channel_id: message.channel.id, + }).reaction + ); + } + + removeMessageReaction(message, emoji, user) { + let endpoint = Constants.Endpoints.selfMessageReaction(message.channel.id, message.id, emoji); + if (user.id !== this.client.user.id) { + endpoint = Constants.Endpoints.userMessageReaction(message.channel.id, message.id, emoji, null, user.id); + } + return this.rest.makeRequest('delete', endpoint, true).then(() => + this.client.actions.MessageReactionRemove.handle({ + user_id: user.id, + message_id: message.id, + emoji: parseEmoji(emoji), + channel_id: message.channel.id, + }).reaction + ); + } + + removeMessageReactions(message) { + return this.rest.makeRequest('delete', Constants.Endpoints.messageReactions(message.channel.id, message.id), true) + .then(() => message); + } + + getMessageReactionUsers(message, emoji, limit = 100) { + return this.rest.makeRequest( + 'get', Constants.Endpoints.messageReaction(message.channel.id, message.id, emoji, limit), true + ); + } + + getMyApplication() { + return this.rest.makeRequest('get', Constants.Endpoints.myApplication, true).then(app => + new ClientOAuth2Application(this.client, app) + ); + } + + setNote(user, note) { + return this.rest.makeRequest('put', Constants.Endpoints.note(user.id), true, { note }).then(() => user); + } +} + +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..2cc1a59 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/Burst.js @@ -0,0 +1,70 @@ +const RequestHandler = require('./RequestHandler'); + +class BurstRequestHandler extends RequestHandler { + constructor(restManager, endpoint) { + super(restManager, endpoint); + this.requestRemaining = 1; + this.first = true; + } + + push(request) { + super.push(request); + this.handle(); + } + + handleNext(time) { + if (this.waiting) return; + this.waiting = true; + this.restManager.client.setTimeout(() => { + this.requestRemaining = this.requestLimit; + this.waiting = false; + this.handle(); + }, time); + } + + execute(item) { + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.requestLimit = 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(); + this.handleNext( + this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset + ); + } + if (err) { + if (err.status === 429) { + this.requestRemaining = 0; + this.queue.unshift(item); + this.restManager.client.setTimeout(() => { + this.globalLimit = false; + this.handle(); + }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + } else { + item.reject(err); + } + } else { + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + if (this.first) { + this.first = false; + this.handle(); + } + } + }); + } + + handle() { + super.handle(); + if (this.requestRemaining < 1 || this.queue.length === 0 || this.globalLimit) return; + while (this.queue.length > 0 && this.requestRemaining > 0) { + this.execute(this.queue.shift()); + this.requestRemaining--; + } + } +} + +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..a1a2f34 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/RequestHandler.js @@ -0,0 +1,51 @@ +/** + * 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} + */ + 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() { + return; + } +} + +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..0abf36d --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js @@ -0,0 +1,104 @@ +const RequestHandler = require('./RequestHandler'); + +/** + * 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); + + /** + * Whether this rate limiter is waiting for a response from a request + * @type {boolean} + */ + this.waiting = false; + + /** + * 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; + } + + 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) { + return new Promise(resolve => { + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.requestLimit = 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.restManager.client.setTimeout(() => { + this.waiting = false; + this.globalLimit = false; + resolve(); + }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + } else { + this.queue.shift(); + this.waiting = false; + item.reject(err); + resolve(err); + } + } else { + this.queue.shift(); + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + if (this.requestRemaining === 0) { + this.restManager.client.setTimeout( + () => { + this.waiting = false; + resolve(data); + }, + this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset + ); + } else { + this.waiting = false; + resolve(data); + } + } + }); + }); + } + + handle() { + super.handle(); + + if (this.waiting || this.queue.length === 0 || this.globalLimit) return; + this.waiting = true; + + const item = this.queue[0]; + this.execute(item).then(() => 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..12393ff --- /dev/null +++ b/node_modules/discord.js/src/client/rest/UserAgentManager.js @@ -0,0 +1,22 @@ +const Constants = require('../../util/Constants'); + +class UserAgentManager { + constructor(restManager) { + this.restManager = restManager; + this._userAgent = { + url: 'https://github.com/hydrabolt/discord.js', + version: Constants.Package.version, + }; + } + + set(info) { + this._userAgent.url = info.url || 'https://github.com/hydrabolt/discord.js'; + this._userAgent.version = info.version || Constants.Package.version; + } + + get userAgent() { + return `DiscordBot (${this._userAgent.url}, ${this._userAgent.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..ea83745 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/ClientVoiceManager.js @@ -0,0 +1,245 @@ +const Collection = require('../../util/Collection'); +const mergeDefault = require('../../util/MergeDefault'); +const Constants = require('../../util/Constants'); +const VoiceConnection = require('./VoiceConnection'); +const EventEmitter = require('events').EventEmitter; + +/** + * 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<string, VoiceConnection>} + */ + this.connections = new Collection(); + + /** + * Pending connection attempts, maps guild ID to VoiceChannel + * @type {Collection<string, VoiceChannel>} + */ + this.pending = new Collection(); + + this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); + this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); + } + + onVoiceServer(data) { + if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint); + } + + onVoiceStateUpdate(data) { + if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setSessionID(data.session_id); + } + + /** + * Sends a request to the main gateway to join a voice channel + * @param {VoiceChannel} channel The channel to join + * @param {Object} [options] The options to provide + */ + sendVoiceStateUpdate(channel, options = {}) { + if (!this.client.user) throw new Error('Unable to join because there is no client user.'); + if (!channel.permissionsFor) { + throw new Error('Channel does not support permissionsFor; is it really a voice channel?'); + } + const permissions = channel.permissionsFor(this.client.user); + if (!permissions) { + throw new Error('There is no permission set for the client user in this channel - are they part of the guild?'); + } + if (!permissions.hasPermission('CONNECT')) { + throw new Error('You do not have permission to join this voice channel.'); + } + + options = mergeDefault({ + guild_id: channel.guild.id, + channel_id: channel.id, + self_mute: false, + self_deaf: false, + }, options); + + this.client.ws.send({ + op: Constants.OPCodes.VOICE_STATE_UPDATE, + d: options, + }); + } + + /** + * 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 (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.'); + if (!channel.joinable) throw new Error('You do not have permission to join this voice channel.'); + + const existingConnection = this.connections.get(channel.guild.id); + if (existingConnection) { + if (existingConnection.channel.id !== channel.id) { + this.sendVoiceStateUpdate(channel); + this.connections.get(channel.guild.id).channel = channel; + } + resolve(existingConnection); + return; + } + + const pendingConnection = new PendingVoiceConnection(this, channel); + this.pending.set(channel.guild.id, pendingConnection); + + pendingConnection.on('fail', reason => { + this.pending.delete(channel.guild.id); + reject(reason); + }); + + pendingConnection.on('pass', voiceConnection => { + this.pending.delete(channel.guild.id); + this.connections.set(channel.guild.id, voiceConnection); + voiceConnection.once('ready', () => resolve(voiceConnection)); + voiceConnection.once('error', reject); + voiceConnection.once('disconnect', () => this.connections.delete(channel.guild.id)); + }); + }); + } +} + +/** + * Represents a Pending Voice Connection + * @private + */ +class PendingVoiceConnection extends EventEmitter { + constructor(voiceManager, channel) { + super(); + + /** + * The ClientVoiceManager that instantiated this pending connection + * @type {ClientVoiceManager} + */ + this.voiceManager = voiceManager; + + /** + * The channel that this pending voice connection will attempt to join + * @type {VoiceChannel} + */ + this.channel = channel; + + /** + * The timeout that will be invoked after 15 seconds signifying a failure to connect + * @type {Timeout} + */ + this.deathTimer = this.voiceManager.client.setTimeout( + () => this.fail(new Error('Connection not established within 15 seconds.')), 15000); + + /** + * An object containing data required to connect to the voice servers with + * @type {Object} + */ + this.data = {}; + + this.sendVoiceStateUpdate(); + } + + checkReady() { + if (this.data.token && this.data.endpoint && this.data.session_id) { + this.pass(); + return true; + } else { + return false; + } + } + + /** + * Set the token and endpoint required to connect to the the voice servers + * @param {string} token the token + * @param {string} endpoint the endpoint + * @returns {void} + */ + setTokenAndEndpoint(token, endpoint) { + if (!token) { + this.fail(new Error('Token not provided from voice server packet.')); + return; + } + if (!endpoint) { + this.fail(new Error('Endpoint not provided from voice server packet.')); + return; + } + if (this.data.token) { + this.fail(new Error('There is already a registered token for this connection.')); + return; + } + if (this.data.endpoint) { + this.fail(new Error('There is already a registered endpoint for this connection.')); + return; + } + + endpoint = endpoint.match(/([^:]*)/)[0]; + + if (!endpoint) { + this.fail(new Error('Failed to find an endpoint.')); + return; + } + + this.data.token = token; + this.data.endpoint = endpoint; + + this.checkReady(); + } + + /** + * Sets the Session ID for the connection + * @param {string} sessionID the session ID + */ + setSessionID(sessionID) { + if (!sessionID) { + this.fail(new Error('Session ID not supplied.')); + return; + } + if (this.data.session_id) { + this.fail(new Error('There is already a registered session ID for this connection.')); + return; + } + this.data.session_id = sessionID; + + this.checkReady(); + } + + clean() { + clearInterval(this.deathTimer); + this.emit('fail', new Error('Clean-up triggered :fourTriggered:')); + } + + pass() { + clearInterval(this.deathTimer); + this.emit('pass', this.upgrade()); + } + + fail(reason) { + this.emit('fail', reason); + this.clean(); + } + + sendVoiceStateUpdate() { + try { + this.voiceManager.sendVoiceStateUpdate(this.channel); + } catch (error) { + this.fail(error); + } + } + + /** + * Upgrades this Pending Connection to a full Voice Connection + * @returns {VoiceConnection} + */ + upgrade() { + return new VoiceConnection(this); + } +} + +module.exports = ClientVoiceManager; 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..ac44ff8 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceConnection.js @@ -0,0 +1,276 @@ +const VoiceWebSocket = require('./VoiceWebSocket'); +const VoiceUDP = require('./VoiceUDPClient'); +const Constants = require('../../util/Constants'); +const AudioPlayer = require('./player/AudioPlayer'); +const VoiceReceiver = require('./receiver/VoiceReceiver'); +const EventEmitter = require('events').EventEmitter; +const fs = require('fs'); + +/** + * Represents a connection to a voice channel in Discord. + * ```js + * // obtained using: + * voiceChannel.join().then(connection => { + * + * }); + * ``` + * @extends {EventEmitter} + */ +class VoiceConnection extends EventEmitter { + constructor(pendingConnection) { + super(); + + /** + * The Voice Manager that instantiated this connection + * @type {ClientVoiceManager} + */ + this.voiceManager = pendingConnection.voiceManager; + + /** + * The voice channel this connection is currently serving + * @type {VoiceChannel} + */ + this.channel = pendingConnection.channel; + + /** + * 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 = pendingConnection.data; + + /** + * The audio player for this voice connection + * @type {AudioPlayer} + */ + this.player = new AudioPlayer(this); + + this.player.on('debug', m => { + /** + * Debug info from the connection + * @event VoiceConnection#debug + * @param {string} message the debug message + */ + this.emit('debug', `audio player - ${m}`); + }); + + this.player.on('error', e => { + /** + * Warning info from the connection + * @event VoiceConnection#warn + * @param {string|Error} warning the warning + */ + this.emit('warn', e); + this.player.cleanup(); + }); + + /** + * Map SSRC to speaking values + * @type {Map<number, boolean>} + * @private + */ + this.ssrcMap = new Map(); + + /** + * Whether this connection is ready + * @type {boolean} + * @private + */ + this.ready = false; + + /** + * Object that wraps contains the `ws` and `udp` sockets of this voice connection + * @type {Object} + * @private + */ + this.sockets = {}; + this.connect(); + } + + /** + * 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; + this.speaking = value; + this.sockets.ws.sendPacket({ + op: Constants.VoiceOPCodes.SPEAKING, + d: { + speaking: true, + delay: 0, + }, + }).catch(e => { + this.emit('debug', e); + }); + } + + /** + * Disconnect the voice connection, causing a disconnect and closing event to be emitted. + */ + disconnect() { + this.emit('closing'); + this.voiceManager.client.ws.send({ + op: Constants.OPCodes.VOICE_STATE_UPDATE, + d: { + guild_id: this.channel.guild.id, + channel_id: null, + self_mute: false, + self_deaf: false, + }, + }); + /** + * Emitted when the voice connection disconnects + * @event VoiceConnection#disconnect + */ + this.emit('disconnect'); + } + + /** + * Connect the voice connection + * @private + */ + connect() { + 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.'); + this.sockets.ws = new VoiceWebSocket(this); + this.sockets.udp = new VoiceUDP(this); + this.sockets.ws.on('error', e => this.emit('error', e)); + this.sockets.udp.on('error', e => this.emit('error', e)); + this.sockets.ws.once('ready', d => { + this.authentication.port = d.port; + this.authentication.ssrc = d.ssrc; + /** + * Emitted whenever the connection encounters an error. + * @event VoiceConnection#error + * @param {Error} error the encountered error + */ + this.sockets.udp.findEndpointAddress() + .then(address => { + this.sockets.udp.createUDPSocket(address); + }, e => this.emit('error', e)); + }); + this.sockets.ws.once('sessionDescription', (mode, secret) => { + this.authentication.encryptionMode = mode; + this.authentication.secretKey = secret; + /** + * 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'); + this.ready = true; + }); + this.sockets.ws.on('speaking', data => { + const guild = this.channel.guild; + const user = this.voiceManager.client.users.get(data.user_id); + this.ssrcMap.set(+data.ssrc, user); + if (!data.speaking) { + for (const receiver of this.receivers) { + const opusStream = receiver.opusStreams.get(user.id); + const pcmStream = receiver.pcmStreams.get(user.id); + if (opusStream) { + opusStream.push(null); + opusStream.open = false; + receiver.opusStreams.delete(user.id); + } + if (pcmStream) { + pcmStream.push(null); + pcmStream.open = false; + receiver.pcmStreams.delete(user.id); + } + } + } + /** + * 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.ready) this.emit('speaking', user, data.speaking); + guild._memberSpeakUpdate(data.user_id, data.speaking); + }); + } + + /** + * 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 + */ + + /** + * Play the given file in the voice connection. + * @param {string} file The 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.playStream(fs.createReadStream(file), 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, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + return this.player.playUnknownStream(stream, options); + } + + /** + * Plays a stream of 16-bit signed stereo PCM at 48KHz. + * @param {ReadableStream} stream The audio stream to play. + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + return this.player.playPCMStream(stream, null, 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..b7b0c0c --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceUDPClient.js @@ -0,0 +1,145 @@ +const udp = require('dgram'); +const dns = require('dns'); +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) { + try { + this.socket.close(); + } catch (e) { + return; + } + this.socket = null; + } + } + + /** + * The port of the discord voice server + * @type {number} + * @readonly + */ + get discordPort() { + return this.voiceConnection.authentication.port; + } + + /** + * Tries to resolve the voice server endpoint to an address + * @returns {Promise<string>} + */ + findEndpointAddress() { + return new Promise((resolve, reject) => { + dns.lookup(this.voiceConnection.authentication.endpoint, (error, address) => { + if (error) { + reject(error); + return; + } + this.discordAddress = address; + resolve(address); + }); + }); + } + + /** + * 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 = new Buffer(70); + blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4); + this.send(blankMessage); + } +} + +function parseLocalPacket(message) { + try { + const packet = new Buffer(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..bafa5dd --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceWebSocket.js @@ -0,0 +1,249 @@ +const Constants = require('../../util/Constants'); +const SecretKey = require('./util/SecretKey'); +const EventEmitter = require('events').EventEmitter; + +let WebSocket; +try { + WebSocket = require('uws'); +} catch (err) { + WebSocket = require('ws'); +} + +/** + * Represents a Voice Connection's WebSocket + * @extends {EventEmitter} + * @private + */ +class VoiceWebSocket extends EventEmitter { + constructor(voiceConnection) { + super(); + + /** + * 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(); + } + + /** + * The client of this voice websocket + * @type {Client} + * @readonly + */ + get client() { + return this.voiceConnection.voiceManager.client; + } + + /** + * 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('error', 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.session_id, + }, + }).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#speaking + */ + this.emit('speaking', 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..e08a365 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js @@ -0,0 +1,307 @@ +const EventEmitter = require('events').EventEmitter; +const NaCl = require('tweetnacl'); + +const nonce = new Buffer(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'); + * }); + * ``` + * @extends {EventEmitter} + */ +class StreamDispatcher extends EventEmitter { + constructor(player, stream, sd, streamOptions) { + super(); + this.player = player; + this.stream = stream; + this.streamingData = { + channels: 2, + count: 0, + sequence: sd.sequence, + timestamp: sd.timestamp, + pausedTime: 0, + }; + this._startStreaming(); + this._triggered = false; + this._volume = streamOptions.volume; + + /** + * 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} + */ + this.passes = streamOptions.passes || 1; + + /** + * Whether playing is paused + * @type {boolean} + */ + this.paused = false; + + this.setVolume(streamOptions.volume || 1); + } + + /** + * 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; + } + + /** + * The volume of the stream, relative to the stream's input volume + * @type {number} + * @readonly + */ + get volume() { + return this._volume; + } + + /** + * 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) { + this._volume = volume; + } + + /** + * Set the volume in decibels + * @param {number} db The decibels + */ + setVolumeDecibels(db) { + this._volume = 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._volume = Math.pow(value, 1.660964); + } + + /** + * 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._triggerTerminalState('end', reason); + } + + _setSpeaking(value) { + 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); + } + + _sendBuffer(buffer, sequence, timestamp) { + let repeats = this.passes; + const packet = this._createPacket(sequence, timestamp, this.player.opusEncoder.encode(buffer)); + while (repeats--) { + this.player.voiceConnection.sockets.udp.send(packet) + .catch(e => this.emit('debug', `Failed to send a packet ${e}`)); + } + } + + _createPacket(sequence, timestamp, buffer) { + const packetBuffer = new Buffer(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 = NaCl.secretbox(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key); + + for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i]; + + return packetBuffer; + } + + _applyVolume(buffer) { + if (this._volume === 1) return buffer; + + const out = new Buffer(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(this._volume * buffer.readInt16LE(i)))); + out.writeInt16LE(uint, i); + } + + return out; + } + + _send() { + try { + if (this._triggered) { + this._setSpeaking(false); + return; + } + + const data = this.streamingData; + + if (data.missed >= 5) { + this._triggerTerminalState('end', 'Stream is not generating quickly enough.'); + return; + } + + if (this.paused) { + // data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0; + data.pausedTime += data.length * 10; + this.player.voiceConnection.voiceManager.client.setTimeout(() => this._send(), data.length * 10); + return; + } + + this._setSpeaking(true); + + if (!data.startTime) { + /** + * Emitted once the dispatcher starts streaming + * @event StreamDispatcher#start + */ + this.emit('start'); + data.startTime = Date.now(); + } + + const bufferLength = 1920 * data.channels; + let buffer = this.stream.read(bufferLength); + if (!buffer) { + data.missed++; + data.pausedTime += data.length * 10; + this.player.voiceConnection.voiceManager.client.setTimeout(() => this._send(), data.length * 10); + return; + } + + data.missed = 0; + + if (buffer.length !== bufferLength) { + const newBuffer = new Buffer(bufferLength).fill(0); + buffer.copy(newBuffer); + buffer = newBuffer; + } + + buffer = this._applyVolume(buffer); + + data.count++; + data.sequence = (data.sequence + 1) < 65536 ? data.sequence + 1 : 0; + data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0; + + 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._send(), nextTime); + } catch (e) { + this._triggerTerminalState('error', e); + } + } + + _triggerEnd(reason) { + /** + * Emitted once the stream has ended. Attach a `once` listener to this. + * @event StreamDispatcher#end + * @param {string} reason The reason for the end of the dispatcher. If it ended because it reached the end of the + * stream, this would be `stream`. If you invoke `.end()` without specifying a reason, this would be `user`. + */ + this.emit('end', reason); + } + + _triggerError(err) { + this.emit('end'); + /** + * Emitted once the stream has encountered an error. Attach a `once` listener to this. Also emits `end`. + * @event StreamDispatcher#error + * @param {Error} err The encountered error + */ + this.emit('error', err); + } + + _triggerTerminalState(state, err) { + if (this._triggered) return; + /** + * Emitted when the stream wants to give debug information. + * @event StreamDispatcher#debug + * @param {string} information The debug information + */ + this.emit('debug', `Triggered terminal state ${state} - stream is now dead`); + this._triggered = true; + this._setSpeaking(false); + switch (state) { + case 'end': + this._triggerEnd(err); + break; + case 'error': + this._triggerError(err); + break; + default: + this.emit('error', 'Unknown trigger state'); + break; + } + } + + _startStreaming() { + if (!this.stream) { + this.emit('error', 'No stream'); + return; + } + + this.stream.on('end', err => this._triggerTerminalState('end', err || 'stream')); + this.stream.on('error', err => this._triggerTerminalState('error', err)); + + const data = this.streamingData; + data.length = 20; + data.missed = 0; + + this.stream.once('readable', () => this._send()); + } + + _setPaused(paused) { + if (paused) { + this.paused = true; + this._setSpeaking(false); + } else { + this.paused = false; + this._setSpeaking(true); + } + } +} + +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..6c3ba6e --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/BaseOpusEngine.js @@ -0,0 +1,15 @@ +class BaseOpus { + constructor(player) { + this.player = player; + } + + encode(buffer) { + return buffer; + } + + decode(buffer) { + return buffer; + } +} + +module.exports = BaseOpus; 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..10f287b --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js @@ -0,0 +1,27 @@ +const OpusEngine = require('./BaseOpusEngine'); + +let opus; + +class NodeOpusEngine extends OpusEngine { + constructor(player) { + super(player); + try { + opus = require('node-opus'); + } catch (err) { + throw err; + } + this.encoder = new opus.OpusEncoder(48000, 2); + } + + 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..ffd512a --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/OpusEngineList.js @@ -0,0 +1,24 @@ +const list = [ + require('./NodeOpusEngine'), + require('./OpusScriptEngine'), +]; + +function fetch(Encoder) { + try { + return new Encoder(); + } catch (err) { + return null; + } +} + +exports.add = encoder => { + list.push(encoder); +}; + +exports.fetch = () => { + for (const encoder of list) { + const fetched = fetch(encoder); + 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..33b4ff5 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/OpusScriptEngine.js @@ -0,0 +1,27 @@ +const OpusEngine = require('./BaseOpusEngine'); + +let OpusScript; + +class NodeOpusEngine extends OpusEngine { + constructor(player) { + super(player); + try { + OpusScript = require('opusscript'); + } catch (err) { + throw err; + } + this.encoder = new OpusScript(48000, 2); + } + + encode(buffer) { + super.encode(buffer); + return this.encoder.encode(buffer, 960); + } + + decode(buffer) { + super.decode(buffer); + return this.encoder.decode(buffer); + } +} + +module.exports = NodeOpusEngine; diff --git a/node_modules/discord.js/src/client/voice/pcm/ConverterEngine.js b/node_modules/discord.js/src/client/voice/pcm/ConverterEngine.js new file mode 100644 index 0000000..6b7502f --- /dev/null +++ b/node_modules/discord.js/src/client/voice/pcm/ConverterEngine.js @@ -0,0 +1,14 @@ +const EventEmitter = require('events').EventEmitter; + +class ConverterEngine extends EventEmitter { + constructor(player) { + super(); + this.player = player; + } + + createConvertStream() { + return; + } +} + +module.exports = ConverterEngine; diff --git a/node_modules/discord.js/src/client/voice/pcm/ConverterEngineList.js b/node_modules/discord.js/src/client/voice/pcm/ConverterEngineList.js new file mode 100644 index 0000000..56d430e --- /dev/null +++ b/node_modules/discord.js/src/client/voice/pcm/ConverterEngineList.js @@ -0,0 +1 @@ +exports.fetch = () => require('./FfmpegConverterEngine'); diff --git a/node_modules/discord.js/src/client/voice/pcm/FfmpegConverterEngine.js b/node_modules/discord.js/src/client/voice/pcm/FfmpegConverterEngine.js new file mode 100644 index 0000000..8fb725b --- /dev/null +++ b/node_modules/discord.js/src/client/voice/pcm/FfmpegConverterEngine.js @@ -0,0 +1,86 @@ +const ConverterEngine = require('./ConverterEngine'); +const ChildProcess = require('child_process'); +const EventEmitter = require('events').EventEmitter; + +class PCMConversionProcess extends EventEmitter { + constructor(process) { + super(); + this.process = process; + this.input = null; + this.process.on('error', e => this.emit('error', e)); + } + + setInput(stream) { + this.input = stream; + stream.pipe(this.process.stdin, { end: false }); + this.input.on('error', e => this.emit('error', e)); + this.process.stdin.on('error', e => this.emit('error', e)); + } + + destroy() { + this.emit('debug', 'destroying a ffmpeg process:'); + if (this.input && this.input.unpipe && this.process.stdin) { + this.input.unpipe(this.process.stdin); + this.emit('unpiped the user input stream from the process input stream'); + } + if (this.process.stdin) { + this.process.stdin.end(); + this.emit('ended the process stdin'); + } + if (this.process.stdin.destroy) { + this.process.stdin.destroy(); + this.emit('destroyed the process stdin'); + } + if (this.process.kill) { + this.process.kill(); + this.emit('killed the process'); + } + } + +} + +class FfmpegConverterEngine extends ConverterEngine { + constructor(player) { + super(player); + this.command = chooseCommand(); + } + + handleError(encoder, err) { + if (encoder.destroy) encoder.destroy(); + this.emit('error', err); + } + + createConvertStream(seek = 0) { + super.createConvertStream(); + const encoder = ChildProcess.spawn(this.command, [ + '-analyzeduration', '0', + '-loglevel', '0', + '-i', '-', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', + '-ss', String(seek), + 'pipe:1', + ], { stdio: ['pipe', 'pipe', 'ignore'] }); + return new PCMConversionProcess(encoder); + } +} + +function chooseCommand() { + for (const cmd of [ + 'ffmpeg', + 'avconv', + './ffmpeg', + './avconv', + 'node_modules\\ffmpeg-binaries\\bin\\ffmpeg', + 'node_modules/ffmpeg-binaries/bin/ffmpeg', + ]) { + if (!ChildProcess.spawnSync(cmd, ['-h']).error) return cmd; + } + throw new Error( + 'FFMPEG was not found on your system, so audio cannot be played. ' + + 'Please make sure FFMPEG is installed and in your PATH.' + ); +} + +module.exports = FfmpegConverterEngine; 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..96c6c24 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/AudioPlayer.js @@ -0,0 +1,80 @@ +const PCMConverters = require('../pcm/ConverterEngineList'); +const OpusEncoders = require('../opus/OpusEngineList'); +const EventEmitter = require('events').EventEmitter; +const StreamDispatcher = require('../dispatcher/StreamDispatcher'); + +/** + * Represents the Audio Player of a Voice Connection + * @extends {EventEmitter} + * @private + */ +class AudioPlayer extends EventEmitter { + constructor(voiceConnection) { + super(); + /** + * The voice connection the player belongs to + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + this.audioToPCM = new (PCMConverters.fetch())(); + this.opusEncoder = OpusEncoders.fetch(); + this.currentConverter = null; + /** + * The current stream dispatcher, if a stream is being played + * @type {StreamDispatcher} + */ + this.dispatcher = null; + this.audioToPCM.on('error', e => this.emit('error', e)); + this.streamingData = { + channels: 2, + count: 0, + sequence: 0, + timestamp: 0, + pausedTime: 0, + }; + this.voiceConnection.on('closing', () => this.cleanup(null, 'voice connection closing')); + } + + playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + stream.on('end', () => { + this.emit('debug', 'Input stream to converter has ended'); + }); + stream.on('error', e => this.emit('error', e)); + const conversionProcess = this.audioToPCM.createConvertStream(options.seek); + conversionProcess.on('error', e => this.emit('error', e)); + conversionProcess.setInput(stream); + return this.playPCMStream(conversionProcess.process.stdout, conversionProcess, options); + } + + cleanup(checkStream, reason) { + // cleanup is a lot less aggressive than v9 because it doesn't try to kill every single stream it is aware of + this.emit('debug', `Clean up triggered due to ${reason}`); + const filter = checkStream && this.dispatcher && this.dispatcher.stream === checkStream; + if (this.currentConverter && (checkStream ? filter : true)) { + this.currentConverter.destroy(); + this.currentConverter = null; + } + } + + playPCMStream(stream, converter, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + stream.on('end', () => this.emit('debug', 'PCM input stream ended')); + this.cleanup(null, 'outstanding play stream'); + this.currentConverter = converter; + if (this.dispatcher) { + this.streamingData = this.dispatcher.streamingData; + } + stream.on('error', e => this.emit('error', e)); + const dispatcher = new StreamDispatcher(this, stream, this.streamingData, options); + dispatcher.on('error', e => this.emit('error', e)); + dispatcher.on('end', () => this.cleanup(dispatcher.stream, 'dispatcher ended')); + dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); + this.dispatcher = dispatcher; + dispatcher.on('debug', m => this.emit('debug', `Stream dispatch - ${m}`)); + return dispatcher; + } + +} + +module.exports = AudioPlayer; diff --git a/node_modules/discord.js/src/client/voice/player/BasePlayer.js b/node_modules/discord.js/src/client/voice/player/BasePlayer.js new file mode 100644 index 0000000..d5285cd --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/BasePlayer.js @@ -0,0 +1,121 @@ +const OpusEngines = require('../opus/OpusEngineList'); +const ConverterEngines = require('../pcm/ConverterEngineList'); +const Constants = require('../../../util/Constants'); +const StreamDispatcher = require('../dispatcher/StreamDispatcher'); +const EventEmitter = require('events').EventEmitter; + +class VoiceConnectionPlayer extends EventEmitter { + constructor(connection) { + super(); + this.connection = connection; + this.opusEncoder = OpusEngines.fetch(); + const Engine = ConverterEngines.fetch(); + this.converterEngine = new Engine(this); + this.converterEngine.on('error', err => { + this._shutdown(); + this.emit('error', err); + }); + this.speaking = false; + this.processMap = new Map(); + this.dispatcher = null; + this._streamingData = { + sequence: 0, + timestamp: 0, + }; + } + + convertStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + const encoder = this.converterEngine.createConvertStream(options.seek); + const pipe = stream.pipe(encoder.stdin, { end: false }); + pipe.on('unpipe', () => { + this.killStream(encoder.stdout); + pipe.destroy(); + }); + this.processMap.set(encoder.stdout, { + pcmConverter: encoder, + inputStream: stream, + }); + return encoder.stdout; + } + + _shutdown() { + this.speaking = false; + if (this.dispatcher) this.dispatcher._triggerTerminalState('end', 'ended by parent player shutdown'); + for (const stream of this.processMap.keys()) this.killStream(stream); + } + + killStream(stream) { + const streams = this.processMap.get(stream); + this._streamingData = this.dispatcher.streamingData; + this.emit(Constants.Events.DEBUG, 'Cleaning up player after audio stream ended or encountered an error'); + + const dummyHandler = () => null; + + if (streams) { + this.processMap.delete(stream); + if (streams.inputStream && streams.pcmConverter) { + try { + streams.inputStream.once('error', dummyHandler); + streams.pcmConverter.once('error', dummyHandler); + streams.pcmConverter.stdin.once('error', dummyHandler); + streams.pcmConverter.stdout.once('error', dummyHandler); + if (streams.inputStream.unpipe) { + streams.inputStream.unpipe(streams.pcmConverter.stdin); + this.emit(Constants.Events.DEBUG, '- Unpiped input stream'); + } else if (streams.inputStream.destroy) { + streams.inputStream.destroy(); + this.emit(Constants.Events.DEBUG, '- Couldn\'t unpipe input stream, so destroyed input stream'); + } + if (streams.pcmConverter.stdin) { + streams.pcmConverter.stdin.end(); + this.emit(Constants.Events.DEBUG, '- Ended input stream to PCM converter'); + } + if (streams.pcmConverter && streams.pcmConverter.kill) { + streams.pcmConverter.kill('SIGINT'); + this.emit(Constants.Events.DEBUG, '- Killed the PCM converter'); + } + } catch (err) { + // if an error happened make sure the pcm converter is killed anyway + try { + if (streams.pcmConverter && streams.pcmConverter.kill) { + streams.pcmConverter.kill('SIGINT'); + this.emit(Constants.Events.DEBUG, '- Killed the PCM converter after previous error (abnormal)'); + } + } catch (e) { + return e; + } + return err; + } + } + } + return null; + } + + setSpeaking(value) { + if (this.speaking === value) return; + this.speaking = value; + this.connection.websocket.send({ + op: Constants.VoiceOPCodes.SPEAKING, + d: { + speaking: true, + delay: 0, + }, + }).catch(e => { + this.emit('debug', e); + }); + } + + playPCMStream(pcmStream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + const dispatcher = new StreamDispatcher(this, pcmStream, this._streamingData, options); + dispatcher.on('speaking', value => this.setSpeaking(value)); + dispatcher.on('end', () => this.killStream(pcmStream)); + dispatcher.on('error', () => this.killStream(pcmStream)); + dispatcher.setVolume(options.volume); + this.dispatcher = dispatcher; + return dispatcher; + } +} + +module.exports = VoiceConnectionPlayer; diff --git a/node_modules/discord.js/src/client/voice/player/DefaultPlayer.js b/node_modules/discord.js/src/client/voice/player/DefaultPlayer.js new file mode 100644 index 0000000..b465e8c --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/DefaultPlayer.js @@ -0,0 +1,19 @@ +const BasePlayer = require('./BasePlayer'); +const fs = require('fs'); + +class DefaultPlayer extends BasePlayer { + playFile(file, { seek = 0, volume = 1 } = {}) { + const options = { seek: seek, volume: volume }; + return this.playStream(fs.createReadStream(file), options); + } + + playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + this._shutdown(); + const options = { seek, volume, passes }; + const pcmStream = this.convertStream(stream, options); + const dispatcher = this.playPCMStream(pcmStream, options); + return dispatcher; + } +} + +module.exports = DefaultPlayer; 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..50ace27 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/VoiceReadable.js @@ -0,0 +1,19 @@ +const Readable = require('stream').Readable; + +class VoiceReadable extends Readable { + constructor() { + super(); + this._packets = []; + this.open = true; + } + + _read() { + return; + } + + _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..bc9156f --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/VoiceReceiver.js @@ -0,0 +1,154 @@ +const EventEmitter = require('events').EventEmitter; +const NaCl = require('tweetnacl'); +const Readable = require('./VoiceReadable'); + +const nonce = new Buffer(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(); + + /** + * 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 = this.voiceConnection.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; + return; + } + + /** + * Destroy this VoiceReceiver, also ending any streams that it may be controlling. + */ + destroy() { + this.voiceConnection.sockets.udp.socket.removeListener('message', this._listener); + for (const stream of this.pcmStreams) { + stream[1]._push(null); + this.pcmStreams.delete(stream[0]); + } + for (const stream of this.opusStreams) { + stream[1]._push(null); + this.opusStreams.delete(stream[0]); + } + this.destroyed = true; + } + + /** + * 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 32-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 = NaCl.secretbox.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key); + if (!data) { + /** + * Emitted whenever a voice packet cannot be decrypted + * @event VoiceReceiver#warn + * @param {string} message The warning message + */ + this.emit('warn', 'Failed to decrypt voice packet'); + return; + } + data = new Buffer(data); + 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) { + /** + * 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 + */ + const pcm = this.voiceConnection.player.opusEncoder.decode(data); + if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm); + this.emit('pcm', user, pcm); + } + } +} + +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..508a1ba --- /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 in key) this.key[index] = key[index]; + } +} + +module.exports = SecretKey; 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..b67af59 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/WebSocketManager.js @@ -0,0 +1,373 @@ +const browser = typeof window !== 'undefined'; +const EventEmitter = require('events').EventEmitter; +const Constants = require('../../util/Constants'); +const convertArrayBuffer = require('../../util/ConvertArrayBuffer'); +const pako = require('pako'); +const zlib = require('zlib'); +const PacketManager = require('./packets/WebSocketPacketManager'); + +let WebSocket, erlpack; +let serialize = JSON.stringify; +if (browser) { + WebSocket = window.WebSocket; // eslint-disable-line no-undef +} else { + try { + WebSocket = require('uws'); + } catch (err) { + WebSocket = require('ws'); + } + + try { + erlpack = require('erlpack'); + serialize = erlpack.pack; + } catch (err) { + erlpack = null; + } +} + +/** + * The WebSocket Manager of the Client + * @private + */ +class WebSocketManager extends EventEmitter { + constructor(client) { + super(); + /** + * The Client that instantiated this WebSocketManager + * @type {Client} + */ + this.client = client; + + /** + * A WebSocket Packet manager, it handles all the messages + * @type {PacketManager} + */ + this.packetManager = new PacketManager(this); + + /** + * The status of the WebSocketManager, a type of Constants.Status. It defaults to IDLE. + * @type {number} + */ + this.status = Constants.Status.IDLE; + + /** + * The session ID of the connection, null if not yet available. + * @type {?string} + */ + this.sessionID = null; + + /** + * The packet count of the client, null if not yet available. + * @type {?number} + */ + this.sequence = -1; + + /** + * The gateway address for this WebSocket connection, null if not yet available. + * @type {?string} + */ + this.gateway = null; + + /** + * Whether READY was emitted normally (all packets received) or not + * @type {boolean} + */ + this.normalReady = false; + + /** + * The WebSocket connection to the gateway + * @type {?WebSocket} + */ + this.ws = null; + + /** + * An object with keys that are websocket event names that should be ignored + * @type {Object} + */ + this.disabledEvents = {}; + for (const event of client.options.disabledEvents) this.disabledEvents[event] = true; + + this.first = true; + + this.lastHeartbeatAck = true; + } + + /** + * Connects the client to a given gateway + * @param {string} gateway The gateway to connect to + */ + _connect(gateway) { + this.client.emit('debug', `Connecting to gateway ${gateway}`); + this.normalReady = false; + if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING; + this.ws = new WebSocket(gateway); + if (browser) this.ws.binaryType = 'arraybuffer'; + this.ws.onopen = this.eventOpen.bind(this); + this.ws.onmessage = this.eventMessage.bind(this); + this.ws.onclose = this.eventClose.bind(this); + this.ws.onerror = this.eventError.bind(this); + this._queue = []; + this._remaining = 120; + this.client.setInterval(() => { + this._remaining = 120; + this._remainingReset = Date.now(); + }, 60e3); + } + + connect(gateway) { + gateway = `${gateway}&encoding=${erlpack ? 'etf' : 'json'}`; + if (this.first) { + this._connect(gateway); + this.first = false; + } else { + this.client.setTimeout(() => this._connect(gateway), 5500); + } + } + + heartbeat(normal) { + if (normal && !this.lastHeartbeatAck) { + this.ws.close(1007); + return; + } + + this.client.emit('debug', 'Sending heartbeat'); + this.client._pingTimestamp = Date.now(); + this.client.ws.send({ + op: Constants.OPCodes.HEARTBEAT, + d: this.sequence, + }, true); + + this.lastHeartbeatAck = false; + } + + /** + * Sends a packet to the gateway + * @param {Object} data An object that can be JSON stringified + * @param {boolean} force Whether or not to send the packet immediately + */ + send(data, force = false) { + if (force) { + this._send(serialize(data)); + return; + } + this._queue.push(serialize(data)); + this.doQueue(); + } + + destroy() { + this.ws.close(1000); + this._queue = []; + this.status = Constants.Status.IDLE; + } + + _send(data) { + if (this.ws.readyState === WebSocket.OPEN) { + this.emit('send', data); + this.ws.send(data); + } + } + + doQueue() { + const item = this._queue[0]; + if (!(this.ws.readyState === WebSocket.OPEN && item)) return; + if (this.remaining === 0) { + this.client.setTimeout(this.doQueue.bind(this), Date.now() - this.remainingReset); + return; + } + this._remaining--; + this._send(item); + this._queue.shift(); + this.doQueue(); + } + + /** + * Run whenever the gateway connections opens up + */ + eventOpen() { + this.client.emit('debug', 'Connection to gateway opened'); + this.lastHeartbeatAck = true; + if (this.status === Constants.Status.RECONNECTING) this._sendResume(); + else this._sendNewIdentify(); + } + + /** + * Sends a gateway resume packet, in cases of unexpected disconnections. + */ + _sendResume() { + if (!this.sessionID) { + this._sendNewIdentify(); + return; + } + this.client.emit('debug', 'Identifying as resumed session'); + const payload = { + token: this.client.token, + session_id: this.sessionID, + seq: this.sequence, + }; + + this.send({ + op: Constants.OPCodes.RESUME, + d: payload, + }); + } + + /** + * Sends a new identification packet, in cases of new connections or failed reconnections. + */ + _sendNewIdentify() { + this.reconnecting = false; + const payload = this.client.options.ws; + payload.token = this.client.token; + if (this.client.options.shardCount > 0) { + payload.shard = [Number(this.client.options.shardId), Number(this.client.options.shardCount)]; + } + this.client.emit('debug', 'Identifying as new session'); + this.send({ + op: Constants.OPCodes.IDENTIFY, + d: payload, + }); + this.sequence = -1; + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * Run whenever the connection to the gateway is closed, it will try to reconnect the client. + * @param {CloseEvent} event The WebSocket close event + */ + eventClose(event) { + this.emit('close', event); + this.client.clearInterval(this.client.manager.heartbeatInterval); + this.status = Constants.Status.DISCONNECTED; + this._queue = []; + /** + * Emitted whenever the client websocket is disconnected + * @event Client#disconnect + * @param {CloseEvent} event The WebSocket close event + */ + if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT, event); + if (event.code === 4004) return; + if (event.code === 4010) return; + if (!this.reconnecting && event.code !== 1000) this.tryReconnect(); + } + + /** + * Run whenever a message is received from the WebSocket. Returns `true` if the message + * was handled properly. + * @param {Object} event The received websocket data + * @returns {boolean} + */ + eventMessage(event) { + const data = this.tryParseEventData(event.data); + if (data === null) { + this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE)); + return false; + } + + this.client.emit('raw', data); + + if (data.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(data.d.heartbeat_interval); + return this.packetManager.handle(data); + } + + /** + * Parses the raw data from a websocket event, inflating it if necessary + * @param {*} data Event data + * @returns {Object} + */ + parseEventData(data) { + if (erlpack) { + if (data instanceof ArrayBuffer) data = convertArrayBuffer(data); + return erlpack.unpack(data); + } else { + if (data instanceof ArrayBuffer) data = pako.inflate(data, { to: 'string' }); + else if (data instanceof Buffer) data = zlib.inflateSync(data).toString(); + return JSON.parse(data); + } + } + + /** + * Tries to call `parseEventData()` and return its result, or returns `null` upon thrown errors. + * @param {*} data Event data + * @returns {?Object} + */ + tryParseEventData(data) { + try { + return this.parseEventData(data); + } catch (err) { + return null; + } + } + + /** + * Run whenever an error occurs with the WebSocket connection. Tries to reconnect + * @param {Error} err The encountered error + */ + eventError(err) { + /** + * Emitted whenever the Client encounters a serious connection error + * @event Client#error + * @param {Error} error The encountered error + */ + if (this.client.listenerCount('error') > 0) this.client.emit('error', err); + this.tryReconnect(); + } + + _emitReady(normal = true) { + /** + * 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(); + this.normalReady = normal; + } + + /** + * Runs on new packets before `READY` to see if the Client is ready yet, if it is prepares + * the `READY` event. + */ + checkIfReady() { + if (this.status !== Constants.Status.READY && this.status !== Constants.Status.NEARLY) { + let unavailableCount = 0; + for (const guildID of this.client.guilds.keys()) { + unavailableCount += this.client.guilds.get(guildID).available ? 0 : 1; + } + if (unavailableCount === 0) { + this.status = Constants.Status.NEARLY; + if (this.client.options.fetchAllMembers) { + const promises = this.client.guilds.map(g => g.fetchMembers()); + Promise.all(promises).then(() => this._emitReady(), e => { + this.client.emit(Constants.Events.WARN, 'Error in pre-ready guild member fetching'); + this.client.emit(Constants.Events.ERROR, e); + this._emitReady(); + }); + return; + } + this._emitReady(); + } + } + } + + /** + * Tries to reconnect the client, changing the status to Constants.Status.RECONNECTING. + */ + tryReconnect() { + if (this.status === Constants.Status.RECONNECTING || this.status === Constants.Status.CONNECTING) return; + this.status = Constants.Status.RECONNECTING; + this.ws.close(); + this.packetManager.handleQueue(); + /** + * Emitted when the Client tries to reconnect after being disconnected + * @event Client#reconnecting + */ + this.client.emit(Constants.Events.RECONNECTING); + this.connect(this.client.ws.gateway); + } +} + +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..78f5777 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/WebSocketPacketManager.js @@ -0,0 +1,125 @@ +const Constants = require('../../../util/Constants'); + +const BeforeReadyWhitelist = [ + Constants.WSEvents.READY, + 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(websocketManager) { + this.ws = websocketManager; + this.handlers = {}; + this.queue = []; + + this.register(Constants.WSEvents.READY, require('./handlers/Ready')); + 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.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.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_ALL, require('./handlers/MessageReactionRemoveAll')); + } + + 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]); + this.queue.splice(index, 1); + }); + } + + setSequence(s) { + if (s && s > this.ws.sequence) this.ws.sequence = s; + } + + handle(packet) { + if (packet.op === Constants.OPCodes.RECONNECT) { + this.setSequence(packet.s); + this.ws.tryReconnect(); + return false; + } + + if (packet.op === Constants.OPCodes.INVALID_SESSION) { + if (packet.d) { + setTimeout(() => { + this.ws._sendResume(); + }, 2500); + } else { + this.ws.sessionID = null; + this.ws._sendNewIdentify(); + } + return 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.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 (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..636df81 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelPinsUpdate.js @@ -0,0 +1,31 @@ +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) 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. + * @event Client#channelPinsUpdate + * @param {Channel} channel The channel that the pins update occured in + * @param {Date} time The time of the pins update + */ + +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..c7fbd7e --- /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..c6ebdaa --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildEmojisUpdate.js @@ -0,0 +1,40 @@ +const AbstractHandler = require('./AbstractHandler'); + +function mappify(iterable) { + const map = new Map(); + for (const x of iterable) map.set(...x); + return map; +} + +class GuildEmojisUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = 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 + client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji); + } + } else { + // emoji added + client.actions.GuildEmojiCreate.handle(guild, emoji); + } + } + + for (const emoji of deletions.values()) { + // emoji deleted + client.actions.GuildEmojiDelete.handle(emoji); + } + } +} + +module.exports = GuildEmojisUpdate; 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..02a3c3c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMembersChunk.js @@ -0,0 +1,28 @@ +// ##untested## + +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +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; + + const members = data.members.map(member => guild._addMember(member, false)); + + guild._checkChunks(); + client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members); + + 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 + */ + +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/MessageCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageCreate.js new file mode 100644 index 0000000..058dc85 --- /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..b06ce98 --- /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..6cd3648 --- /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<string, 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/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..09d78a0 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/PresenceUpdate.js @@ -0,0 +1,72 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); +const cloneObject = require('../../../../util/CloneObject'); + +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 = 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) { + const oldMember = cloneObject(member); + if (member.presence) { + oldMember.frozenPresence = 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..10bc6b2 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/Ready.js @@ -0,0 +1,69 @@ +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(); + + 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) 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 in 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); + client.once('ready', client.syncGuilds.bind(client)); + + 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, + }); + } + + client.setTimeout(() => { + if (!client.ws.normalReady) client.ws._emitReady(false); + }, 1200 * data.guilds.length); + + this.packetManager.ws.sessionID = data.session_id; + this.packetManager.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/TypingStart.js b/node_modules/discord.js/src/client/websocket/packets/handlers/TypingStart.js new file mode 100644 index 0000000..1a35ca7 --- /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/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/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..ddbfbfc --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -0,0 +1,49 @@ +const AbstractHandler = require('./AbstractHandler'); + +const Constants = require('../../../../util/Constants'); +const cloneObject = require('../../../../util/CloneObject'); + +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 = 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 && data.channel_id) { + client.emit('self.voiceStateUpdate', data); + } + + const newChannel = client.channels.get(data.channel_id); + if (newChannel) newChannel.members.set(member.user.id, member); + + member.serverMute = data.mute; + member.serverDeaf = data.deaf; + member.selfMute = data.self_mute; + member.selfDeaf = data.self_deaf; + 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/index.js b/node_modules/discord.js/src/index.js new file mode 100644 index 0000000..85326bf --- /dev/null +++ b/node_modules/discord.js/src/index.js @@ -0,0 +1,47 @@ +module.exports = { + Client: require('./client/Client'), + WebhookClient: require('./client/WebhookClient'), + Shard: require('./sharding/Shard'), + ShardClientUtil: require('./sharding/ShardClientUtil'), + ShardingManager: require('./sharding/ShardingManager'), + + Collection: require('./util/Collection'), + splitMessage: require('./util/SplitMessage'), + escapeMarkdown: require('./util/EscapeMarkdown'), + fetchRecommendedShards: require('./util/FetchRecommendedShards'), + + Channel: require('./structures/Channel'), + ClientOAuth2Application: require('./structures/ClientOAuth2Application'), + ClientUser: require('./structures/ClientUser'), + DMChannel: require('./structures/DMChannel'), + Emoji: require('./structures/Emoji'), + EvaluatedPermissions: require('./structures/EvaluatedPermissions'), + Game: require('./structures/Presence').Game, + GroupDMChannel: require('./structures/GroupDMChannel'), + Guild: require('./structures/Guild'), + GuildChannel: require('./structures/GuildChannel'), + GuildMember: require('./structures/GuildMember'), + Invite: require('./structures/Invite'), + Message: require('./structures/Message'), + MessageAttachment: require('./structures/MessageAttachment'), + MessageCollector: require('./structures/MessageCollector'), + MessageEmbed: require('./structures/MessageEmbed'), + MessageReaction: require('./structures/MessageReaction'), + OAuth2Application: require('./structures/OAuth2Application'), + PartialGuild: require('./structures/PartialGuild'), + PartialGuildChannel: require('./structures/PartialGuildChannel'), + PermissionOverwrites: require('./structures/PermissionOverwrites'), + Presence: require('./structures/Presence').Presence, + ReactionEmoji: require('./structures/ReactionEmoji'), + RichEmbed: require('./structures/RichEmbed'), + Role: require('./structures/Role'), + TextChannel: require('./structures/TextChannel'), + User: require('./structures/User'), + VoiceChannel: require('./structures/VoiceChannel'), + Webhook: require('./structures/Webhook'), + + version: require('../package').version, + Constants: require('./util/Constants'), +}; + +if (typeof window !== 'undefined') window.Discord = module.exports; // eslint-disable-line no-undef diff --git a/node_modules/discord.js/src/sharding/Shard.js b/node_modules/discord.js/src/sharding/Shard.js new file mode 100644 index 0000000..ab9b923 --- /dev/null +++ b/node_modules/discord.js/src/sharding/Shard.js @@ -0,0 +1,164 @@ +const childProcess = require('child_process'); +const path = require('path'); +const makeError = require('../util/MakeError'); +const makePlainError = require('../util/MakePlainError'); + +/** + * Represents a Shard spawned by the ShardingManager. + */ +class Shard { + /** + * @param {ShardingManager} manager The sharding manager + * @param {number} id The ID of this shard + * @param {Array} [args=[]] Command line arguments to pass to the script + */ + constructor(manager, id, args = []) { + /** + * Manager that created the shard + * @type {ShardingManager} + */ + this.manager = manager; + + /** + * ID of the shard + * @type {number} + */ + this.id = id; + + /** + * The environment variables for the shard + * @type {Object} + */ + this.env = Object.assign({}, process.env, { + SHARD_ID: this.id, + SHARD_COUNT: this.manager.totalShards, + CLIENT_TOKEN: this.manager.token, + }); + + /** + * Process of the shard + * @type {ChildProcess} + */ + this.process = childProcess.fork(path.resolve(this.manager.file), args, { + env: this.env, + }); + this.process.on('message', this._handleMessage.bind(this)); + this.process.once('exit', () => { + if (this.manager.respawn) this.manager.createShard(this.id); + }); + + this._evals = new Map(); + this._fetches = new Map(); + } + + /** + * Sends a message to the shard's process. + * @param {*} message Message to send to the shard + * @returns {Promise<Shard>} + */ + send(message) { + return new Promise((resolve, reject) => { + const sent = this.process.send(message, err => { + if (err) reject(err); else resolve(this); + }); + if (!sent) throw new Error('Failed to send message to shard\'s process.'); + }); + } + + /** + * Fetches a Client property value of the shard. + * @param {string} prop Name of the Client property to get, using periods for nesting + * @returns {Promise<*>} + * @example + * shard.fetchClientValue('guilds.size').then(count => { + * console.log(`${count} guilds in shard ${shard.id}`); + * }).catch(console.error); + */ + fetchClientValue(prop) { + if (this._fetches.has(prop)) return this._fetches.get(prop); + + const promise = new Promise((resolve, reject) => { + const listener = message => { + if (!message || message._fetchProp !== prop) return; + this.process.removeListener('message', listener); + this._fetches.delete(prop); + resolve(message._result); + }; + this.process.on('message', listener); + + this.send({ _fetchProp: prop }).catch(err => { + this.process.removeListener('message', listener); + this._fetches.delete(prop); + reject(err); + }); + }); + + this._fetches.set(prop, promise); + return promise; + } + + /** + * Evaluates a script on the shard, in the context of the Client. + * @param {string} script JavaScript to run on the shard + * @returns {Promise<*>} Result of the script execution + */ + eval(script) { + if (this._evals.has(script)) return this._evals.get(script); + + const promise = new Promise((resolve, reject) => { + const listener = message => { + if (!message || message._eval !== script) return; + this.process.removeListener('message', listener); + this._evals.delete(script); + if (!message._error) resolve(message._result); else reject(makeError(message._error)); + }; + this.process.on('message', listener); + + this.send({ _eval: script }).catch(err => { + this.process.removeListener('message', listener); + this._evals.delete(script); + reject(err); + }); + }); + + this._evals.set(script, promise); + return promise; + } + + /** + * Handles an IPC message + * @param {*} message Message received + * @private + */ + _handleMessage(message) { + if (message) { + // Shard is requesting a property fetch + if (message._sFetchProp) { + this.manager.fetchClientValues(message._sFetchProp).then( + results => this.send({ _sFetchProp: message._sFetchProp, _result: results }), + err => this.send({ _sFetchProp: message._sFetchProp, _error: makePlainError(err) }) + ); + return; + } + + // Shard is requesting an eval broadcast + if (message._sEval) { + this.manager.broadcastEval(message._sEval).then( + results => this.send({ _sEval: message._sEval, _result: results }), + err => this.send({ _sEval: message._sEval, _error: makePlainError(err) }) + ); + return; + } + } + + /** + * Emitted upon recieving a message from a shard + * @event ShardingManager#message + * @param {Shard} shard Shard that sent the message + * @param {*} message Message that was received + */ + this.manager.emit('message', this, message); + } +} + +module.exports = Shard; diff --git a/node_modules/discord.js/src/sharding/ShardClientUtil.js b/node_modules/discord.js/src/sharding/ShardClientUtil.js new file mode 100644 index 0000000..6449941 --- /dev/null +++ b/node_modules/discord.js/src/sharding/ShardClientUtil.js @@ -0,0 +1,143 @@ +const makeError = require('../util/MakeError'); +const makePlainError = require('../util/MakePlainError'); + +/** + * Helper class for sharded clients spawned as a child process, such as from a ShardingManager + */ +class ShardClientUtil { + /** + * @param {Client} client Client of the current shard + */ + constructor(client) { + this.client = client; + process.on('message', this._handleMessage.bind(this)); + } + + /** + * ID of this shard + * @type {number} + * @readonly + */ + get id() { + return this.client.options.shardId; + } + + /** + * Total number of shards + * @type {number} + * @readonly + */ + get count() { + return this.client.options.shardCount; + } + + /** + * Sends a message to the master process + * @param {*} message Message to send + * @returns {Promise<void>} + */ + send(message) { + return new Promise((resolve, reject) => { + const sent = process.send(message, err => { + if (err) reject(err); else resolve(); + }); + if (!sent) throw new Error('Failed to send message to master process.'); + }); + } + + /** + * Fetches a Client property value of each shard. + * @param {string} prop Name of the Client property to get, using periods for nesting + * @returns {Promise<Array>} + * @example + * client.shard.fetchClientValues('guilds.size').then(results => { + * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`); + * }).catch(console.error); + */ + fetchClientValues(prop) { + return new Promise((resolve, reject) => { + const listener = message => { + if (!message || message._sFetchProp !== prop) return; + process.removeListener('message', listener); + if (!message._error) resolve(message._result); else reject(makeError(message._error)); + }; + process.on('message', listener); + + this.send({ _sFetchProp: prop }).catch(err => { + process.removeListener('message', listener); + reject(err); + }); + }); + } + + /** + * Evaluates a script on all shards, in the context of the Clients. + * @param {string} script JavaScript to run on each shard + * @returns {Promise<Array>} Results of the script execution + */ + broadcastEval(script) { + return new Promise((resolve, reject) => { + const listener = message => { + if (!message || message._sEval !== script) return; + process.removeListener('message', listener); + if (!message._error) resolve(message._result); else reject(makeError(message._error)); + }; + process.on('message', listener); + + this.send({ _sEval: script }).catch(err => { + process.removeListener('message', listener); + reject(err); + }); + }); + } + + /** + * Handles an IPC message + * @param {*} message Message received + * @private + */ + _handleMessage(message) { + if (!message) return; + if (message._fetchProp) { + const props = message._fetchProp.split('.'); + let value = this.client; + for (const prop of props) value = value[prop]; + this._respond('fetchProp', { _fetchProp: message._fetchProp, _result: value }); + } else if (message._eval) { + try { + this._respond('eval', { _eval: message._eval, _result: this.client._eval(message._eval) }); + } catch (err) { + this._respond('eval', { _eval: message._eval, _error: makePlainError(err) }); + } + } + } + + /** + * Sends a message to the master process, emitting an error from the client upon failure + * @param {string} type Type of response to send + * @param {*} message Message to send + * @private + */ + _respond(type, message) { + this.send(message).catch(err => { + err.message = `Error when sending ${type} response to master process: ${err.message}`; + this.client.emit('error', err); + }); + } + + /** + * Creates/gets the singleton of this class + * @param {Client} client Client to use + * @returns {ShardClientUtil} + */ + static singleton(client) { + if (!this._singleton) { + this._singleton = new this(client); + } else { + client.emit('warn', 'Multiple clients created in child process; only the first will handle sharding helpers.'); + } + return this._singleton; + } +} + +module.exports = ShardClientUtil; diff --git a/node_modules/discord.js/src/sharding/ShardingManager.js b/node_modules/discord.js/src/sharding/ShardingManager.js new file mode 100644 index 0000000..671b5d7 --- /dev/null +++ b/node_modules/discord.js/src/sharding/ShardingManager.js @@ -0,0 +1,193 @@ +const path = require('path'); +const fs = require('fs'); +const EventEmitter = require('events').EventEmitter; +const mergeDefault = require('../util/MergeDefault'); +const Shard = require('./Shard'); +const Collection = require('../util/Collection'); +const fetchRecommendedShards = require('../util/FetchRecommendedShards'); + +/** + * This is a utility class that can be used to help you spawn shards of your Client. Each shard is completely separate + * from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely. + * If you do not select an amount of shards, the manager will automatically decide the best amount. + * @extends {EventEmitter} + */ +class ShardingManager extends EventEmitter { + /** + * @param {string} file Path to your shard script file + * @param {Object} [options] Options for the sharding manager + * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto" + * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting + * @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning + * @param {string} [options.token] Token to use for automatic shard count and passing to shards + */ + constructor(file, options = {}) { + super(); + options = mergeDefault({ + totalShards: 'auto', + respawn: true, + shardArgs: [], + token: null, + }, options); + + /** + * Path to the shard script file + * @type {string} + */ + this.file = file; + if (!file) throw new Error('File must be specified.'); + if (!path.isAbsolute(file)) this.file = path.resolve(process.cwd(), file); + const stats = fs.statSync(this.file); + if (!stats.isFile()) throw new Error('File path does not point to a file.'); + + /** + * Amount of shards that this manager is going to spawn + * @type {number|string} + */ + this.totalShards = options.totalShards; + if (this.totalShards !== 'auto') { + if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) { + throw new TypeError('Amount of shards must be a number.'); + } + if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.'); + if (this.totalShards !== Math.floor(this.totalShards)) { + throw new RangeError('Amount of shards must be an integer.'); + } + } + + /** + * Whether shards should automatically respawn upon exiting + * @type {boolean} + */ + this.respawn = options.respawn; + + /** + * An array of arguments to pass to shards. + * @type {string[]} + */ + this.shardArgs = options.shardArgs; + + /** + * Token to use for obtaining the automatic shard count, and passing to shards + * @type {?string} + */ + this.token = options.token ? options.token.replace(/^Bot\s*/i, '') : null; + + /** + * A collection of shards that this manager has spawned + * @type {Collection<number, Shard>} + */ + this.shards = new Collection(); + } + + /** + * Spawns a single shard. + * @param {number} id The ID of the shard to spawn. **This is usually not necessary.** + * @returns {Promise<Shard>} + */ + createShard(id = this.shards.size) { + const shard = new Shard(this, id, this.shardArgs); + this.shards.set(id, shard); + /** + * Emitted upon launching a shard + * @event ShardingManager#launch + * @param {Shard} shard Shard that was launched + */ + this.emit('launch', shard); + return Promise.resolve(shard); + } + + /** + * Spawns multiple shards. + * @param {number} [amount=this.totalShards] Number of shards to spawn + * @param {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds) + * @returns {Promise<Collection<number, Shard>>} + */ + spawn(amount = this.totalShards, delay = 5500) { + if (amount === 'auto') { + return fetchRecommendedShards(this.token).then(count => { + this.totalShards = count; + return this._spawn(count, delay); + }); + } else { + if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.'); + if (amount < 1) throw new RangeError('Amount of shards must be at least 1.'); + if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.'); + return this._spawn(amount, delay); + } + } + + /** + * Actually spawns shards, unlike that poser above >:( + * @param {number} amount Number of shards to spawn + * @param {number} delay How long to wait in between spawning each shard (in milliseconds) + * @returns {Promise<Collection<number, Shard>>} + * @private + */ + _spawn(amount, delay) { + return new Promise(resolve => { + if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`); + this.totalShards = amount; + + this.createShard(); + if (this.shards.size >= this.totalShards) { + resolve(this.shards); + return; + } + + if (delay <= 0) { + while (this.shards.size < this.totalShards) this.createShard(); + resolve(this.shards); + } else { + const interval = setInterval(() => { + this.createShard(); + if (this.shards.size >= this.totalShards) { + clearInterval(interval); + resolve(this.shards); + } + }, delay); + } + }); + } + + /** + * Send a message to all shards. + * @param {*} message Message to be sent to the shards + * @returns {Promise<Shard[]>} + */ + broadcast(message) { + const promises = []; + for (const shard of this.shards.values()) promises.push(shard.send(message)); + return Promise.all(promises); + } + + /** + * Evaluates a script on all shards, in the context of the Clients. + * @param {string} script JavaScript to run on each shard + * @returns {Promise<Array>} Results of the script execution + */ + broadcastEval(script) { + const promises = []; + for (const shard of this.shards.values()) promises.push(shard.eval(script)); + return Promise.all(promises); + } + + /** + * Fetches a Client property value of each shard. + * @param {string} prop Name of the Client property to get, using periods for nesting + * @returns {Promise<Array>} + * @example + * manager.fetchClientValues('guilds.size').then(results => { + * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`); + * }).catch(console.error); + */ + fetchClientValues(prop) { + if (this.shards.size === 0) return Promise.reject(new Error('No shards have been spawned.')); + if (this.shards.size !== this.totalShards) return Promise.reject(new Error('Still spawning shards.')); + const promises = []; + for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop)); + return Promise.all(promises); + } +} + +module.exports = ShardingManager; diff --git a/node_modules/discord.js/src/structures/Channel.js b/node_modules/discord.js/src/structures/Channel.js new file mode 100644 index 0000000..b37b14b --- /dev/null +++ b/node_modules/discord.js/src/structures/Channel.js @@ -0,0 +1,67 @@ +/** + * Represents any channel on Discord + */ +class Channel { + constructor(client, data) { + /** + * The client that instantiated the Channel + * @name Channel#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The type of the channel, either: + * * `dm` - a DM channel + * * `group` - a Group DM channel + * * `text` - a guild text channel + * * `voice` - a guild voice channel + * @type {string} + */ + this.type = null; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The unique ID of the channel + * @type {string} + */ + this.id = data.id; + } + + /** + * The timestamp the channel was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the channel was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Deletes the channel + * @returns {Promise<Channel>} + * @example + * // delete the channel + * channel.delete() + * .then() // success + * .catch(console.error); // log error + */ + delete() { + return this.client.rest.methods.deleteChannel(this); + } +} + +module.exports = Channel; diff --git a/node_modules/discord.js/src/structures/ClientOAuth2Application.js b/node_modules/discord.js/src/structures/ClientOAuth2Application.js new file mode 100644 index 0000000..46e1250 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientOAuth2Application.js @@ -0,0 +1,26 @@ +const User = require('./User'); +const OAuth2Application = require('./OAuth2Application'); + +/** + * Represents the client's OAuth2 Application + * @extends {OAuth2Application} + */ +class ClientOAuth2Application extends OAuth2Application { + setup(data) { + super.setup(data); + + /** + * The app's flags + * @type {number} + */ + this.flags = data.flags; + + /** + * The app's owner + * @type {User} + */ + this.owner = new User(this.client, data.owner); + } +} + +module.exports = ClientOAuth2Application; diff --git a/node_modules/discord.js/src/structures/ClientUser.js b/node_modules/discord.js/src/structures/ClientUser.js new file mode 100644 index 0000000..d526af6 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUser.js @@ -0,0 +1,274 @@ +const User = require('./User'); +const Collection = require('../util/Collection'); + +/** + * Represents the logged in client's Discord user + * @extends {User} + */ +class ClientUser extends User { + setup(data) { + super.setup(data); + + /** + * Whether or not this account has been verified + * @type {boolean} + */ + this.verified = data.verified; + + /** + * The email of this account + * @type {string} + */ + this.email = data.email; + this.localPresence = {}; + this._typing = new Map(); + + /** + * A Collection of friends for the logged in user. + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<string, User>} + */ + this.friends = new Collection(); + + /** + * A Collection of blocked users for the logged in user. + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<string, User>} + */ + this.blocked = new Collection(); + + /** + * A Collection of notes for the logged in user. + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<string, string>} + */ + this.notes = new Collection(); + } + + edit(data) { + return this.client.rest.methods.updateCurrentUser(data); + } + + /** + * Set the username of the logged in Client. + * <info>Changing usernames in Discord is heavily rate limited, with only 2 requests + * every hour. Use this sparingly!</info> + * @param {string} username The new username + * @param {string} [password] Current password (only for user accounts) + * @returns {Promise<ClientUser>} + * @example + * // set username + * client.user.setUsername('discordjs') + * .then(user => console.log(`My new username is ${user.username}`)) + * .catch(console.error); + */ + setUsername(username, password) { + return this.client.rest.methods.updateCurrentUser({ username }, password); + } + + /** + * Changes the email for the client user's account. + * <warn>This is only available when using a user account.</warn> + * @param {string} email New email to change to + * @param {string} password Current password + * @returns {Promise<ClientUser>} + * @example + * // set email + * client.user.setEmail('bob@gmail.com', 'some amazing password 123') + * .then(user => console.log(`My new email is ${user.email}`)) + * .catch(console.error); + */ + setEmail(email, password) { + return this.client.rest.methods.updateCurrentUser({ email }, password); + } + + /** + * Changes the password for the client user's account. + * <warn>This is only available when using a user account.</warn> + * @param {string} newPassword New password to change to + * @param {string} oldPassword Current password + * @returns {Promise<ClientUser>} + * @example + * // set password + * client.user.setPassword('some new amazing password 456', 'some amazing password 123') + * .then(user => console.log('New password set!')) + * .catch(console.error); + */ + setPassword(newPassword, oldPassword) { + return this.client.rest.methods.updateCurrentUser({ password: newPassword }, oldPassword); + } + + /** + * Set the avatar of the logged in Client. + * @param {BufferResolvable|Base64Resolvable} avatar The new avatar + * @returns {Promise<ClientUser>} + * @example + * // set avatar + * client.user.setAvatar('./avatar.png') + * .then(user => console.log(`New avatar set!`)) + * .catch(console.error); + */ + setAvatar(avatar) { + if (avatar.startsWith('data:')) { + return this.client.rest.methods.updateCurrentUser({ avatar }); + } else { + return this.client.resolver.resolveBuffer(avatar).then(data => + this.client.rest.methods.updateCurrentUser({ avatar: data }) + ); + } + } + + /** + * Data resembling a raw Discord presence + * @typedef {Object} PresenceData + * @property {PresenceStatus} [status] Status of the user + * @property {boolean} [afk] Whether the user is AFK + * @property {Object} [game] Game the user is playing + * @property {string} [game.name] Name of the game + * @property {string} [game.url] Twitch stream URL + */ + + /** + * Sets the full presence of the client user. + * @param {PresenceData} data Data for the presence + * @returns {Promise<ClientUser>} + */ + setPresence(data) { + // {"op":3,"d":{"status":"dnd","since":0,"game":null,"afk":false}} + return new Promise(resolve => { + let status = this.localPresence.status || this.presence.status; + let game = this.localPresence.game; + let afk = this.localPresence.afk || this.presence.afk; + + if (!game && this.presence.game) { + game = { + name: this.presence.game.name, + type: this.presence.game.type, + url: this.presence.game.url, + }; + } + + if (data.status) { + if (typeof data.status !== 'string') throw new TypeError('Status must be a string'); + status = data.status; + } + + if (data.game) { + game = data.game; + if (game.url) game.type = 1; + } + + if (typeof data.afk !== 'undefined') afk = data.afk; + afk = Boolean(afk); + + this.localPresence = { status, game, afk }; + this.localPresence.since = 0; + this.localPresence.game = this.localPresence.game || null; + + this.client.ws.send({ + op: 3, + d: this.localPresence, + }); + + this.client._setPresence(this.id, this.localPresence); + + resolve(this); + }); + } + + /** + * A user's status. Must be one of: + * - `online` + * - `idle` + * - `invisible` + * - `dnd` (do not disturb) + * @typedef {string} PresenceStatus + */ + + /** + * Sets the status of the client user. + * @param {PresenceStatus} status Status to change to + * @returns {Promise<ClientUser>} + */ + setStatus(status) { + return this.setPresence({ status }); + } + + /** + * Sets the game the client user is playing. + * @param {string} game Game being played + * @param {string} [streamingURL] Twitch stream URL + * @returns {Promise<ClientUser>} + */ + setGame(game, streamingURL) { + return this.setPresence({ game: { + name: game, + url: streamingURL, + } }); + } + + /** + * Sets/removes the AFK flag for the client user. + * @param {boolean} afk Whether or not the user is AFK + * @returns {Promise<ClientUser>} + */ + setAFK(afk) { + return this.setPresence({ afk }); + } + + /** + * Fetches messages that mentioned the client's user + * @param {Object} [options] Options for the fetch + * @param {number} [options.limit=25] Maximum number of mentions to retrieve + * @param {boolean} [options.roles=true] Whether to include role mentions + * @param {boolean} [options.everyone=true] Whether to include everyone/here mentions + * @param {Guild|string} [options.guild] Limit the search to a specific guild + * @returns {Promise<Message[]>} + */ + fetchMentions(options = { limit: 25, roles: true, everyone: true, guild: null }) { + return this.client.rest.methods.fetchMentions(options); + } + + /** + * Send a friend request + * <warn>This is only available when using a user account.</warn> + * @param {UserResolvable} user The user to send the friend request to. + * @returns {Promise<User>} The user the friend request was sent to. + */ + addFriend(user) { + user = this.client.resolver.resolveUser(user); + return this.client.rest.methods.addFriend(user); + } + + /** + * Remove a friend + * <warn>This is only available when using a user account.</warn> + * @param {UserResolvable} user The user to remove from your friends + * @returns {Promise<User>} The user that was removed + */ + removeFriend(user) { + user = this.client.resolver.resolveUser(user); + return this.client.rest.methods.removeFriend(user); + } + + /** + * Creates a guild + * <warn>This is only available when using a user account.</warn> + * @param {string} name The name of the guild + * @param {string} region The region for the server + * @param {BufferResolvable|Base64Resolvable} [icon=null] The icon for the guild + * @returns {Promise<Guild>} The guild that was created + */ + createGuild(name, region, icon = null) { + if (!icon) return this.client.rest.methods.createGuild({ name, icon, region }); + if (icon.startsWith('data:')) { + return this.client.rest.methods.createGuild({ name, icon, region }); + } else { + return this.client.resolver.resolveBuffer(icon).then(data => + this.client.rest.methods.createGuild({ name, icon: data, region }) + ); + } + } +} + +module.exports = ClientUser; diff --git a/node_modules/discord.js/src/structures/DMChannel.js b/node_modules/discord.js/src/structures/DMChannel.js new file mode 100644 index 0000000..de49c8b --- /dev/null +++ b/node_modules/discord.js/src/structures/DMChannel.js @@ -0,0 +1,60 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Collection = require('../util/Collection'); + +/** + * Represents a direct message channel between two users. + * @extends {Channel} + * @implements {TextBasedChannel} + */ +class DMChannel extends Channel { + constructor(client, data) { + super(client, data); + this.type = 'dm'; + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The recipient on the other end of the DM + * @type {User} + */ + this.recipient = this.client.dataManager.newUser(data.recipients[0]); + + this.lastMessageID = data.last_message_id; + } + + /** + * When concatenated with a string, this automatically concatenates the recipient's mention instead of the + * DM channel object. + * @returns {string} + */ + toString() { + return this.recipient.toString(); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } + fetchMessage() { return; } + fetchMessages() { return; } + fetchPinnedMessages() { return; } + startTyping() { return; } + stopTyping() { return; } + get typing() { return; } + get typingCount() { return; } + createCollector() { return; } + awaitMessages() { return; } + bulkDelete() { return; } + _cacheMessage() { return; } +} + +TextBasedChannel.applyToClass(DMChannel, true); + +module.exports = DMChannel; diff --git a/node_modules/discord.js/src/structures/Emoji.js b/node_modules/discord.js/src/structures/Emoji.js new file mode 100644 index 0000000..d8a62e1 --- /dev/null +++ b/node_modules/discord.js/src/structures/Emoji.js @@ -0,0 +1,140 @@ +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); + +/** + * Represents a custom emoji + */ +class Emoji { + constructor(guild, data) { + /** + * The Client that instantiated this object + * @name Emoji#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: guild.client }); + + /** + * The guild this emoji is part of + * @type {Guild} + */ + this.guild = guild; + + this.setup(data); + } + + setup(data) { + /** + * The ID of the emoji + * @type {string} + */ + this.id = data.id; + + /** + * The name of the emoji + * @type {string} + */ + this.name = data.name; + + /** + * Whether or not this emoji requires colons surrounding it + * @type {boolean} + */ + this.requiresColons = data.require_colons; + + /** + * Whether this emoji is managed by an external service + * @type {boolean} + */ + this.managed = data.managed; + + this._roles = data.roles; + } + + /** + * The timestamp the emoji was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the emoji was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A collection of roles this emoji is active for (empty if all), mapped by role ID. + * @type {Collection<string, Role>} + * @readonly + */ + get roles() { + const roles = new Collection(); + for (const role of this._roles) { + if (this.guild.roles.has(role)) roles.set(role, this.guild.roles.get(role)); + } + return roles; + } + + /** + * The URL to the emoji file + * @type {string} + * @readonly + */ + get url() { + return Constants.Endpoints.emoji(this.id); + } + + /** + * When concatenated with a string, this automatically returns the emoji mention rather than the object. + * @returns {string} + * @example + * // send an emoji: + * const emoji = guild.emojis.first(); + * msg.reply(`Hello! ${emoji}`); + */ + toString() { + return this.requiresColons ? `<:${this.name}:${this.id}>` : this.name; + } + + /** + * Whether this emoji is the same as another one + * @param {Emoji|Object} other the emoji to compare it to + * @returns {boolean} whether the emoji is equal to the given emoji or not + */ + equals(other) { + if (other instanceof Emoji) { + return ( + other.id === this.id && + other.name === this.name && + other.managed === this.managed && + other.requiresColons === this.requiresColons + ); + } else { + return ( + other.id === this.id && + other.name === this.name + ); + } + } + + /** + * The identifier of this emoji, used for message reactions + * @readonly + * @type {string} + */ + get identifier() { + if (this.id) { + return `${this.name}:${this.id}`; + } + return encodeURIComponent(this.name); + } +} + +module.exports = Emoji; diff --git a/node_modules/discord.js/src/structures/EvaluatedPermissions.js b/node_modules/discord.js/src/structures/EvaluatedPermissions.js new file mode 100644 index 0000000..ae8a643 --- /dev/null +++ b/node_modules/discord.js/src/structures/EvaluatedPermissions.js @@ -0,0 +1,67 @@ +const Constants = require('../util/Constants'); + +/** + * The final evaluated permissions for a member in a channel + */ +class EvaluatedPermissions { + constructor(member, raw) { + /** + * The member this permissions refer to + * @type {GuildMember} + */ + this.member = member; + + /** + * A number representing the packed permissions + * @type {number} + */ + this.raw = raw; + } + + /** + * Get an object mapping permission name, e.g. `READ_MESSAGES` to a boolean - whether the user + * can perform this or not. + * @returns {Object<string, boolean>} + */ + serialize() { + const serializedPermissions = {}; + for (const permissionName in Constants.PermissionFlags) { + serializedPermissions[permissionName] = this.hasPermission(permissionName); + } + return serializedPermissions; + } + + /** + * Checks whether the user has a certain permission, e.g. `READ_MESSAGES`. + * @param {PermissionResolvable} permission The permission to check for + * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permission + * @returns {boolean} + */ + hasPermission(permission, explicit = false) { + permission = this.member.client.resolver.resolvePermission(permission); + if (!explicit && (this.raw & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true; + return (this.raw & permission) > 0; + } + + /** + * Checks whether the user has all specified permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions + * @returns {boolean} + */ + hasPermissions(permissions, explicit = false) { + return permissions.every(p => this.hasPermission(p, explicit)); + } + + /** + * Checks whether the user has all specified permissions, and lists any missing permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions + * @returns {PermissionResolvable[]} + */ + missingPermissions(permissions, explicit = false) { + return permissions.filter(p => !this.hasPermission(p, explicit)); + } +} + +module.exports = EvaluatedPermissions; diff --git a/node_modules/discord.js/src/structures/GroupDMChannel.js b/node_modules/discord.js/src/structures/GroupDMChannel.js new file mode 100644 index 0000000..84fe4f9 --- /dev/null +++ b/node_modules/discord.js/src/structures/GroupDMChannel.js @@ -0,0 +1,144 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Collection = require('../util/Collection'); + +/* +{ type: 3, + recipients: + [ { username: 'Charlie', + id: '123', + discriminator: '6631', + avatar: '123' }, + { username: 'Ben', + id: '123', + discriminator: '2055', + avatar: '123' }, + { username: 'Adam', + id: '123', + discriminator: '2406', + avatar: '123' } ], + owner_id: '123', + name: null, + last_message_id: '123', + id: '123', + icon: null } +*/ + +/** + * Represents a Group DM on Discord + * @extends {Channel} + * @implements {TextBasedChannel} + */ +class GroupDMChannel extends Channel { + constructor(client, data) { + super(client, data); + this.type = 'group'; + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The name of this Group DM, can be null if one isn't set. + * @type {string} + */ + this.name = data.name; + + /** + * A hash of the Group DM icon. + * @type {string} + */ + this.icon = data.icon; + + /** + * The user ID of this Group DM's owner. + * @type {string} + */ + this.ownerID = data.owner_id; + + if (!this.recipients) { + /** + * A collection of the recipients of this DM, mapped by their ID. + * @type {Collection<string, User>} + */ + this.recipients = new Collection(); + } + + if (data.recipients) { + for (const recipient of data.recipients) { + const user = this.client.dataManager.newUser(recipient); + this.recipients.set(user.id, user); + } + } + + this.lastMessageID = data.last_message_id; + } + + /** + * The owner of this Group DM. + * @type {User} + * @readonly + */ + get owner() { + return this.client.users.get(this.ownerID); + } + + /** + * Whether this channel equals another channel. It compares all properties, so for most operations + * it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often + * what most users need. + * @param {GroupDMChannel} channel Channel to compare with + * @returns {boolean} + */ + equals(channel) { + const equal = channel && + this.id === channel.id && + this.name === channel.name && + this.icon === channel.icon && + this.ownerID === channel.ownerID; + + if (equal) { + return this.recipients.equals(channel.recipients); + } + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the channel's name instead of the Channel object. + * @returns {string} + * @example + * // logs: Hello from My Group DM! + * console.log(`Hello from ${channel}!`); + * @example + * // logs: Hello from My Group DM! + * console.log(`Hello from ' + channel + '!'); + */ + toString() { + return this.name; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } + fetchMessage() { return; } + fetchMessages() { return; } + fetchPinnedMessages() { return; } + startTyping() { return; } + stopTyping() { return; } + get typing() { return; } + get typingCount() { return; } + createCollector() { return; } + awaitMessages() { return; } + bulkDelete() { return; } + _cacheMessage() { return; } +} + +TextBasedChannel.applyToClass(GroupDMChannel, true); + +module.exports = GroupDMChannel; diff --git a/node_modules/discord.js/src/structures/Guild.js b/node_modules/discord.js/src/structures/Guild.js new file mode 100644 index 0000000..acd5b6f --- /dev/null +++ b/node_modules/discord.js/src/structures/Guild.js @@ -0,0 +1,851 @@ +const User = require('./User'); +const Role = require('./Role'); +const Emoji = require('./Emoji'); +const Presence = require('./Presence').Presence; +const GuildMember = require('./GuildMember'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const cloneObject = require('../util/CloneObject'); +const arraysEqual = require('../util/ArraysEqual'); + +/** + * Represents a guild (or a server) on Discord. + * <info>It's recommended to see if a guild is available before performing operations or reading data from it. You can + * check this with `guild.available`.</info> + */ +class Guild { + constructor(client, data) { + /** + * The Client that created the instance of the the Guild. + * @name Guild#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * A collection of members that are in this guild. The key is the member's ID, the value is the member. + * @type {Collection<string, GuildMember>} + */ + this.members = new Collection(); + + /** + * A collection of channels that are in this guild. The key is the channel's ID, the value is the channel. + * @type {Collection<string, GuildChannel>} + */ + this.channels = new Collection(); + + /** + * A collection of roles that are in this guild. The key is the role's ID, the value is the role. + * @type {Collection<string, Role>} + */ + this.roles = new Collection(); + + /** + * A collection of presences in this guild + * @type {Collection<string, Presence>} + */ + this.presences = new Collection(); + + if (!data) return; + if (data.unavailable) { + /** + * Whether the guild is available to access. If it is not available, it indicates a server outage. + * @type {boolean} + */ + this.available = false; + + /** + * The Unique ID of the Guild, useful for comparisons. + * @type {string} + */ + this.id = data.id; + } else { + this.available = true; + this.setup(data); + } + } + + /** + * Sets up the Guild + * @param {*} data The raw data of the guild + * @private + */ + setup(data) { + /** + * The name of the guild + * @type {string} + */ + this.name = data.name; + + /** + * The hash of the guild icon, or null if there is no icon. + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image, or null if no splash (VIP only) + * @type {?string} + */ + this.splash = data.splash; + + /** + * The region the guild is located in + * @type {string} + */ + this.region = data.region; + + /** + * The full amount of members in this guild as of `READY` + * @type {number} + */ + this.memberCount = data.member_count || this.memberCount; + + /** + * Whether the guild is "large" (has more than 250 members) + * @type {boolean} + */ + this.large = data.large || this.large; + + /** + * An array of guild features. + * @type {Object[]} + */ + this.features = data.features; + + /** + * The ID of the application that created this guild (if applicable) + * @type {?string} + */ + this.applicationID = data.application_id; + + /** + * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji. + * @type {Collection<string, Emoji>} + */ + this.emojis = new Collection(); + for (const emoji of data.emojis) this.emojis.set(emoji.id, new Emoji(this, emoji)); + + /** + * The time in seconds before a user is counted as "away from keyboard". + * @type {?number} + */ + this.afkTimeout = data.afk_timeout; + + /** + * The ID of the voice channel where AFK members are moved. + * @type {?string} + */ + this.afkChannelID = data.afk_channel_id; + + /** + * Whether embedded images are enabled on this guild. + * @type {boolean} + */ + this.embedEnabled = data.embed_enabled; + + /** + * The verification level of the guild. + * @type {number} + */ + this.verificationLevel = data.verification_level; + + /** + * The timestamp the client user joined the guild at + * @type {number} + */ + this.joinedTimestamp = data.joined_at ? new Date(data.joined_at).getTime() : this.joinedTimestamp; + + this.id = data.id; + this.available = !data.unavailable; + this.features = data.features || this.features || []; + + if (data.members) { + this.members.clear(); + for (const guildUser of data.members) this._addMember(guildUser, false); + } + + if (data.owner_id) { + /** + * The user ID of this guild's owner. + * @type {string} + */ + this.ownerID = data.owner_id; + } + + if (data.channels) { + this.channels.clear(); + for (const channel of data.channels) this.client.dataManager.newChannel(channel, this); + } + + if (data.roles) { + this.roles.clear(); + for (const role of data.roles) { + const newRole = new Role(this, role); + this.roles.set(newRole.id, newRole); + } + } + + if (data.presences) { + for (const presence of data.presences) { + this._setPresence(presence.user.id, presence); + } + } + + this._rawVoiceStates = new Collection(); + if (data.voice_states) { + for (const voiceState of data.voice_states) { + this._rawVoiceStates.set(voiceState.user_id, voiceState); + const member = this.members.get(voiceState.user_id); + if (member) { + member.serverMute = voiceState.mute; + member.serverDeaf = voiceState.deaf; + member.selfMute = voiceState.self_mute; + member.selfDeaf = voiceState.self_deaf; + member.voiceSessionID = voiceState.session_id; + member.voiceChannelID = voiceState.channel_id; + this.channels.get(voiceState.channel_id).members.set(member.user.id, member); + } + } + } + } + + /** + * The timestamp the guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the guild was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the client user joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * Gets the URL to this guild's icon (if it has one, otherwise it returns null) + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.guildIcon(this.id, this.icon); + } + + /** + * Gets the URL to this guild's splash (if it has one, otherwise it returns null) + * @type {?string} + * @readonly + */ + get splashURL() { + if (!this.splash) return null; + return Constants.Endpoints.guildSplash(this.id, this.splash); + } + + /** + * The owner of the guild + * @type {GuildMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID); + } + + /** + * If the client is connected to any voice channel in this guild, this will be the relevant VoiceConnection. + * @type {?VoiceConnection} + * @readonly + */ + get voiceConnection() { + if (this.client.browser) return null; + return this.client.voice.connections.get(this.id) || null; + } + + /** + * The `#general` GuildChannel of the server. + * @type {GuildChannel} + * @readonly + */ + get defaultChannel() { + return this.channels.get(this.id); + } + + /** + * Returns the GuildMember form of a User object, if the user is present in the guild. + * @param {UserResolvable} user The user that you want to obtain the GuildMember of + * @returns {?GuildMember} + * @example + * // get the guild member of a user + * const member = guild.member(message.author); + */ + member(user) { + return this.client.resolver.resolveGuildMember(this, user); + } + + /** + * Fetch a collection of banned users in this guild. + * @returns {Promise<Collection<string, User>>} + */ + fetchBans() { + return this.client.rest.methods.getGuildBans(this); + } + + /** + * Fetch a collection of invites to this guild. Resolves with a collection mapping invites by their codes. + * @returns {Promise<Collection<string, Invite>>} + */ + fetchInvites() { + return this.client.rest.methods.getGuildInvites(this); + } + + /** + * Fetch all webhooks for the guild. + * @returns {Collection<Webhook>} + */ + fetchWebhooks() { + return this.client.rest.methods.getGuildWebhooks(this); + } + + /** + * Fetch a single guild member from a user. + * @param {UserResolvable} user The user to fetch the member for + * @returns {Promise<GuildMember>} + */ + fetchMember(user) { + if (this._fetchWaiter) return Promise.reject(new Error('Already fetching guild members.')); + user = this.client.resolver.resolveUser(user); + if (!user) return Promise.reject(new Error('User is not cached. Use Client.fetchUser first.')); + if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); + return this.client.rest.methods.getGuildMember(this, user); + } + + /** + * Fetches all the members in the guild, even if they are offline. If the guild has less than 250 members, + * this should not be necessary. + * @param {string} [query=''] An optional query to provide when fetching members + * @returns {Promise<Guild>} + */ + fetchMembers(query = '') { + return new Promise((resolve, reject) => { + if (this._fetchWaiter) throw new Error('Already fetching guild members in ${this.id}.'); + if (this.memberCount === this.members.size) { + resolve(this); + return; + } + this._fetchWaiter = resolve; + this.client.ws.send({ + op: Constants.OPCodes.REQUEST_GUILD_MEMBERS, + d: { + guild_id: this.id, + query, + limit: 0, + }, + }); + this._checkChunks(); + this.client.setTimeout(() => reject(new Error('Members didn\'t arrive in time.')), 120 * 1000); + }); + } + + /** + * The data for editing a guild + * @typedef {Object} GuildEditData + * @property {string} [name] The name of the guild + * @property {string} [region] The region of the guild + * @property {number} [verificationLevel] The verification level of the guild + * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild + * @property {number} [afkTimeout] The AFK timeout of the guild + * @property {Base64Resolvable} [icon] The icon of the guild + * @property {GuildMemberResolvable} [owner] The owner of the guild + * @property {Base64Resolvable} [splash] The splash screen of the guild + */ + + /** + * Updates the Guild with new information - e.g. a new name. + * @param {GuildEditData} data The data to update the guild with + * @returns {Promise<Guild>} + * @example + * // set the guild name and region + * guild.edit({ + * name: 'Discord Guild', + * region: 'london', + * }) + * .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`)) + * .catch(console.error); + */ + edit(data) { + return this.client.rest.methods.updateGuild(this, data); + } + + /** + * Edit the name of the guild. + * @param {string} name The new name of the guild + * @returns {Promise<Guild>} + * @example + * // edit the guild name + * guild.setName('Discord Guild') + * .then(updated => console.log(`Updated guild name to ${guild.name}`)) + * .catch(console.error); + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Edit the region of the guild. + * @param {string} region The new region of the guild. + * @returns {Promise<Guild>} + * @example + * // edit the guild region + * guild.setRegion('london') + * .then(updated => console.log(`Updated guild region to ${guild.region}`)) + * .catch(console.error); + */ + setRegion(region) { + return this.edit({ region }); + } + + /** + * Edit the verification level of the guild. + * @param {number} verificationLevel The new verification level of the guild + * @returns {Promise<Guild>} + * @example + * // edit the guild verification level + * guild.setVerificationLevel(1) + * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`)) + * .catch(console.error); + */ + setVerificationLevel(verificationLevel) { + return this.edit({ verificationLevel }); + } + + /** + * Edit the AFK channel of the guild. + * @param {ChannelResolvable} afkChannel The new AFK channel + * @returns {Promise<Guild>} + * @example + * // edit the guild AFK channel + * guild.setAFKChannel(channel) + * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`)) + * .catch(console.error); + */ + setAFKChannel(afkChannel) { + return this.edit({ afkChannel }); + } + + /** + * Edit the AFK timeout of the guild. + * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK + * @returns {Promise<Guild>} + * @example + * // edit the guild AFK channel + * guild.setAFKTimeout(60) + * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`)) + * .catch(console.error); + */ + setAFKTimeout(afkTimeout) { + return this.edit({ afkTimeout }); + } + + /** + * Set a new guild icon. + * @param {Base64Resolvable} icon The new icon of the guild + * @returns {Promise<Guild>} + * @example + * // edit the guild icon + * guild.setIcon(fs.readFileSync('./icon.png')) + * .then(updated => console.log('Updated the guild icon')) + * .catch(console.error); + */ + setIcon(icon) { + return this.edit({ icon }); + } + + /** + * Sets a new owner of the guild. + * @param {GuildMemberResolvable} owner The new owner of the guild + * @returns {Promise<Guild>} + * @example + * // edit the guild owner + * guild.setOwner(guilds.members[0]) + * .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`)) + * .catch(console.error); + */ + setOwner(owner) { + return this.edit({ owner }); + } + + /** + * Set a new guild splash screen. + * @param {Base64Resolvable} splash The new splash screen of the guild + * @returns {Promise<Guild>} + * @example + * // edit the guild splash + * guild.setIcon(fs.readFileSync('./splash.png')) + * .then(updated => console.log('Updated the guild splash')) + * .catch(console.error); + */ + setSplash(splash) { + return this.edit({ splash }); + } + + /** + * Bans a user from the guild. + * @param {UserResolvable} user The user to ban + * @param {number} [deleteDays=0] The amount of days worth of messages from this user that should + * also be deleted. Between `0` and `7`. + * @returns {Promise<GuildMember|User|string>} Result object will be resolved as specifically as possible. + * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot + * be resolved, the user ID will be the result. + * @example + * // ban a user + * guild.ban('123123123123'); + */ + ban(user, deleteDays = 0) { + return this.client.rest.methods.banGuildMember(this, user, deleteDays); + } + + /** + * Unbans a user from the guild. + * @param {UserResolvable} user The user to unban + * @returns {Promise<User>} + * @example + * // unban a user + * guild.unban('123123123123') + * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) + * .catch(reject); + */ + unban(user) { + return this.client.rest.methods.unbanGuildMember(this, user); + } + + /** + * Prunes members from the guild based on how long they have been inactive. + * @param {number} days Number of days of inactivity required to kick + * @param {boolean} [dry=false] If true, will return number of users that will be kicked, without actually doing it + * @returns {Promise<number>} The number of members that were/will be kicked + * @example + * // see how many members will be pruned + * guild.pruneMembers(12, true) + * .then(pruned => console.log(`This will prune ${pruned} people!`); + * .catch(console.error); + * @example + * // actually prune the members + * guild.pruneMembers(12) + * .then(pruned => console.log(`I just pruned ${pruned} people!`); + * .catch(console.error); + */ + pruneMembers(days, dry = false) { + if (typeof days !== 'number') throw new TypeError('Days must be a number.'); + return this.client.rest.methods.pruneGuildMembers(this, days, dry); + } + + /** + * Syncs this guild (already done automatically every 30 seconds). + * <warn>This is only available when using a user account.</warn> + */ + sync() { + if (!this.client.user.bot) this.client.syncGuilds([this]); + } + + /** + * Creates a new channel in the guild. + * @param {string} name The name of the new channel + * @param {string} type The type of the new channel, either `text` or `voice` + * @param {Array<PermissionOverwrites|Object>} overwrites Permission overwrites to apply to the new channel + * @returns {Promise<TextChannel|VoiceChannel>} + * @example + * // create a new text channel + * guild.createChannel('new-general', 'text') + * .then(channel => console.log(`Created new channel ${channel}`)) + * .catch(console.error); + */ + createChannel(name, type, overwrites) { + return this.client.rest.methods.createChannel(this, name, type, overwrites); + } + + /** + * Creates a new role in the guild, and optionally updates it with the given information. + * @param {RoleData} [data] The data to update the role with + * @returns {Promise<Role>} + * @example + * // create a new role + * guild.createRole() + * .then(role => console.log(`Created role ${role}`)) + * .catch(console.error); + * @example + * // create a new role with data + * guild.createRole({ name: 'Super Cool People' }) + * .then(role => console.log(`Created role ${role}`)) + * .catch(console.error) + */ + createRole(data) { + const create = this.client.rest.methods.createGuildRole(this); + if (!data) return create; + return create.then(role => role.edit(data)); + } + + /** + * Creates a new custom emoji in the guild. + * @param {BufferResolvable} attachment The image for the emoji. + * @param {string} name The name for the emoji. + * @returns {Promise<Emoji>} The created emoji. + * @example + * // create a new emoji from a url + * guild.createEmoji('https://i.imgur.com/w3duR07.png', 'rip') + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + * @example + * // create a new emoji from a file on your computer + * guild.createEmoji('./memes/banana.png', 'banana') + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + */ + createEmoji(attachment, name) { + return new Promise(resolve => { + if (attachment.startsWith('data:')) { + resolve(this.client.rest.methods.createEmoji(this, attachment, name)); + } else { + this.client.resolver.resolveBuffer(attachment).then(data => + resolve(this.client.rest.methods.createEmoji(this, data, name)) + ); + } + }); + } + + /** + * Delete an emoji. + * @param {Emoji|string} emoji The emoji to delete. + * @returns {Promise} + */ + deleteEmoji(emoji) { + if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); + return this.client.rest.methods.deleteEmoji(emoji); + } + + /** + * Causes the Client to leave the guild. + * @returns {Promise<Guild>} + * @example + * // leave a guild + * guild.leave() + * .then(g => console.log(`Left the guild ${g}`)) + * .catch(console.error); + */ + leave() { + return this.client.rest.methods.leaveGuild(this); + } + + /** + * Causes the Client to delete the guild. + * @returns {Promise<Guild>} + * @example + * // delete a guild + * guild.delete() + * .then(g => console.log(`Deleted the guild ${g}`)) + * .catch(console.error); + */ + delete() { + return this.client.rest.methods.deleteGuild(this); + } + + /** + * Set the position of a role in this guild + * @param {string|Role} role the role to edit, can be a role object or a role ID. + * @param {number} position the new position of the role + * @returns {Promise<Guild>} + */ + setRolePosition(role, position) { + if (typeof role === 'string') { + role = this.roles.get(role); + if (!role) return Promise.reject(new Error('Supplied role is not a role or string.')); + } + + position = Number(position); + if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + + const lowestAffected = Math.min(role.position, position); + const highestAffected = Math.max(role.position, position); + + const rolesToUpdate = this.roles.filter(r => r.position >= lowestAffected && r.position <= highestAffected); + + // stop role positions getting stupidly inflated + if (position > role.position) { + position = rolesToUpdate.first().position; + } else { + position = rolesToUpdate.last().position; + } + + const updatedRoles = []; + + for (const uRole of rolesToUpdate.values()) { + updatedRoles.push({ + id: uRole.id, + position: uRole.id === role.id ? position : uRole.position + (position < role.position ? 1 : -1), + }); + } + + return this.client.rest.methods.setRolePositions(this.id, updatedRoles); + } + + /** + * Whether this Guild equals another Guild. It compares all properties, so for most operations + * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often + * what most users need. + * @param {Guild} guild Guild to compare with + * @returns {boolean} + */ + equals(guild) { + let equal = + guild && + this.id === guild.id && + this.available === !guild.unavailable && + this.splash === guild.splash && + this.region === guild.region && + this.name === guild.name && + this.memberCount === guild.member_count && + this.large === guild.large && + this.icon === guild.icon && + arraysEqual(this.features, guild.features) && + this.ownerID === guild.owner_id && + this.verificationLevel === guild.verification_level && + this.embedEnabled === guild.embed_enabled; + + if (equal) { + if (this.embedChannel) { + if (this.embedChannel.id !== guild.embed_channel_id) equal = false; + } else if (guild.embed_channel_id) { + equal = false; + } + } + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the guild's name instead of the Guild object. + * @returns {string} + * @example + * // logs: Hello from My Guild! + * console.log(`Hello from ${guild}!`); + * @example + * // logs: Hello from My Guild! + * console.log(`Hello from ' + guild + '!'); + */ + toString() { + return this.name; + } + + _addMember(guildUser, emitEvent = true) { + const existing = this.members.has(guildUser.user.id); + if (!(guildUser.user instanceof User)) guildUser.user = this.client.dataManager.newUser(guildUser.user); + + guildUser.joined_at = guildUser.joined_at || 0; + const member = new GuildMember(this, guildUser); + this.members.set(member.id, member); + + if (this._rawVoiceStates && this._rawVoiceStates.has(member.user.id)) { + const voiceState = this._rawVoiceStates.get(member.user.id); + member.serverMute = voiceState.mute; + member.serverDeaf = voiceState.deaf; + member.selfMute = voiceState.self_mute; + member.selfDeaf = voiceState.self_deaf; + member.voiceSessionID = voiceState.session_id; + member.voiceChannelID = voiceState.channel_id; + if (this.client.channels.has(voiceState.channel_id)) { + this.client.channels.get(voiceState.channel_id).members.set(member.user.id, member); + } else { + this.client.emit('warn', `Member ${member.id} added in guild ${this.id} with an uncached voice channel`); + } + } + + /** + * Emitted whenever a user joins a guild. + * @event Client#guildMemberAdd + * @param {GuildMember} member The member that has joined a guild + */ + if (this.client.ws.status === Constants.Status.READY && emitEvent && !existing) { + this.client.emit(Constants.Events.GUILD_MEMBER_ADD, member); + } + + this._checkChunks(); + return member; + } + + _updateMember(member, data) { + const oldMember = cloneObject(member); + + if (data.roles) member._roles = data.roles; + if (typeof data.nick !== 'undefined') member.nickname = data.nick; + + const notSame = member.nickname !== oldMember.nickname || !arraysEqual(member._roles, oldMember._roles); + + if (this.client.ws.status === Constants.Status.READY && notSame) { + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname + * @event Client#guildMemberUpdate + * @param {GuildMember} oldMember The member before the update + * @param {GuildMember} newMember The member after the update + */ + this.client.emit(Constants.Events.GUILD_MEMBER_UPDATE, oldMember, member); + } + + return { + old: oldMember, + mem: member, + }; + } + + _removeMember(guildMember) { + this.members.delete(guildMember.id); + this._checkChunks(); + } + + _memberSpeakUpdate(user, speaking) { + const member = this.members.get(user); + if (member && member.speaking !== speaking) { + member.speaking = speaking; + /** + * Emitted once a guild member starts/stops speaking + * @event Client#guildMemberSpeaking + * @param {GuildMember} member The member that started/stopped speaking + * @param {boolean} speaking Whether or not the member is speaking + */ + this.client.emit(Constants.Events.GUILD_MEMBER_SPEAKING, member, speaking); + } + } + + _setPresence(id, presence) { + if (this.presences.get(id)) { + this.presences.get(id).update(presence); + return; + } + this.presences.set(id, new Presence(presence)); + } + + _checkChunks() { + if (this._fetchWaiter) { + if (this.members.size === this.memberCount) { + this._fetchWaiter(this); + this._fetchWaiter = null; + } + } + } +} + +module.exports = Guild; diff --git a/node_modules/discord.js/src/structures/GuildChannel.js b/node_modules/discord.js/src/structures/GuildChannel.js new file mode 100644 index 0000000..a930789 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildChannel.js @@ -0,0 +1,299 @@ +const Channel = require('./Channel'); +const Role = require('./Role'); +const PermissionOverwrites = require('./PermissionOverwrites'); +const EvaluatedPermissions = require('./EvaluatedPermissions'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); + +/** + * Represents a guild channel (i.e. text channels and voice channels) + * @extends {Channel} + */ +class GuildChannel extends Channel { + constructor(guild, data) { + super(guild.client, data); + + /** + * The guild the channel is in + * @type {Guild} + */ + this.guild = guild; + } + + setup(data) { + super.setup(data); + + /** + * The name of the guild channel + * @type {string} + */ + this.name = data.name; + + /** + * The position of the channel in the list. + * @type {number} + */ + this.position = data.position; + + /** + * A map of permission overwrites in this channel for roles and users. + * @type {Collection<string, PermissionOverwrites>} + */ + this.permissionOverwrites = new Collection(); + if (data.permission_overwrites) { + for (const overwrite of data.permission_overwrites) { + this.permissionOverwrites.set(overwrite.id, new PermissionOverwrites(this, overwrite)); + } + } + } + + /** + * Gets the overall set of permissions for a user in this channel, taking into account roles and permission + * overwrites. + * @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for + * @returns {?EvaluatedPermissions} + */ + permissionsFor(member) { + member = this.client.resolver.resolveGuildMember(this.guild, member); + if (!member) return null; + if (member.id === this.guild.ownerID) return new EvaluatedPermissions(member, Constants.ALL_PERMISSIONS); + + let permissions = 0; + + const roles = member.roles; + for (const role of roles.values()) permissions |= role.permissions; + + const overwrites = this.overwritesFor(member, true, roles); + for (const overwrite of overwrites.role.concat(overwrites.member)) { + permissions &= ~overwrite.deny; + permissions |= overwrite.allow; + } + + const admin = Boolean(permissions & Constants.PermissionFlags.ADMINISTRATOR); + if (admin) permissions = Constants.ALL_PERMISSIONS; + + return new EvaluatedPermissions(member, permissions); + } + + overwritesFor(member, verified = false, roles = null) { + if (!verified) member = this.client.resolver.resolveGuildMember(this.guild, member); + if (!member) return []; + + roles = roles || member.roles; + const roleOverwrites = []; + const memberOverwrites = []; + + for (const overwrite of this.permissionOverwrites.values()) { + if (overwrite.id === member.id) { + memberOverwrites.push(overwrite); + } else if (roles.has(overwrite.id)) { + roleOverwrites.push(overwrite); + } + } + + return { + role: roleOverwrites, + member: memberOverwrites, + }; + } + + /** + * An object mapping permission flags to `true` (enabled) or `false` (disabled) + * ```js + * { + * 'SEND_MESSAGES': true, + * 'ATTACH_FILES': false, + * } + * ``` + * @typedef {Object} PermissionOverwriteOptions + */ + + /** + * Overwrites the permissions for a user or role in this channel. + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The configuration for the update + * @returns {Promise} + * @example + * // overwrite permissions for a message author + * message.channel.overwritePermissions(message.author, { + * SEND_MESSAGES: false + * }) + * .then(() => console.log('Done!')) + * .catch(console.error); + */ + overwritePermissions(userOrRole, options) { + const payload = { + allow: 0, + deny: 0, + }; + + if (userOrRole instanceof Role) { + payload.type = 'role'; + } else if (this.guild.roles.has(userOrRole)) { + userOrRole = this.guild.roles.get(userOrRole); + payload.type = 'role'; + } else { + userOrRole = this.client.resolver.resolveUser(userOrRole); + payload.type = 'member'; + if (!userOrRole) return Promise.reject(new TypeError('Supplied parameter was neither a User nor a Role.')); + } + + payload.id = userOrRole.id; + + const prevOverwrite = this.permissionOverwrites.get(userOrRole.id); + + if (prevOverwrite) { + payload.allow = prevOverwrite.allow; + payload.deny = prevOverwrite.deny; + } + + for (const perm in options) { + if (options[perm] === true) { + payload.allow |= Constants.PermissionFlags[perm] || 0; + payload.deny &= ~(Constants.PermissionFlags[perm] || 0); + } else if (options[perm] === false) { + payload.allow &= ~(Constants.PermissionFlags[perm] || 0); + payload.deny |= Constants.PermissionFlags[perm] || 0; + } else if (options[perm] === null) { + payload.allow &= ~(Constants.PermissionFlags[perm] || 0); + payload.deny &= ~(Constants.PermissionFlags[perm] || 0); + } + } + + return this.client.rest.methods.setChannelOverwrite(this, payload); + } + + /** + * The data for a guild channel + * @typedef {Object} ChannelData + * @property {string} [name] The name of the channel + * @property {number} [position] The position of the channel + * @property {string} [topic] The topic of the text channel + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the channel + */ + + /** + * Edits the channel + * @param {ChannelData} data The new data for the channel + * @returns {Promise<GuildChannel>} + * @example + * // edit a channel + * channel.edit({name: 'new-channel'}) + * .then(c => console.log(`Edited channel ${c}`)) + * .catch(console.error); + */ + edit(data) { + return this.client.rest.methods.updateChannel(this, data); + } + + /** + * Set a new name for the guild channel + * @param {string} name The new name for the guild channel + * @returns {Promise<GuildChannel>} + * @example + * // set a new channel name + * channel.setName('not_general') + * .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`)) + * .catch(console.error); + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Set a new position for the guild channel + * @param {number} position The new position for the guild channel + * @returns {Promise<GuildChannel>} + * @example + * // set a new channel position + * channel.setPosition(2) + * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) + * .catch(console.error); + */ + setPosition(position) { + return this.client.rest.methods.updateChannel(this, { position }); + } + + /** + * Set a new topic for the guild channel + * @param {string} topic The new topic for the guild channel + * @returns {Promise<GuildChannel>} + * @example + * // set a new channel topic + * channel.setTopic('needs more rate limiting') + * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) + * .catch(console.error); + */ + setTopic(topic) { + return this.client.rest.methods.updateChannel(this, { topic }); + } + + /** + * Options given when creating a guild channel invite + * @typedef {Object} InviteOptions + * @property {boolean} [temporary=false] Whether the invite should kick users after 24hrs if they are not given a role + * @property {number} [maxAge=0] Time in seconds the invite expires in + * @property {number} [maxUses=0] Maximum amount of uses for this invite + */ + + /** + * Create an invite to this guild channel + * @param {InviteOptions} [options={}] The options for the invite + * @returns {Promise<Invite>} + */ + createInvite(options = {}) { + return this.client.rest.methods.createChannelInvite(this, options); + } + + /** + * Clone this channel + * @param {string} [name=this.name] Optional name for the new channel, otherwise it has the name of this channel + * @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites + * @returns {Promise<GuildChannel>} + */ + clone(name = this.name, withPermissions = true) { + return this.guild.createChannel(name, this.type, withPermissions ? this.permissionOverwrites : []); + } + + /** + * Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel. + * In most cases, a simple `channel.id === channel2.id` will do, and is much faster too. + * @param {GuildChannel} channel Channel to compare with + * @returns {boolean} + */ + equals(channel) { + let equal = channel && + this.id === channel.id && + this.type === channel.type && + this.topic === channel.topic && + this.position === channel.position && + this.name === channel.name; + + if (equal) { + if (this.permissionOverwrites && channel.permissionOverwrites) { + equal = this.permissionOverwrites.equals(channel.permissionOverwrites); + } else { + equal = !this.permissionOverwrites && !channel.permissionOverwrites; + } + } + + return equal; + } + + /** + * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. + * @returns {string} + * @example + * // Outputs: Hello from #general + * console.log(`Hello from ${channel}`); + * @example + * // Outputs: Hello from #general + * console.log('Hello from ' + channel); + */ + toString() { + return `<#${this.id}>`; + } +} + +module.exports = GuildChannel; diff --git a/node_modules/discord.js/src/structures/GuildMember.js b/node_modules/discord.js/src/structures/GuildMember.js new file mode 100644 index 0000000..60a498a --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildMember.js @@ -0,0 +1,442 @@ +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Role = require('./Role'); +const EvaluatedPermissions = require('./EvaluatedPermissions'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const Presence = require('./Presence').Presence; + +/** + * Represents a member of a guild on Discord + * @implements {TextBasedChannel} + */ +class GuildMember { + constructor(guild, data) { + /** + * The Client that instantiated this GuildMember + * @name GuildMember#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: guild.client }); + + /** + * The guild that this member is part of + * @type {Guild} + */ + this.guild = guild; + + /** + * The user that this guild member instance Represents + * @type {User} + */ + this.user = {}; + + this._roles = []; + if (data) this.setup(data); + + /** + * The ID of the last message sent by the member in their guild, if one was sent. + * @type {?string} + */ + this.lastMessageID = null; + } + + setup(data) { + /** + * Whether this member is deafened server-wide + * @type {boolean} + */ + this.serverDeaf = data.deaf; + + /** + * Whether this member is muted server-wide + * @type {boolean} + */ + this.serverMute = data.mute; + + /** + * Whether this member is self-muted + * @type {boolean} + */ + this.selfMute = data.self_mute; + + /** + * Whether this member is self-deafened + * @type {boolean} + */ + this.selfDeaf = data.self_deaf; + + /** + * The voice session ID of this member, if any + * @type {?string} + */ + this.voiceSessionID = data.session_id; + + /** + * The voice channel ID of this member, if any + * @type {?string} + */ + this.voiceChannelID = data.channel_id; + + /** + * Whether this member is speaking + * @type {boolean} + */ + this.speaking = false; + + /** + * The nickname of this guild member, if they have one + * @type {?string} + */ + this.nickname = data.nick || null; + + /** + * The timestamp the member joined the guild at + * @type {number} + */ + this.joinedTimestamp = new Date(data.joined_at).getTime(); + + this.user = data.user; + this._roles = data.roles; + } + + /** + * The time the member joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * The presence of this guild member + * @type {Presence} + * @readonly + */ + get presence() { + return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(); + } + + /** + * A list of roles that are applied to this GuildMember, mapped by the role ID. + * @type {Collection<string, Role>} + * @readonly + */ + get roles() { + const list = new Collection(); + const everyoneRole = this.guild.roles.get(this.guild.id); + + if (everyoneRole) list.set(everyoneRole.id, everyoneRole); + + for (const roleID of this._roles) { + const role = this.guild.roles.get(roleID); + if (role) list.set(role.id, role); + } + + return list; + } + + /** + * The role of the member with the highest position. + * @type {Role} + * @readonly + */ + get highestRole() { + return this.roles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); + } + + /** + * Whether this member is muted in any way + * @type {boolean} + * @readonly + */ + get mute() { + return this.selfMute || this.serverMute; + } + + /** + * Whether this member is deafened in any way + * @type {boolean} + * @readonly + */ + get deaf() { + return this.selfDeaf || this.serverDeaf; + } + + /** + * The voice channel this member is in, if any + * @type {?VoiceChannel} + * @readonly + */ + get voiceChannel() { + return this.guild.channels.get(this.voiceChannelID); + } + + /** + * The ID of this user + * @type {string} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * The nickname of the member, or their username if they don't have one + * @type {string} + * @readonly + */ + get displayName() { + return this.nickname || this.user.username; + } + + /** + * The overall set of permissions for the guild member, taking only roles into account + * @type {EvaluatedPermissions} + * @readonly + */ + get permissions() { + if (this.user.id === this.guild.ownerID) return new EvaluatedPermissions(this, Constants.ALL_PERMISSIONS); + + let permissions = 0; + const roles = this.roles; + for (const role of roles.values()) permissions |= role.permissions; + + const admin = Boolean(permissions & Constants.PermissionFlags.ADMINISTRATOR); + if (admin) permissions = Constants.ALL_PERMISSIONS; + + return new EvaluatedPermissions(this, permissions); + } + + /** + * Whether the member is kickable by the client user. + * @type {boolean} + * @readonly + */ + get kickable() { + if (this.user.id === this.guild.ownerID) return false; + if (this.user.id === this.client.user.id) return false; + const clientMember = this.guild.member(this.client.user); + if (!clientMember.hasPermission(Constants.PermissionFlags.KICK_MEMBERS)) return false; + return clientMember.highestRole.comparePositionTo(this.highestRole) > 0; + } + + /** + * Whether the member is bannable by the client user. + * @type {boolean} + * @readonly + */ + get bannable() { + if (this.user.id === this.guild.ownerID) return false; + if (this.user.id === this.client.user.id) return false; + const clientMember = this.guild.member(this.client.user); + if (!clientMember.hasPermission(Constants.PermissionFlags.BAN_MEMBERS)) return false; + return clientMember.highestRole.comparePositionTo(this.highestRole) > 0; + } + + /** + * Returns `channel.permissionsFor(guildMember)`. Returns evaluated permissions for a member in a guild channel. + * @param {ChannelResolvable} channel Guild channel to use as context + * @returns {?EvaluatedPermissions} + */ + permissionsIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + if (!channel || !channel.guild) throw new Error('Could not resolve channel to a guild channel.'); + return channel.permissionsFor(this); + } + + /** + * Checks if any of the member's roles have a permission. + * @param {PermissionResolvable} permission The permission to check for + * @param {boolean} [explicit=false] Whether to require the roles to explicitly have the exact permission + * @returns {boolean} + */ + hasPermission(permission, explicit = false) { + if (!explicit && this.user.id === this.guild.ownerID) return true; + return this.roles.some(r => r.hasPermission(permission, explicit)); + } + + /** + * Checks whether the roles of the member allows them to perform specific actions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions + * @returns {boolean} + */ + hasPermissions(permissions, explicit = false) { + if (!explicit && this.user.id === this.guild.ownerID) return true; + return permissions.every(p => this.hasPermission(p, explicit)); + } + + /** + * Checks whether the roles of the member allows them to perform specific actions, and lists any missing permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions + * @returns {PermissionResolvable[]} + */ + missingPermissions(permissions, explicit = false) { + return permissions.filter(p => !this.hasPermission(p, explicit)); + } + + /** + * Edit a guild member + * @param {GuildmemberEditData} data The data to edit the member with + * @returns {Promise<GuildMember>} + */ + edit(data) { + return this.client.rest.methods.updateGuildMember(this, data); + } + + /** + * Mute/unmute a user + * @param {boolean} mute Whether or not the member should be muted + * @returns {Promise<GuildMember>} + */ + setMute(mute) { + return this.edit({ mute }); + } + + /** + * Deafen/undeafen a user + * @param {boolean} deaf Whether or not the member should be deafened + * @returns {Promise<GuildMember>} + */ + setDeaf(deaf) { + return this.edit({ deaf }); + } + + /** + * Moves the guild member to the given channel. + * @param {ChannelResolvable} channel The channel to move the member to + * @returns {Promise<GuildMember>} + */ + setVoiceChannel(channel) { + return this.edit({ channel }); + } + + /** + * Sets the roles applied to the member. + * @param {Collection<string, Role>|Role[]|string[]} roles The roles or role IDs to apply + * @returns {Promise<GuildMember>} + */ + setRoles(roles) { + return this.edit({ roles }); + } + + /** + * Adds a single role to the member. + * @param {Role|string} role The role or ID of the role to add + * @returns {Promise<GuildMember>} + */ + addRole(role) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + return this.client.rest.methods.addMemberRole(this, role); + } + + /** + * Adds multiple roles to the member. + * @param {Collection<string, Role>|Role[]|string[]} roles The roles or role IDs to add + * @returns {Promise<GuildMember>} + */ + addRoles(roles) { + let allRoles; + if (roles instanceof Collection) { + allRoles = this._roles.slice(); + for (const role of roles.values()) allRoles.push(role.id); + } else { + allRoles = this._roles.concat(roles); + } + return this.edit({ roles: allRoles }); + } + + /** + * Removes a single role from the member. + * @param {Role|string} role The role or ID of the role to remove + * @returns {Promise<GuildMember>} + */ + removeRole(role) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + return this.client.rest.methods.removeMemberRole(this, role); + } + + /** + * Removes multiple roles from the member. + * @param {Collection<string, Role>|Role[]|string[]} roles The roles or role IDs to remove + * @returns {Promise<GuildMember>} + */ + removeRoles(roles) { + const allRoles = this._roles.slice(); + if (roles instanceof Collection) { + for (const role of roles.values()) { + const index = allRoles.indexOf(role.id); + if (index >= 0) allRoles.splice(index, 1); + } + } else { + for (const role of roles) { + const index = allRoles.indexOf(role instanceof Role ? role.id : role); + if (index >= 0) allRoles.splice(index, 1); + } + } + return this.edit({ roles: allRoles }); + } + + /** + * Set the nickname for the guild member + * @param {string} nick The nickname for the guild member + * @returns {Promise<GuildMember>} + */ + setNickname(nick) { + return this.edit({ nick }); + } + + /** + * Deletes any DMs with this guild member + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.client.rest.methods.deleteChannel(this); + } + + /** + * Kick this member from the guild + * @returns {Promise<GuildMember>} + */ + kick() { + return this.client.rest.methods.kickGuildMember(this.guild, this); + } + + /** + * Ban this guild member + * @param {number} [deleteDays=0] The amount of days worth of messages from this member that should + * also be deleted. Between `0` and `7`. + * @returns {Promise<GuildMember>} + * @example + * // ban a guild member + * guildMember.ban(7); + */ + ban(deleteDays = 0) { + return this.client.rest.methods.banGuildMember(this.guild, this, deleteDays); + } + + /** + * When concatenated with a string, this automatically concatenates the user's mention instead of the Member object. + * @returns {string} + * @example + * // logs: Hello from <@123456789>! + * console.log(`Hello from ${member}!`); + */ + toString() { + return `<@${this.nickname ? '!' : ''}${this.user.id}>`; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } +} + +TextBasedChannel.applyToClass(GuildMember); + +module.exports = GuildMember; diff --git a/node_modules/discord.js/src/structures/Invite.js b/node_modules/discord.js/src/structures/Invite.js new file mode 100644 index 0000000..b4b34da --- /dev/null +++ b/node_modules/discord.js/src/structures/Invite.js @@ -0,0 +1,159 @@ +const PartialGuild = require('./PartialGuild'); +const PartialGuildChannel = require('./PartialGuildChannel'); +const Constants = require('../util/Constants'); + +/* +{ max_age: 86400, + code: 'CG9A5', + guild: + { splash: null, + id: '123123123', + icon: '123123123', + name: 'name' }, + created_at: '2016-08-28T19:07:04.763368+00:00', + temporary: false, + uses: 0, + max_uses: 0, + inviter: + { username: '123', + discriminator: '4204', + bot: true, + id: '123123123', + avatar: '123123123' }, + channel: { type: 0, id: '123123', name: 'heavy-testing' } } +*/ + +/** + * Represents an invitation to a guild channel. + * <warn>The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing.</warn> + */ +class Invite { + constructor(client, data) { + /** + * The client that instantiated the invite + * @name Invite#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The guild the invite is for. If this guild is already known, this will be a Guild object. If the guild is + * unknown, this will be a PartialGuild object. + * @type {Guild|PartialGuild} + */ + this.guild = this.client.guilds.get(data.guild.id) || new PartialGuild(this.client, data.guild); + + /** + * The code for this invite + * @type {string} + */ + this.code = data.code; + + /** + * Whether or not this invite is temporary + * @type {boolean} + */ + this.temporary = data.temporary; + + /** + * The maximum age of the invite, in seconds + * @type {?number} + */ + this.maxAge = data.max_age; + + /** + * How many times this invite has been used + * @type {number} + */ + this.uses = data.uses; + + /** + * The maximum uses of this invite + * @type {number} + */ + this.maxUses = data.max_uses; + + if (data.inviter) { + /** + * The user who created this invite + * @type {User} + */ + this.inviter = this.client.dataManager.newUser(data.inviter); + } + + /** + * The channel the invite is for. If this channel is already known, this will be a GuildChannel object. + * If the channel is unknown, this will be a PartialGuildChannel object. + * @type {GuildChannel|PartialGuildChannel} + */ + this.channel = this.client.channels.get(data.channel.id) || new PartialGuildChannel(this.client, data.channel); + + /** + * The timestamp the invite was created at + * @type {number} + */ + this.createdTimestamp = new Date(data.created_at).getTime(); + } + + /** + * The time the invite was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The timestamp the invite will expire at + * @type {number} + * @readonly + */ + get expiresTimestamp() { + return this.createdTimestamp + (this.maxAge * 1000); + } + + /** + * The time the invite will expire + * @type {Date} + * @readonly + */ + get expiresAt() { + return new Date(this.expiresTimestamp); + } + + /** + * The URL to the invite + * @type {string} + * @readonly + */ + get url() { + return Constants.Endpoints.inviteLink(this.code); + } + + /** + * Deletes this invite + * @returns {Promise<Invite>} + */ + delete() { + return this.client.rest.methods.deleteInvite(this); + } + + /** + * When concatenated with a string, this automatically concatenates the invite's URL instead of the object. + * @returns {string} + * @example + * // logs: Invite: https://discord.gg/A1b2C3 + * console.log(`Invite: ${invite}`); + */ + toString() { + return this.url; + } +} + +module.exports = Invite; diff --git a/node_modules/discord.js/src/structures/Message.js b/node_modules/discord.js/src/structures/Message.js new file mode 100644 index 0000000..7fcc5b4 --- /dev/null +++ b/node_modules/discord.js/src/structures/Message.js @@ -0,0 +1,568 @@ +const Attachment = require('./MessageAttachment'); +const Embed = require('./MessageEmbed'); +const MessageReaction = require('./MessageReaction'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); +const escapeMarkdown = require('../util/EscapeMarkdown'); + +// Done purely for GuildMember, which would cause a bad circular dependency +const Discord = require('..'); + +/** + * Represents a message on Discord + */ +class Message { + constructor(channel, data, client) { + /** + * The Client that instantiated the Message + * @name Message#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The channel that the message was sent in + * @type {TextChannel|DMChannel|GroupDMChannel} + */ + this.channel = channel; + + if (data) this.setup(data); + } + + setup(data) { // eslint-disable-line complexity + /** + * The ID of the message (unique in the channel it was sent) + * @type {string} + */ + this.id = data.id; + + /** + * The type of the message + * @type {string} + */ + this.type = Constants.MessageTypes[data.type]; + + /** + * The content of the message + * @type {string} + */ + this.content = data.content; + + /** + * The author of the message + * @type {User} + */ + this.author = this.client.dataManager.newUser(data.author); + + /** + * Represents the author of the message as a guild member. Only available if the message comes from a guild + * where the author is still a member. + * @type {GuildMember} + */ + this.member = this.guild ? this.guild.member(this.author) || null : null; + + /** + * Whether or not this message is pinned + * @type {boolean} + */ + this.pinned = data.pinned; + + /** + * Whether or not the message was Text-To-Speech + * @type {boolean} + */ + this.tts = data.tts; + + /** + * A random number used for checking message delivery + * @type {string} + */ + this.nonce = data.nonce; + + /** + * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) + * @type {boolean} + */ + this.system = data.type === 6; + + /** + * A list of embeds in the message - e.g. YouTube Player + * @type {MessageEmbed[]} + */ + this.embeds = data.embeds.map(e => new Embed(this, e)); + + /** + * A collection of attachments in the message - e.g. Pictures - mapped by their ID. + * @type {Collection<string, MessageAttachment>} + */ + this.attachments = new Collection(); + for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment)); + + /** + * The timestamp the message was sent at + * @type {number} + */ + this.createdTimestamp = new Date(data.timestamp).getTime(); + + /** + * The timestamp the message was last edited at (if applicable) + * @type {?number} + */ + this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; + + /** + * An object containing a further users, roles or channels collections + * @type {Object} + * @property {Collection<string, User>} mentions.users Mentioned users, maps their ID to the user object. + * @property {Collection<string, Role>} mentions.roles Mentioned roles, maps their ID to the role object. + * @property {Collection<string, GuildChannel>} mentions.channels Mentioned channels, + * maps their ID to the channel object. + * @property {boolean} mentions.everyone Whether or not @everyone was mentioned. + */ + this.mentions = { + users: new Collection(), + roles: new Collection(), + channels: new Collection(), + everyone: data.mention_everyone, + }; + + for (const mention of data.mentions) { + let user = this.client.users.get(mention.id); + if (user) { + this.mentions.users.set(user.id, user); + } else { + user = this.client.dataManager.newUser(mention); + this.mentions.users.set(user.id, user); + } + } + + if (data.mention_roles) { + for (const mention of data.mention_roles) { + const role = this.channel.guild.roles.get(mention); + if (role) this.mentions.roles.set(role.id, role); + } + } + + if (this.channel.guild) { + const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || []; + for (const raw of channMentionsRaw) { + const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]); + if (chan) this.mentions.channels.set(chan.id, chan); + } + } + + this._edits = []; + + /** + * A collection of reactions to this message, mapped by the reaction "id". + * @type {Collection<string, MessageReaction>} + */ + this.reactions = new Collection(); + + if (data.reactions && data.reactions.length > 0) { + for (const reaction of data.reactions) { + const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name; + this.reactions.set(id, new MessageReaction(this, reaction.emoji, reaction.count, reaction.me)); + } + } + + /** + * ID of the webhook that sent the message, if applicable + * @type {?string} + */ + this.webhookID = data.webhook_id || null; + } + + patch(data) { // eslint-disable-line complexity + if (data.author) { + this.author = this.client.users.get(data.author.id); + if (this.guild) this.member = this.guild.member(this.author); + } + if (data.content) this.content = data.content; + if (data.timestamp) this.createdTimestamp = new Date(data.timestamp).getTime(); + if (data.edited_timestamp) { + this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; + } + if ('tts' in data) this.tts = data.tts; + if ('mention_everyone' in data) this.mentions.everyone = data.mention_everyone; + if (data.nonce) this.nonce = data.nonce; + if (data.embeds) this.embeds = data.embeds.map(e => new Embed(this, e)); + if (data.type > -1) { + this.system = false; + if (data.type === 6) this.system = true; + } + if (data.attachments) { + this.attachments = new Collection(); + for (const attachment of data.attachments) { + this.attachments.set(attachment.id, new Attachment(this, attachment)); + } + } + if (data.mentions) { + for (const mention of data.mentions) { + let user = this.client.users.get(mention.id); + if (user) { + this.mentions.users.set(user.id, user); + } else { + user = this.client.dataManager.newUser(mention); + this.mentions.users.set(user.id, user); + } + } + } + if (data.mention_roles) { + for (const mention of data.mention_roles) { + const role = this.channel.guild.roles.get(mention); + if (role) this.mentions.roles.set(role.id, role); + } + } + if (data.id) this.id = data.id; + if (this.channel.guild && data.content) { + const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || []; + for (const raw of channMentionsRaw) { + const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]); + if (chan) this.mentions.channels.set(chan.id, chan); + } + } + if (data.reactions) { + this.reactions = new Collection(); + if (data.reactions.length > 0) { + for (const reaction of data.reactions) { + const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name; + this.reactions.set(id, new MessageReaction(this, data.emoji, data.count, data.me)); + } + } + } + } + + /** + * The time the message was sent + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the message was last edited at (if applicable) + * @type {?Date} + * @readonly + */ + get editedAt() { + return this.editedTimestamp ? new Date(this.editedTimestamp) : null; + } + + /** + * The guild the message was sent in (if in a guild channel) + * @type {?Guild} + * @readonly + */ + get guild() { + return this.channel.guild || null; + } + + /** + * The message contents with all mentions replaced by the equivalent text. If mentions cannot be resolved to a name, + * the relevant mention in the message content will not be converted. + * @type {string} + * @readonly + */ + get cleanContent() { + return this.content + .replace(/@(everyone|here)/g, '@\u200b$1') + .replace(/<@!?[0-9]+>/g, (input) => { + const id = input.replace(/<|!|>|@/g, ''); + if (this.channel.type === 'dm' || this.channel.type === 'group') { + return this.client.users.has(id) ? `@${this.client.users.get(id).username}` : input; + } + + const member = this.channel.guild.members.get(id); + if (member) { + if (member.nickname) return `@${member.nickname}`; + return `@${member.user.username}`; + } else { + const user = this.client.users.get(id); + if (user) return `@${user.username}`; + return input; + } + }) + .replace(/<#[0-9]+>/g, (input) => { + const channel = this.client.channels.get(input.replace(/<|#|>/g, '')); + if (channel) return `#${channel.name}`; + return input; + }) + .replace(/<@&[0-9]+>/g, (input) => { + if (this.channel.type === 'dm' || this.channel.type === 'group') return input; + const role = this.guild.roles.get(input.replace(/<|@|>|&/g, '')); + if (role) return `@${role.name}`; + return input; + }); + } + + /** + * An array of cached versions of the message, including the current version. + * Sorted from latest (first) to oldest (last). + * @type {Message[]} + * @readonly + */ + get edits() { + const copy = this._edits.slice(); + copy.unshift(this); + return copy; + } + + /** + * Whether the message is editable by the client user. + * @type {boolean} + * @readonly + */ + get editable() { + return this.author.id === this.client.user.id; + } + + /** + * Whether the message is deletable by the client user. + * @type {boolean} + * @readonly + */ + get deletable() { + return this.author.id === this.client.user.id || (this.guild && + this.channel.permissionsFor(this.client.user).hasPermission(Constants.PermissionFlags.MANAGE_MESSAGES) + ); + } + + /** + * Whether the message is pinnable by the client user. + * @type {boolean} + * @readonly + */ + get pinnable() { + return !this.guild || + this.channel.permissionsFor(this.client.user).hasPermission(Constants.PermissionFlags.MANAGE_MESSAGES); + } + + /** + * Whether or not a user, channel or role is mentioned in this message. + * @param {GuildChannel|User|Role|string} data either a guild channel, user or a role object, or a string representing + * the ID of any of these. + * @returns {boolean} + */ + isMentioned(data) { + data = data && data.id ? data.id : data; + return this.mentions.users.has(data) || this.mentions.channels.has(data) || this.mentions.roles.has(data); + } + + /** + * Whether or not a guild member is mentioned in this message. Takes into account + * user mentions, role mentions, and @everyone/@here mentions. + * @param {GuildMember|User} member Member/user to check for a mention of + * @returns {boolean} + */ + isMemberMentioned(member) { + if (this.mentions.everyone) return true; + if (this.mentions.users.has(member.id)) return true; + if (member instanceof Discord.GuildMember && member.roles.some(r => this.mentions.roles.has(r.id))) return true; + return false; + } + + /** + * Options that can be passed into editMessage + * @typedef {Object} MessageEditOptions + * @property {Object} [embed] An embed to be added/edited + * @property {string|boolean} [code] Language for optional codeblock formatting to apply + */ + + /** + * Edit the content of the message + * @param {StringResolvable} [content] The new content for the message + * @param {MessageEditOptions} [options] The options to provide + * @returns {Promise<Message>} + * @example + * // update the content of a message + * message.edit('This is my new content!') + * .then(msg => console.log(`Updated the content of a message from ${msg.author}`)) + * .catch(console.error); + */ + edit(content, options) { + if (!options && typeof content === 'object') { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.client.rest.methods.updateMessage(this, content, options); + } + + /** + * Edit the content of the message, with a code block + * @param {string} lang Language for the code block + * @param {StringResolvable} content The new content for the message + * @returns {Promise<Message>} + */ + editCode(lang, content) { + content = escapeMarkdown(this.client.resolver.resolveString(content), true); + return this.edit(`\`\`\`${lang || ''}\n${content}\n\`\`\``); + } + + /** + * Pins this message to the channel's pinned messages + * @returns {Promise<Message>} + */ + pin() { + return this.client.rest.methods.pinMessage(this); + } + + /** + * Unpins this message from the channel's pinned messages + * @returns {Promise<Message>} + */ + unpin() { + return this.client.rest.methods.unpinMessage(this); + } + + /** + * Add a reaction to the message + * @param {string|Emoji|ReactionEmoji} emoji Emoji to react with + * @returns {Promise<MessageReaction>} + */ + react(emoji) { + emoji = this.client.resolver.resolveEmojiIdentifier(emoji); + if (!emoji) throw new TypeError('Emoji must be a string or Emoji/ReactionEmoji'); + + return this.client.rest.methods.addMessageReaction(this, emoji); + } + + /** + * Remove all reactions from a message + * @returns {Promise<Message>} + */ + clearReactions() { + return this.client.rest.methods.removeMessageReactions(this); + } + + /** + * Deletes the message + * @param {number} [timeout=0] How long to wait to delete the message in milliseconds + * @returns {Promise<Message>} + * @example + * // delete a message + * message.delete() + * .then(msg => console.log(`Deleted message from ${msg.author}`)) + * .catch(console.error); + */ + delete(timeout = 0) { + if (timeout <= 0) { + return this.client.rest.methods.deleteMessage(this); + } else { + return new Promise(resolve => { + this.client.setTimeout(() => { + resolve(this.delete()); + }, timeout); + }); + } + } + + /** + * Reply to the message + * @param {StringResolvable} content The content for the message + * @param {MessageOptions} [options = {}] The options to provide + * @returns {Promise<Message|Message[]>} + * @example + * // reply to a message + * message.reply('Hey, I\'m a reply!') + * .then(msg => console.log(`Sent a reply to ${msg.author}`)) + * .catch(console.error); + */ + reply(content, options = {}) { + content = `${this.guild || this.channel.type === 'group' ? `${this.author}, ` : ''}${content}`; + return this.channel.send(content, options); + } + + /** + * Fetches the webhook used to create this message. + * @returns {Promise<?Webhook>} + */ + fetchWebhook() { + if (!this.webhookID) return Promise.reject(new Error('The message was not sent by a webhook.')); + return this.client.fetchWebhook(this.webhookID); + } + + /** + * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages + * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This + * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties. + * @param {Message} message The message to compare it to + * @param {Object} rawData Raw data passed through the WebSocket about this message + * @returns {boolean} + */ + equals(message, rawData) { + if (!message) return false; + const embedUpdate = !message.author && !message.attachments; + if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length; + + let equal = this.id === message.id && + this.author.id === message.author.id && + this.content === message.content && + this.tts === message.tts && + this.nonce === message.nonce && + this.embeds.length === message.embeds.length && + this.attachments.length === message.attachments.length; + + if (equal && rawData) { + equal = this.mentions.everyone === message.mentions.everyone && + this.createdTimestamp === new Date(rawData.timestamp).getTime() && + this.editedTimestamp === new Date(rawData.edited_timestamp).getTime(); + } + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the message's content instead of the object. + * @returns {string} + * @example + * // logs: Message: This is a message! + * console.log(`Message: ${message}`); + */ + toString() { + return this.content; + } + + _addReaction(emoji, user) { + const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; + let reaction; + if (this.reactions.has(emojiID)) { + reaction = this.reactions.get(emojiID); + if (!reaction.me) reaction.me = user.id === this.client.user.id; + } else { + reaction = new MessageReaction(this, emoji, 0, user.id === this.client.user.id); + this.reactions.set(emojiID, reaction); + } + if (!reaction.users.has(user.id)) { + reaction.users.set(user.id, user); + reaction.count++; + return reaction; + } + return null; + } + + _removeReaction(emoji, user) { + const emojiID = emoji.id || emoji; + if (this.reactions.has(emojiID)) { + const reaction = this.reactions.get(emojiID); + if (reaction.users.has(user.id)) { + reaction.users.delete(user.id); + reaction.count--; + if (user.id === this.client.user.id) reaction.me = false; + return reaction; + } + } + return null; + } + + _clearReactions() { + this.reactions.clear(); + } +} + +module.exports = Message; diff --git a/node_modules/discord.js/src/structures/MessageAttachment.js b/node_modules/discord.js/src/structures/MessageAttachment.js new file mode 100644 index 0000000..29dfb52 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageAttachment.js @@ -0,0 +1,68 @@ +/** + * Represents an attachment in a message + */ +class MessageAttachment { + constructor(message, data) { + /** + * The Client that instantiated this MessageAttachment. + * @name MessageAttachment#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: message.client }); + + /** + * The message this attachment is part of. + * @type {Message} + */ + this.message = message; + + this.setup(data); + } + + setup(data) { + /** + * The ID of this attachment + * @type {string} + */ + this.id = data.id; + + /** + * The file name of this attachment + * @type {string} + */ + this.filename = data.filename; + + /** + * The size of this attachment in bytes + * @type {number} + */ + this.filesize = data.size; + + /** + * The URL to this attachment + * @type {string} + */ + this.url = data.url; + + /** + * The Proxy URL to this attachment + * @type {string} + */ + this.proxyURL = data.proxy_url; + + /** + * The height of this attachment (if an image) + * @type {?number} + */ + this.height = data.height; + + /** + * The width of this attachment (if an image) + * @type {?number} + */ + this.width = data.width; + } +} + +module.exports = MessageAttachment; diff --git a/node_modules/discord.js/src/structures/MessageCollector.js b/node_modules/discord.js/src/structures/MessageCollector.js new file mode 100644 index 0000000..f84ecbd --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageCollector.js @@ -0,0 +1,151 @@ +const EventEmitter = require('events').EventEmitter; +const Collection = require('../util/Collection'); + +/** + * Collects messages based on a specified filter, then emits them. + * @extends {EventEmitter} + */ +class MessageCollector extends EventEmitter { + /** + * A function that takes a Message object and a MessageCollector and returns a boolean. + * ```js + * function(message, collector) { + * if (message.content.includes('discord')) { + * return true; // passed the filter test + * } + * return false; // failed the filter test + * } + * ``` + * @typedef {Function} CollectorFilterFunction + */ + + /** + * An object containing options used to configure a MessageCollector. All properties are optional. + * @typedef {Object} CollectorOptions + * @property {number} [time] Duration for the collector in milliseconds + * @property {number} [max] Maximum number of messages to handle + * @property {number} [maxMatches] Maximum number of successfully filtered messages to obtain + */ + + /** + * @param {Channel} channel The channel to collect messages in + * @param {CollectorFilterFunction} filter The filter function + * @param {CollectorOptions} [options] Options for the collector + */ + constructor(channel, filter, options = {}) { + super(); + + /** + * The channel this collector is operating on + * @type {Channel} + */ + this.channel = channel; + + /** + * A function used to filter messages that the collector collects. + * @type {CollectorFilterFunction} + */ + this.filter = filter; + + /** + * Options for the collecor. + * @type {CollectorOptions} + */ + this.options = options; + + /** + * Whether this collector has stopped collecting messages. + * @type {boolean} + */ + this.ended = false; + + /** + * A collection of collected messages, mapped by message ID. + * @type {Collection<string, Message>} + */ + this.collected = new Collection(); + + this.listener = message => this.verify(message); + this.channel.client.on('message', this.listener); + if (options.time) this.channel.client.setTimeout(() => this.stop('time'), options.time); + } + + /** + * Verifies a message against the filter and options + * @private + * @param {Message} message The message + * @returns {boolean} + */ + verify(message) { + if (this.channel ? this.channel.id !== message.channel.id : false) return false; + if (this.filter(message, this)) { + this.collected.set(message.id, message); + /** + * Emitted whenever the collector receives a message that passes the filter test. + * @param {Message} message The received message + * @param {MessageCollector} collector The collector the message passed through + * @event MessageCollector#message + */ + this.emit('message', message, this); + if (this.collected.size >= this.options.maxMatches) this.stop('matchesLimit'); + else if (this.options.max && this.collected.size === this.options.max) this.stop('limit'); + return true; + } + return false; + } + + /** + * Returns a promise that resolves when a valid message is sent. Rejects + * with collected messages if the Collector ends before receiving a message. + * @type {Promise<Message>} + * @readonly + */ + get next() { + return new Promise((resolve, reject) => { + if (this.ended) { + reject(this.collected); + return; + } + + const cleanup = () => { + this.removeListener('message', onMessage); + this.removeListener('end', onEnd); + }; + + const onMessage = (...args) => { + cleanup(); + resolve(...args); + }; + + const onEnd = (...args) => { + cleanup(); + reject(...args); + }; + + this.once('message', onMessage); + this.once('end', onEnd); + }); + } + + /** + * Stops the collector and emits `end`. + * @param {string} [reason='user'] An optional reason for stopping the collector + */ + stop(reason = 'user') { + if (this.ended) return; + this.ended = true; + this.channel.client.removeListener('message', this.listener); + /** + * Emitted when the Collector stops collecting. + * @param {Collection<string, Message>} collection A collection of messages collected + * during the lifetime of the collector, mapped by the ID of the messages. + * @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time + * limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it + * ended because it reached its message limit, it will be `limit`. + * @event MessageCollector#end + */ + this.emit('end', this.collected, reason); + } +} + +module.exports = MessageCollector; diff --git a/node_modules/discord.js/src/structures/MessageEmbed.js b/node_modules/discord.js/src/structures/MessageEmbed.js new file mode 100644 index 0000000..1249c42 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageEmbed.js @@ -0,0 +1,293 @@ +/** + * Represents an embed in a message (image/video preview, rich embed, etc.) + */ +class MessageEmbed { + constructor(message, data) { + /** + * The client that instantiated this embed + * @name MessageEmbed#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: message.client }); + + /** + * The message this embed is part of + * @type {Message} + */ + this.message = message; + + this.setup(data); + } + + setup(data) { + /** + * The type of this embed + * @type {string} + */ + this.type = data.type; + + /** + * The title of this embed, if there is one + * @type {?string} + */ + this.title = data.title; + + /** + * The description of this embed, if there is one + * @type {?string} + */ + this.description = data.description; + + /** + * The URL of this embed + * @type {string} + */ + this.url = data.url; + + /** + * The color of the embed + * @type {number} + */ + this.color = data.color; + + /** + * The fields of this embed + * @type {MessageEmbedField[]} + */ + this.fields = []; + if (data.fields) for (const field of data.fields) this.fields.push(new MessageEmbedField(this, field)); + + /** + * The timestamp of this embed + * @type {number} + */ + this.createdTimestamp = data.timestamp; + + /** + * The thumbnail of this embed, if there is one + * @type {MessageEmbedThumbnail} + */ + this.thumbnail = data.thumbnail ? new MessageEmbedThumbnail(this, data.thumbnail) : null; + + /** + * The author of this embed, if there is one + * @type {MessageEmbedAuthor} + */ + this.author = data.author ? new MessageEmbedAuthor(this, data.author) : null; + + /** + * The provider of this embed, if there is one + * @type {MessageEmbedProvider} + */ + this.provider = data.provider ? new MessageEmbedProvider(this, data.provider) : null; + + /** + * The footer of this embed + * @type {MessageEmbedFooter} + */ + this.footer = data.footer ? new MessageEmbedFooter(this, data.footer) : null; + } + + /** + * The date this embed was created + * @type {Date} + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The hexadecimal version of the embed color, with a leading hash. + * @type {string} + * @readonly + */ + get hexColor() { + let col = this.color.toString(16); + while (col.length < 6) col = `0${col}`; + return `#${col}`; + } +} + +/** + * Represents a thumbnail for a message embed + */ +class MessageEmbedThumbnail { + constructor(embed, data) { + /** + * The embed this thumbnail is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The URL for this thumbnail + * @type {string} + */ + this.url = data.url; + + /** + * The Proxy URL for this thumbnail + * @type {string} + */ + this.proxyURL = data.proxy_url; + + /** + * The height of the thumbnail + * @type {number} + */ + this.height = data.height; + + /** + * The width of the thumbnail + * @type {number} + */ + this.width = data.width; + } +} + +/** + * Represents a provider for a message embed + */ +class MessageEmbedProvider { + constructor(embed, data) { + /** + * The embed this provider is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The name of this provider + * @type {string} + */ + this.name = data.name; + + /** + * The URL of this provider + * @type {string} + */ + this.url = data.url; + } +} + +/** + * Represents an author for a message embed + */ +class MessageEmbedAuthor { + constructor(embed, data) { + /** + * The embed this author is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The name of this author + * @type {string} + */ + this.name = data.name; + + /** + * The URL of this author + * @type {string} + */ + this.url = data.url; + + /** + * The icon URL of this author + * @type {string} + */ + this.iconURL = data.icon_url; + } +} + +/** + * Represents a field for a message embed + */ +class MessageEmbedField { + constructor(embed, data) { + /** + * The embed this footer is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The name of this field + * @type {string} + */ + this.name = data.name; + + /** + * The value of this field + * @type {string} + */ + this.value = data.value; + + /** + * If this field is displayed inline + * @type {boolean} + */ + this.inline = data.inline; + } +} + +/** + * Represents the footer of a message embed + */ +class MessageEmbedFooter { + constructor(embed, data) { + /** + * The embed this footer is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The text in this footer + * @type {string} + */ + this.text = data.text; + + /** + * The icon URL of this footer + * @type {string} + */ + this.iconURL = data.icon_url; + + /** + * The proxy icon URL of this footer + * @type {string} + */ + this.proxyIconUrl = data.proxy_icon_url; + } +} + +MessageEmbed.Thumbnail = MessageEmbedThumbnail; +MessageEmbed.Provider = MessageEmbedProvider; +MessageEmbed.Author = MessageEmbedAuthor; +MessageEmbed.Field = MessageEmbedField; +MessageEmbed.Footer = MessageEmbedFooter; + +module.exports = MessageEmbed; diff --git a/node_modules/discord.js/src/structures/MessageReaction.js b/node_modules/discord.js/src/structures/MessageReaction.js new file mode 100644 index 0000000..30c555f --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageReaction.js @@ -0,0 +1,92 @@ +const Collection = require('../util/Collection'); +const Emoji = require('./Emoji'); +const ReactionEmoji = require('./ReactionEmoji'); + +/** + * Represents a reaction to a message + */ +class MessageReaction { + constructor(message, emoji, count, me) { + /** + * The message that this reaction refers to + * @type {Message} + */ + this.message = message; + + /** + * Whether the client has given this reaction + * @type {boolean} + */ + this.me = me; + + /** + * The number of people that have given the same reaction. + * @type {number} + */ + this.count = count || 0; + + /** + * The users that have given this reaction, mapped by their ID. + * @type {Collection<string, User>} + */ + this.users = new Collection(); + + this._emoji = new ReactionEmoji(this, emoji.name, emoji.id); + } + + /** + * The emoji of this reaction, either an Emoji object for known custom emojis, or a ReactionEmoji + * object which has fewer properties. Whatever the prototype of the emoji, it will still have + * `name`, `id`, `identifier` and `toString()` + * @type {Emoji|ReactionEmoji} + */ + get emoji() { + if (this._emoji instanceof Emoji) return this._emoji; + // check to see if the emoji has become known to the client + if (this._emoji.id) { + const emojis = this.message.client.emojis; + if (emojis.has(this._emoji.id)) { + const emoji = emojis.get(this._emoji.id); + this._emoji = emoji; + return emoji; + } + } + return this._emoji; + } + + /** + * Removes a user from this reaction. + * @param {UserResolvable} [user=this.message.client.user] User to remove the reaction of + * @returns {Promise<MessageReaction>} + */ + remove(user = this.message.client.user) { + const message = this.message; + user = this.message.client.resolver.resolveUserID(user); + if (!user) return Promise.reject('Couldn\'t resolve the user ID to remove from the reaction.'); + return message.client.rest.methods.removeMessageReaction( + message, this.emoji.identifier, user + ); + } + + /** + * Fetch all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs. + * @param {number} [limit=100] the maximum amount of users to fetch, defaults to 100 + * @returns {Promise<Collection<string, User>>} + */ + fetchUsers(limit = 100) { + const message = this.message; + return message.client.rest.methods.getMessageReactionUsers( + message, this.emoji.identifier, limit + ).then(users => { + this.users = new Collection(); + for (const rawUser of users) { + const user = this.message.client.dataManager.newUser(rawUser); + this.users.set(user.id, user); + } + this.count = this.users.size; + return users; + }); + } +} + +module.exports = MessageReaction; diff --git a/node_modules/discord.js/src/structures/OAuth2Application.js b/node_modules/discord.js/src/structures/OAuth2Application.js new file mode 100644 index 0000000..b7c7285 --- /dev/null +++ b/node_modules/discord.js/src/structures/OAuth2Application.js @@ -0,0 +1,82 @@ +/** + * Represents an OAuth2 Application + */ +class OAuth2Application { + constructor(client, data) { + /** + * The client that instantiated the application + * @name OAuth2Application#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The ID of the app + * @type {string} + */ + this.id = data.id; + + /** + * The name of the app + * @type {string} + */ + this.name = data.name; + + /** + * The app's description + * @type {string} + */ + this.description = data.description; + + /** + * The app's icon hash + * @type {string} + */ + this.icon = data.icon; + + /** + * The app's icon URL + * @type {string} + */ + this.iconURL = `https://cdn.discordapp.com/app-icons/${this.id}/${this.icon}.jpg`; + + /** + * The app's RPC origins + * @type {Array<string>} + */ + this.rpcOrigins = data.rpc_origins; + } + + /** + * The timestamp the app was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the app was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * When concatenated with a string, this automatically concatenates the app name rather than the app object. + * @returns {string} + */ + toString() { + return this.name; + } +} + +module.exports = OAuth2Application; diff --git a/node_modules/discord.js/src/structures/PartialGuild.js b/node_modules/discord.js/src/structures/PartialGuild.js new file mode 100644 index 0000000..407212e --- /dev/null +++ b/node_modules/discord.js/src/structures/PartialGuild.js @@ -0,0 +1,51 @@ +/* +{ splash: null, + id: '123123123', + icon: '123123123', + name: 'name' } +*/ + +/** + * Represents a guild that the client only has limited information for - e.g. from invites. + */ +class PartialGuild { + constructor(client, data) { + /** + * The Client that instantiated this PartialGuild + * @name PartialGuild#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The ID of this guild + * @type {string} + */ + this.id = data.id; + + /** + * The name of this guild + * @type {string} + */ + this.name = data.name; + + /** + * The hash of this guild's icon, or null if there is none. + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image, or null if no splash (VIP only) + * @type {?string} + */ + this.splash = data.splash; + } +} + +module.exports = PartialGuild; diff --git a/node_modules/discord.js/src/structures/PartialGuildChannel.js b/node_modules/discord.js/src/structures/PartialGuildChannel.js new file mode 100644 index 0000000..e58a6bb --- /dev/null +++ b/node_modules/discord.js/src/structures/PartialGuildChannel.js @@ -0,0 +1,44 @@ +const Constants = require('../util/Constants'); + +/* +{ type: 0, id: '123123', name: 'heavy-testing' } } +*/ + +/** + * Represents a guild channel that the client only has limited information for - e.g. from invites. + */ +class PartialGuildChannel { + constructor(client, data) { + /** + * The Client that instantiated this PartialGuildChannel + * @name PartialGuildChannel#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The ID of this guild channel + * @type {string} + */ + this.id = data.id; + + /** + * The name of this guild channel + * @type {string} + */ + this.name = data.name; + + /** + * The type of this guild channel - `text` or `voice` + * @type {string} + */ + this.type = Constants.ChannelTypes.text === data.type ? 'text' : 'voice'; + } +} + +module.exports = PartialGuildChannel; diff --git a/node_modules/discord.js/src/structures/PermissionOverwrites.js b/node_modules/discord.js/src/structures/PermissionOverwrites.js new file mode 100644 index 0000000..9b2f536 --- /dev/null +++ b/node_modules/discord.js/src/structures/PermissionOverwrites.js @@ -0,0 +1,43 @@ +/** + * Represents a permission overwrite for a role or member in a guild channel. + */ +class PermissionOverwrites { + constructor(guildChannel, data) { + /** + * The GuildChannel this overwrite is for + * @name PermissionOverwrites#channel + * @type {GuildChannel} + * @readonly + */ + Object.defineProperty(this, 'channel', { value: guildChannel }); + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of this overwrite, either a user ID or a role ID + * @type {string} + */ + this.id = data.id; + + /** + * The type of this overwrite + * @type {string} + */ + this.type = data.type; + + this.deny = data.deny; + this.allow = data.allow; + } + + /** + * Delete this Permission Overwrite. + * @returns {Promise<PermissionOverwrites>} + */ + delete() { + return this.channel.client.rest.methods.deletePermissionOverwrites(this); + } +} + +module.exports = PermissionOverwrites; diff --git a/node_modules/discord.js/src/structures/Presence.js b/node_modules/discord.js/src/structures/Presence.js new file mode 100644 index 0000000..ddca5fb --- /dev/null +++ b/node_modules/discord.js/src/structures/Presence.js @@ -0,0 +1,92 @@ +/** + * Represents a user's presence + */ +class Presence { + constructor(data = {}) { + /** + * The status of the presence: + * + * * **`online`** - user is online + * * **`offline`** - user is offline or invisible + * * **`idle`** - user is AFK + * * **`dnd`** - user is in Do not Disturb + * @type {string} + */ + this.status = data.status || 'offline'; + + /** + * The game that the user is playing, `null` if they aren't playing a game. + * @type {?Game} + */ + this.game = data.game ? new Game(data.game) : null; + } + + update(data) { + this.status = data.status || this.status; + this.game = data.game ? new Game(data.game) : null; + } + + /** + * Whether this presence is equal to another + * @param {Presence} presence Presence to compare with + * @returns {boolean} + */ + equals(presence) { + return this === presence || ( + presence && + this.status === presence.status && + this.game ? this.game.equals(presence.game) : !presence.game + ); + } +} + +/** + * Represents a game that is part of a user's presence. + */ +class Game { + constructor(data) { + /** + * The name of the game being played + * @type {string} + */ + this.name = data.name; + + /** + * The type of the game status + * @type {number} + */ + this.type = data.type; + + /** + * If the game is being streamed, a link to the stream + * @type {?string} + */ + this.url = data.url || null; + } + + /** + * Whether or not the game is being streamed + * @type {boolean} + * @readonly + */ + get streaming() { + return this.type === 1; + } + + /** + * Whether this game is equal to another game + * @param {Game} game Game to compare with + * @returns {boolean} + */ + equals(game) { + return this === game || ( + game && + this.name === game.name && + this.type === game.type && + this.url === game.url + ); + } +} + +exports.Presence = Presence; +exports.Game = Game; diff --git a/node_modules/discord.js/src/structures/ReactionEmoji.js b/node_modules/discord.js/src/structures/ReactionEmoji.js new file mode 100644 index 0000000..b6d2cdb --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionEmoji.js @@ -0,0 +1,49 @@ +/** + * Represents a limited emoji set used for both custom and unicode emojis. Custom emojis + * will use this class opposed to the Emoji class when the client doesn't know enough + * information about them. + */ +class ReactionEmoji { + constructor(reaction, name, id) { + /** + * The message reaction this emoji refers to + * @type {MessageReaction} + */ + this.reaction = reaction; + + /** + * The name of this reaction emoji. + * @type {string} + */ + this.name = name; + + /** + * The ID of this reaction emoji. + * @type {string} + */ + this.id = id; + } + + /** + * The identifier of this emoji, used for message reactions + * @readonly + * @type {string} + */ + get identifier() { + if (this.id) return `${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * Creates the text required to form a graphical emoji on Discord. + * @example + * // send the emoji used in a reaction to the channel the reaction is part of + * reaction.message.channel.sendMessage(`The emoji used is ${reaction.emoji}`); + * @returns {string} + */ + toString() { + return this.id ? `<:${this.name}:${this.id}>` : this.name; + } +} + +module.exports = ReactionEmoji; diff --git a/node_modules/discord.js/src/structures/RichEmbed.js b/node_modules/discord.js/src/structures/RichEmbed.js new file mode 100644 index 0000000..fbd9383 --- /dev/null +++ b/node_modules/discord.js/src/structures/RichEmbed.js @@ -0,0 +1,204 @@ +/** + * A rich embed to be sent with a message + * @param {Object} [data] Data to set in the rich embed + */ +class RichEmbed { + constructor(data = {}) { + /** + * Title for this Embed + * @type {string} + */ + this.title = data.title; + + /** + * Description for this Embed + * @type {string} + */ + this.description = data.description; + + /** + * URL for this Embed + * @type {string} + */ + this.url = data.url; + + /** + * Color for this Embed + * @type {number} + */ + this.color = data.color; + + /** + * Author for this Embed + * @type {Object} + */ + this.author = data.author; + + /** + * Timestamp for this Embed + * @type {Date} + */ + this.timestamp = data.timestamp; + + /** + * Fields for this Embed + * @type {Object[]} + */ + this.fields = data.fields || []; + + /** + * Thumbnail for this Embed + * @type {Object} + */ + this.thumbnail = data.thumbnail; + + /** + * Image for this Embed + * @type {Object} + */ + this.image = data.image; + + /** + * Footer for this Embed + * @type {Object} + */ + this.footer = data.footer; + } + + /** + * Sets the title of this embed + * @param {StringResolvable} title The title + * @returns {RichEmbed} This embed + */ + setTitle(title) { + title = resolveString(title); + if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.'); + this.title = title; + return this; + } + + /** + * Sets the description of this embed + * @param {StringResolvable} description The description + * @returns {RichEmbed} This embed + */ + setDescription(description) { + description = resolveString(description); + if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.'); + this.description = description; + return this; + } + + /** + * Sets the URL of this embed + * @param {string} url The URL + * @returns {RichEmbed} This embed + */ + setURL(url) { + this.url = url; + return this; + } + + /** + * Sets the color of this embed + * @param {string|number|number[]} color The color to set + * @returns {RichEmbed} This embed + */ + setColor(color) { + let radix = 10; + if (color instanceof Array) { + color = (color[0] << 16) + (color[1] << 8) + color[2]; + } else if (typeof color === 'string' && color.startsWith('#')) { + radix = 16; + color = color.replace('#', ''); + } + color = parseInt(color, radix); + if (color < 0 || color > 0xFFFFFF) { + throw new RangeError('RichEmbed color must be within the range 0 - 16777215 (0xFFFFFF).'); + } else if (color && isNaN(color)) { + throw new TypeError('Unable to convert RichEmbed color to a number.'); + } + this.color = color; + return this; + } + + /** + * Sets the author of this embed + * @param {StringResolvable} name The name of the author + * @param {string} [icon] The icon URL of the author + * @param {string} [url] The URL of the author + * @returns {RichEmbed} This embed + */ + setAuthor(name, icon, url) { + this.author = { name: resolveString(name), icon_url: icon, url }; + return this; + } + + /** + * Sets the timestamp of this embed + * @param {Date} [timestamp=current date] The timestamp + * @returns {RichEmbed} This embed + */ + setTimestamp(timestamp = new Date()) { + this.timestamp = timestamp; + return this; + } + + /** + * Adds a field to the embed (max 25) + * @param {StringResolvable} name The name of the field + * @param {StringResolvable} value The value of the field + * @param {boolean} [inline=false] Set the field to display inline + * @returns {RichEmbed} This embed + */ + addField(name, value, inline = false) { + if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.'); + name = resolveString(name); + if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); + value = resolveString(value); + if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); + this.fields.push({ name: String(name), value: value, inline }); + return this; + } + + /** + * Set the thumbnail of this embed + * @param {string} url The URL of the thumbnail + * @returns {RichEmbed} This embed + */ + setThumbnail(url) { + this.thumbnail = { url }; + return this; + } + + /** + * Set the image of this embed + * @param {string} url The URL of the thumbnail + * @returns {RichEmbed} This embed + */ + setImage(url) { + this.image = { url }; + return this; + } + + /** + * Sets the footer of this embed + * @param {StringResolvable} text The text of the footer + * @param {string} [icon] The icon URL of the footer + * @returns {RichEmbed} This embed + */ + setFooter(text, icon) { + text = resolveString(text); + if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.'); + this.footer = { text, icon_url: icon }; + return this; + } +} + +module.exports = RichEmbed; + +function resolveString(data) { + if (typeof data === 'string') return data; + if (data instanceof Array) return data.join('\n'); + return String(data); +} diff --git a/node_modules/discord.js/src/structures/Role.js b/node_modules/discord.js/src/structures/Role.js new file mode 100644 index 0000000..c15ff4b --- /dev/null +++ b/node_modules/discord.js/src/structures/Role.js @@ -0,0 +1,341 @@ +const Constants = require('../util/Constants'); + +/** + * Represents a role on Discord + */ +class Role { + constructor(guild, data) { + /** + * The client that instantiated the role + * @name Role#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: guild.client }); + + /** + * The guild that the role belongs to + * @type {Guild} + */ + this.guild = guild; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of the role (unique to the guild it is part of) + * @type {string} + */ + this.id = data.id; + + /** + * The name of the role + * @type {string} + */ + this.name = data.name; + + /** + * The base 10 color of the role + * @type {number} + */ + this.color = data.color; + + /** + * If true, users that are part of this role will appear in a separate category in the users list + * @type {boolean} + */ + this.hoist = data.hoist; + + /** + * The position of the role in the role manager + * @type {number} + */ + this.position = data.position; + + /** + * The evaluated permissions number + * @type {number} + */ + this.permissions = data.permissions; + + /** + * Whether or not the role is managed by an external service + * @type {boolean} + */ + this.managed = data.managed; + + /** + * Whether or not the role can be mentioned by anyone + * @type {boolean} + */ + this.mentionable = data.mentionable; + } + + /** + * The timestamp the role was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the role was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The hexadecimal version of the role color, with a leading hashtag. + * @type {string} + * @readonly + */ + get hexColor() { + let col = this.color.toString(16); + while (col.length < 6) col = `0${col}`; + return `#${col}`; + } + + /** + * The cached guild members that have this role. + * @type {Collection<string, GuildMember>} + * @readonly + */ + get members() { + return this.guild.members.filter(m => m.roles.has(this.id)); + } + + /** + * Whether the role is editable by the client user. + * @type {boolean} + * @readonly + */ + get editable() { + if (this.managed) return false; + const clientMember = this.guild.member(this.client.user); + if (!clientMember.hasPermission(Constants.PermissionFlags.MANAGE_ROLES_OR_PERMISSIONS)) return false; + return clientMember.highestRole.comparePositionTo(this) > 0; + } + + /** + * Get an object mapping permission names to whether or not the role enables that permission + * @returns {Object<string, boolean>} + * @example + * // print the serialized role + * console.log(role.serialize()); + */ + serialize() { + const serializedPermissions = {}; + for (const permissionName in Constants.PermissionFlags) { + serializedPermissions[permissionName] = this.hasPermission(permissionName); + } + return serializedPermissions; + } + + /** + * Checks if the role has a permission. + * @param {PermissionResolvable} permission The permission to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission + * @returns {boolean} + * @example + * // see if a role can ban a member + * if (role.hasPermission('BAN_MEMBERS')) { + * console.log('This role can ban members'); + * } else { + * console.log('This role can\'t ban members'); + * } + */ + hasPermission(permission, explicit = false) { + permission = this.client.resolver.resolvePermission(permission); + if (!explicit && (this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true; + return (this.permissions & permission) > 0; + } + + /** + * Checks if the role has all specified permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permissions + * @returns {boolean} + */ + hasPermissions(permissions, explicit = false) { + return permissions.every(p => this.hasPermission(p, explicit)); + } + + /** + * Compares this role's position to another role's. + * @param {Role} role Role to compare to this one + * @returns {number} Negative number if the this role's position is lower (other role's is higher), + * positive number if the this one is higher (other's is lower), 0 if equal + */ + comparePositionTo(role) { + return this.constructor.comparePositions(this, role); + } + + /** + * The data for a role + * @typedef {Object} RoleData + * @property {string} [name] The name of the role + * @property {number|string} [color] The color of the role, either a hex string or a base 10 number + * @property {boolean} [hoist] Whether or not the role should be hoisted + * @property {number} [position] The position of the role + * @property {string[]} [permissions] The permissions of the role + * @property {boolean} [mentionable] Whether or not the role should be mentionable + */ + + /** + * Edits the role + * @param {RoleData} data The new data for the role + * @returns {Promise<Role>} + * @example + * // edit a role + * role.edit({name: 'new role'}) + * .then(r => console.log(`Edited role ${r}`)) + * .catch(console.error); + */ + edit(data) { + return this.client.rest.methods.updateGuildRole(this, data); + } + + /** + * Set a new name for the role + * @param {string} name The new name of the role + * @returns {Promise<Role>} + * @example + * // set the name of the role + * role.setName('new role') + * .then(r => console.log(`Edited name of role ${r}`)) + * .catch(console.error); + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Set a new color for the role + * @param {number|string} color The new color for the role, either a hex string or a base 10 number + * @returns {Promise<Role>} + * @example + * // set the color of a role + * role.setColor('#FF0000') + * .then(r => console.log(`Set color of role ${r}`)) + * .catch(console.error); + */ + setColor(color) { + return this.edit({ color }); + } + + /** + * Set whether or not the role should be hoisted + * @param {boolean} hoist Whether or not to hoist the role + * @returns {Promise<Role>} + * @example + * // set the hoist of the role + * role.setHoist(true) + * .then(r => console.log(`Role hoisted: ${r.hoist}`)) + * .catch(console.error); + */ + setHoist(hoist) { + return this.edit({ hoist }); + } + + /** + * Set the position of the role + * @param {number} position The position of the role + * @returns {Promise<Role>} + * @example + * // set the position of the role + * role.setPosition(1) + * .then(r => console.log(`Role position: ${r.position}`)) + * .catch(console.error); + */ + setPosition(position) { + return this.guild.setRolePosition(this, position).then(() => this); + } + + /** + * Set the permissions of the role + * @param {string[]} permissions The permissions of the role + * @returns {Promise<Role>} + * @example + * // set the permissions of the role + * role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS']) + * .then(r => console.log(`Role updated ${r}`)) + * .catch(console.error); + */ + setPermissions(permissions) { + return this.edit({ permissions }); + } + + /** + * Set whether this role is mentionable + * @param {boolean} mentionable Whether this role should be mentionable + * @returns {Promise<Role>} + * @example + * // make the role mentionable + * role.setMentionable(true) + * .then(r => console.log(`Role updated ${r}`)) + * .catch(console.error); + */ + setMentionable(mentionable) { + return this.edit({ mentionable }); + } + + /** + * Deletes the role + * @returns {Promise<Role>} + * @example + * // delete a role + * role.delete() + * .then(r => console.log(`Deleted role ${r}`)) + * .catch(console.error); + */ + delete() { + return this.client.rest.methods.deleteGuildRole(this); + } + + /** + * Whether this role equals another role. It compares all properties, so for most operations + * it is advisable to just compare `role.id === role2.id` as it is much faster and is often + * what most users need. + * @param {Role} role Role to compare with + * @returns {boolean} + */ + equals(role) { + return role && + this.id === role.id && + this.name === role.name && + this.color === role.color && + this.hoist === role.hoist && + this.position === role.position && + this.permissions === role.permissions && + this.managed === role.managed; + } + + /** + * When concatenated with a string, this automatically concatenates the role mention rather than the Role object. + * @returns {string} + */ + toString() { + if (this.id === this.guild.id) return '@everyone'; + return `<@&${this.id}>`; + } + + /** + * Compares the positions of two roles. + * @param {Role} role1 First role to compare + * @param {Role} role2 Second role to compare + * @returns {number} Negative number if the first role's position is lower (second role's is higher), + * positive number if the first's is higher (second's is lower), 0 if equal + */ + static comparePositions(role1, role2) { + if (role1.position === role2.position) return role2.id - role1.id; + return role1.position - role2.position; + } +} + +module.exports = Role; diff --git a/node_modules/discord.js/src/structures/TextChannel.js b/node_modules/discord.js/src/structures/TextChannel.js new file mode 100644 index 0000000..9697abd --- /dev/null +++ b/node_modules/discord.js/src/structures/TextChannel.js @@ -0,0 +1,96 @@ +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Collection = require('../util/Collection'); + +/** + * Represents a guild text channel on Discord. + * @extends {GuildChannel} + * @implements {TextBasedChannel} + */ +class TextChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'text'; + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The topic of the text channel, if there is one. + * @type {?string} + */ + this.topic = data.topic; + + this.lastMessageID = data.last_message_id; + } + + /** + * A collection of members that can see this channel, mapped by their ID. + * @type {Collection<string, GuildMember>} + * @readonly + */ + get members() { + const members = new Collection(); + for (const member of this.guild.members.values()) { + if (this.permissionsFor(member).hasPermission('READ_MESSAGES')) { + members.set(member.id, member); + } + } + return members; + } + + /** + * Fetch all webhooks for the channel. + * @returns {Promise<Collection<string, Webhook>>} + */ + fetchWebhooks() { + return this.client.rest.methods.getChannelWebhooks(this); + } + + /** + * Create a webhook for the channel. + * @param {string} name The name of the webhook. + * @param {BufferResolvable} avatar The avatar for the webhook. + * @returns {Promise<Webhook>} webhook The created webhook. + * @example + * channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png') + * .then(webhook => console.log(`Created Webhook ${webhook}`)) + * .catch(console.error) + */ + createWebhook(name, avatar) { + return new Promise(resolve => { + if (avatar.startsWith('data:')) { + resolve(this.client.rest.methods.createWebhook(this, name, avatar)); + } else { + this.client.resolver.resolveBuffer(avatar).then(data => + resolve(this.client.rest.methods.createWebhook(this, name, data)) + ); + } + }); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } + fetchMessage() { return; } + fetchMessages() { return; } + fetchPinnedMessages() { return; } + startTyping() { return; } + stopTyping() { return; } + get typing() { return; } + get typingCount() { return; } + createCollector() { return; } + awaitMessages() { return; } + bulkDelete() { return; } + _cacheMessage() { return; } +} + +TextBasedChannel.applyToClass(TextChannel, true); + +module.exports = TextChannel; diff --git a/node_modules/discord.js/src/structures/User.js b/node_modules/discord.js/src/structures/User.js new file mode 100644 index 0000000..f714828 --- /dev/null +++ b/node_modules/discord.js/src/structures/User.js @@ -0,0 +1,277 @@ +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Constants = require('../util/Constants'); +const Presence = require('./Presence').Presence; + +/** + * Represents a user on Discord. + * @implements {TextBasedChannel} + */ +class User { + constructor(client, data) { + /** + * The Client that created the instance of the the User. + * @name User#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of the user + * @type {string} + */ + this.id = data.id; + + /** + * The username of the user + * @type {string} + */ + this.username = data.username; + + /** + * A discriminator based on username for the user + * @type {string} + */ + this.discriminator = data.discriminator; + + /** + * The ID of the user's avatar + * @type {string} + */ + this.avatar = data.avatar; + + /** + * Whether or not the user is a bot. + * @type {boolean} + */ + this.bot = Boolean(data.bot); + + /** + * The ID of the last message sent by the user, if one was sent. + * @type {?string} + */ + this.lastMessageID = null; + } + + patch(data) { + for (const prop of ['id', 'username', 'discriminator', 'avatar', 'bot']) { + if (typeof data[prop] !== 'undefined') this[prop] = data[prop]; + } + if (data.token) this.client.token = data.token; + } + + /** + * The timestamp the user was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the user was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The presence of this user + * @type {Presence} + * @readonly + */ + get presence() { + if (this.client.presences.has(this.id)) return this.client.presences.get(this.id); + for (const guild of this.client.guilds.values()) { + if (guild.presences.has(this.id)) return guild.presences.get(this.id); + } + return new Presence(); + } + + /** + * A link to the user's avatar (if they have one, otherwise null) + * @type {?string} + * @readonly + */ + get avatarURL() { + if (!this.avatar) return null; + return Constants.Endpoints.avatar(this.id, this.avatar); + } + + /** + * A link to the user's default avatar + * @type {string} + * @readonly + */ + get defaultAvatarURL() { + let defaultAvatars = Object.values(Constants.DefaultAvatars); + let defaultAvatar = this.discriminator % defaultAvatars.length; + return Constants.Endpoints.assets(`${defaultAvatars[defaultAvatar]}.png`); + } + + /** + * A link to the user's avatar if they have one. Otherwise a link to their default avatar will be returned + * @type {string} + * @readonly + */ + get displayAvatarURL() { + return this.avatarURL || this.defaultAvatarURL; + } + + /** + * The note that is set for the user + * <warn>This is only available when using a user account.</warn> + * @type {?string} + * @readonly + */ + get note() { + return this.client.user.notes.get(this.id) || null; + } + + /** + * Check whether the user is typing in a channel. + * @param {ChannelResolvable} channel The channel to check in + * @returns {boolean} + */ + typingIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + return channel._typing.has(this.id); + } + + /** + * Get the time that the user started typing. + * @param {ChannelResolvable} channel The channel to get the time in + * @returns {?Date} + */ + typingSinceIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + return channel._typing.has(this.id) ? new Date(channel._typing.get(this.id).since) : null; + } + + /** + * Get the amount of time the user has been typing in a channel for (in milliseconds), or -1 if they're not typing. + * @param {ChannelResolvable} channel The channel to get the time in + * @returns {number} + */ + typingDurationIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + return channel._typing.has(this.id) ? channel._typing.get(this.id).elapsedTime : -1; + } + + /** + * The DM between the client's user and this user + * @type {?DMChannel} + */ + get dmChannel() { + return this.client.channels.filter(c => c.type === 'dm').find(c => c.recipient.id === this.id); + } + + /** + * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.client.rest.methods.deleteChannel(this); + } + + /** + * Sends a friend request to the user + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + */ + addFriend() { + return this.client.rest.methods.addFriend(this); + } + + /** + * Removes the user from your friends + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + */ + removeFriend() { + return this.client.rest.methods.removeFriend(this); + } + + /** + * Blocks the user + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + */ + block() { + return this.client.rest.methods.blockUser(this); + } + + /** + * Unblocks the user + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + */ + unblock() { + return this.client.rest.methods.unblockUser(this); + } + + /** + * Get the profile of the user + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<UserProfile>} + */ + fetchProfile() { + return this.client.rest.methods.fetchUserProfile(this); + } + + /** + * Sets a note for the user + * <warn>This is only available when using a user account.</warn> + * @param {string} note The note to set for the user + * @returns {Promise<User>} + */ + setNote(note) { + return this.client.rest.methods.setNote(this, note); + } + + /** + * Checks if the user is equal to another. It compares ID, username, discriminator, avatar, and bot flags. + * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. + * @param {User} user User to compare with + * @returns {boolean} + */ + equals(user) { + let equal = user && + this.id === user.id && + this.username === user.username && + this.discriminator === user.discriminator && + this.avatar === user.avatar && + this.bot === Boolean(user.bot); + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the user's mention instead of the User object. + * @returns {string} + * @example + * // logs: Hello from <@123456789>! + * console.log(`Hello from ${user}!`); + */ + toString() { + return `<@${this.id}>`; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } +} + +TextBasedChannel.applyToClass(User); + +module.exports = User; diff --git a/node_modules/discord.js/src/structures/UserConnection.js b/node_modules/discord.js/src/structures/UserConnection.js new file mode 100644 index 0000000..6ee9fc5 --- /dev/null +++ b/node_modules/discord.js/src/structures/UserConnection.js @@ -0,0 +1,48 @@ +/** + * Represents a user connection (or "platform identity") + */ +class UserConnection { + constructor(user, data) { + /** + * The user that owns the Connection + * @type {User} + */ + this.user = user; + + this.setup(data); + } + + setup(data) { + /** + * The type of the Connection + * @type {string} + */ + this.type = data.type; + + /** + * The username of the connection account + * @type {string} + */ + this.name = data.name; + + /** + * The id of the connection account + * @type {string} + */ + this.id = data.id; + + /** + * Whether the connection is revoked + * @type {boolean} + */ + this.revoked = data.revoked; + + /** + * an array of partial server integrations (not yet implemented in this lib) + * @type {Object[]} + */ + this.integrations = data.integrations; + } +} + +module.exports = UserConnection; diff --git a/node_modules/discord.js/src/structures/UserProfile.js b/node_modules/discord.js/src/structures/UserProfile.js new file mode 100644 index 0000000..77f097c --- /dev/null +++ b/node_modules/discord.js/src/structures/UserProfile.js @@ -0,0 +1,56 @@ +const Collection = require('../util/Collection'); +const UserConnection = require('./UserConnection'); + +/** + * Represents a user's profile on Discord. + */ +class UserProfile { + constructor(user, data) { + /** + * The owner of the profile + * @type {User} + */ + this.user = user; + + /** + * The Client that created the instance of the the UserProfile. + * @name UserProfile#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: user.client }); + + /** + * Guilds that the client user and the user share + * @type {Collection<Guild>} + */ + this.mutualGuilds = new Collection(); + + /** + * The user's connections + * @type {Collection<UserConnection>} + */ + this.connections = new Collection(); + + this.setup(data); + } + + setup(data) { + /** + * If the user has Discord Premium + * @type {boolean} + */ + this.premium = data.premium; + + for (const guild of data.mutual_guilds) { + if (this.client.guilds.has(guild.id)) { + this.mutualGuilds.set(guild.id, this.client.guilds.get(guild.id)); + } + } + for (const connection of data.connected_accounts) { + this.connections.set(connection.id, new UserConnection(this.user, connection)); + } + } +} + +module.exports = UserProfile; diff --git a/node_modules/discord.js/src/structures/VoiceChannel.js b/node_modules/discord.js/src/structures/VoiceChannel.js new file mode 100644 index 0000000..848a6d5 --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceChannel.js @@ -0,0 +1,120 @@ +const GuildChannel = require('./GuildChannel'); +const Collection = require('../util/Collection'); + +/** + * Represents a guild voice channel on Discord. + * @extends {GuildChannel} + */ +class VoiceChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + + /** + * The members in this voice channel. + * @type {Collection<string, GuildMember>} + */ + this.members = new Collection(); + + this.type = 'voice'; + } + + setup(data) { + super.setup(data); + + /** + * The bitrate of this voice channel + * @type {number} + */ + this.bitrate = data.bitrate; + + /** + * The maximum amount of users allowed in this channel - 0 means unlimited. + * @type {number} + */ + this.userLimit = data.user_limit; + } + + /** + * The voice connection for this voice channel, if the client is connected + * @type {?VoiceConnection} + * @readonly + */ + get connection() { + const connection = this.guild.voiceConnection; + if (connection && connection.channel.id === this.id) return connection; + return null; + } + + /** + * Checks if the client has permission join the voice channel + * @type {boolean} + */ + get joinable() { + if (this.client.browser) return false; + return this.permissionsFor(this.client.user).hasPermission('CONNECT'); + } + + /** + * Checks if the client has permission to send audio to the voice channel + * @type {boolean} + */ + get speakable() { + return this.permissionsFor(this.client.user).hasPermission('SPEAK'); + } + + /** + * Sets the bitrate of the channel + * @param {number} bitrate The new bitrate + * @returns {Promise<VoiceChannel>} + * @example + * // set the bitrate of a voice channel + * voiceChannel.setBitrate(48000) + * .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`)) + * .catch(console.error); + */ + setBitrate(bitrate) { + return this.edit({ bitrate }); + } + + /** + * Sets the user limit of the channel + * @param {number} userLimit The new user limit + * @returns {Promise<VoiceChannel>} + * @example + * // set the user limit of a voice channel + * voiceChannel.setUserLimit(42) + * .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`)) + * .catch(console.error); + */ + setUserLimit(userLimit) { + return this.edit({ userLimit }); + } + + /** + * Attempts to join this voice channel + * @returns {Promise<VoiceConnection>} + * @example + * // join a voice channel + * voiceChannel.join() + * .then(connection => console.log('Connected!')) + * .catch(console.error); + */ + join() { + if (this.client.browser) return Promise.reject(new Error('Voice connections are not available in browsers.')); + return this.client.voice.joinChannel(this); + } + + /** + * Leaves this voice channel + * @example + * // leave a voice channel + * voiceChannel.leave(); + */ + leave() { + if (this.client.browser) return; + const connection = this.client.voice.connections.get(this.guild.id); + if (connection && connection.channel.id === this.id) connection.disconnect(); + } +} + +module.exports = VoiceChannel; diff --git a/node_modules/discord.js/src/structures/Webhook.js b/node_modules/discord.js/src/structures/Webhook.js new file mode 100644 index 0000000..96984ff --- /dev/null +++ b/node_modules/discord.js/src/structures/Webhook.js @@ -0,0 +1,200 @@ +const path = require('path'); +const escapeMarkdown = require('../util/EscapeMarkdown'); + +/** + * Represents a webhook + */ +class Webhook { + constructor(client, dataOrID, token) { + if (client) { + /** + * The Client that instantiated the Webhook + * @name Webhook#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + if (dataOrID) this.setup(dataOrID); + } else { + this.id = dataOrID; + this.token = token; + Object.defineProperty(this, 'client', { value: this }); + } + } + + setup(data) { + /** + * The name of the webhook + * @type {string} + */ + this.name = data.name; + + /** + * The token for the webhook + * @type {string} + */ + this.token = data.token; + + /** + * The avatar for the webhook + * @type {string} + */ + this.avatar = data.avatar; + + /** + * The ID of the webhook + * @type {string} + */ + this.id = data.id; + + /** + * The guild the webhook belongs to + * @type {string} + */ + this.guildID = data.guild_id; + + /** + * The channel the webhook belongs to + * @type {string} + */ + this.channelID = data.channel_id; + + /** + * The owner of the webhook + * @type {User} + */ + if (data.user) this.owner = data.user; + } + + /** + * Options that can be passed into sendMessage, sendTTSMessage, sendFile, sendCode + * @typedef {Object} WebhookMessageOptions + * @property {boolean} [tts=false] Whether or not the message should be spoken aloud + * @property {boolean} [disableEveryone=this.options.disableEveryone] Whether or not @everyone and @here + * should be replaced with plain-text + */ + + /** + * Send a message with this webhook + * @param {StringResolvable} content The content to send. + * @param {WebhookMessageOptions} [options={}] The options to provide. + * @returns {Promise<Message|Message[]>} + * @example + * // send a message + * webhook.sendMessage('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + sendMessage(content, options = {}) { + return this.client.rest.methods.sendWebhookMessage(this, content, options); + } + + /** + * Send a raw slack message with this webhook + * @param {Object} body The raw body to send. + * @returns {Promise} + * @example + * // send a slack message + * webhook.sendSlackMessage({ + * 'username': 'Wumpus', + * 'attachments': [{ + * 'pretext': 'this looks pretty cool', + * 'color': '#F0F', + * 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png', + * 'footer': 'Powered by sneks', + * 'ts': Date.now() / 1000 + * }] + * }).catch(console.error); + */ + sendSlackMessage(body) { + return this.client.rest.methods.sendSlackWebhookMessage(this, body); + } + + /** + * Send a text-to-speech message with this webhook + * @param {StringResolvable} content The content to send + * @param {WebhookMessageOptions} [options={}] The options to provide + * @returns {Promise<Message|Message[]>} + * @example + * // send a TTS message + * webhook.sendTTSMessage('hello!') + * .then(message => console.log(`Sent tts message: ${message.content}`)) + * .catch(console.error); + */ + sendTTSMessage(content, options = {}) { + Object.assign(options, { tts: true }); + return this.client.rest.methods.sendWebhookMessage(this, content, options); + } + + /** + * Send a file with this webhook + * @param {BufferResolvable} attachment The file to send + * @param {string} [fileName="file.jpg"] The name and extension of the file + * @param {StringResolvable} [content] Text message to send with the attachment + * @param {WebhookMessageOptions} [options] The options to provide + * @returns {Promise<Message>} + */ + sendFile(attachment, fileName, content, options = {}) { + if (!fileName) { + if (typeof attachment === 'string') { + fileName = path.basename(attachment); + } else if (attachment && attachment.path) { + fileName = path.basename(attachment.path); + } else { + fileName = 'file.jpg'; + } + } + return this.client.resolver.resolveBuffer(attachment).then(file => + this.client.rest.methods.sendWebhookMessage(this, content, options, { + file, + name: fileName, + }) + ); + } + + /** + * Send a code block with this webhook + * @param {string} lang Language for the code block + * @param {StringResolvable} content Content of the code block + * @param {WebhookMessageOptions} options The options to provide + * @returns {Promise<Message|Message[]>} + */ + sendCode(lang, content, options = {}) { + if (options.split) { + if (typeof options.split !== 'object') options.split = {}; + if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`; + if (!options.split.append) options.split.append = '\n```'; + } + content = escapeMarkdown(this.client.resolver.resolveString(content), true); + return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options); + } + + /** + * Edit the webhook. + * @param {string} name The new name for the Webhook + * @param {BufferResolvable} avatar The new avatar for the Webhook. + * @returns {Promise<Webhook>} + */ + edit(name = this.name, avatar) { + if (avatar) { + return this.client.resolver.resolveBuffer(avatar).then(file => { + const dataURI = this.client.resolver.resolveBase64(file); + return this.client.rest.methods.editWebhook(this, name, dataURI); + }); + } + return this.client.rest.methods.editWebhook(this, name).then(data => { + this.setup(data); + return this; + }); + } + + /** + * Delete the webhook + * @returns {Promise} + */ + delete() { + return this.client.rest.methods.deleteWebhook(this); + } +} + +module.exports = Webhook; diff --git a/node_modules/discord.js/src/structures/interface/TextBasedChannel.js b/node_modules/discord.js/src/structures/interface/TextBasedChannel.js new file mode 100644 index 0000000..353c0a9 --- /dev/null +++ b/node_modules/discord.js/src/structures/interface/TextBasedChannel.js @@ -0,0 +1,377 @@ +const path = require('path'); +const Message = require('../Message'); +const MessageCollector = require('../MessageCollector'); +const Collection = require('../../util/Collection'); + + +/** + * Interface for classes that have text-channel-like features + * @interface + */ +class TextBasedChannel { + constructor() { + /** + * A collection containing the messages sent to this channel. + * @type {Collection<string, Message>} + */ + this.messages = new Collection(); + + /** + * The ID of the last message in the channel, if one was sent. + * @type {?string} + */ + this.lastMessageID = null; + } + + /** + * Options that can be passed into send, sendMessage, sendFile, sendEmbed, sendCode, and Message#reply + * @typedef {Object} MessageOptions + * @property {boolean} [tts=false] Whether or not the message should be spoken aloud + * @property {string} [nonce=''] The nonce for the message + * @property {Object} [embed] An embed for the message + * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) + * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here + * should be replaced with plain-text + * @property {FileOptions|string} [file] A file to send with the message + * @property {string|boolean} [code] Language for optional codeblock formatting to apply + * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if + * it exceeds the character limit. If an object is provided, these are the options for splitting the message. + */ + + /** + * @typedef {Object} FileOptions + * @property {BufferResolvable} attachment + * @property {string} [name='file.jpg'] + */ + + /** + * Options for splitting a message + * @typedef {Object} SplitOptions + * @property {number} [maxLength=1950] Maximum character length per message piece + * @property {string} [char='\n'] Character to split the message with + * @property {string} [prepend=''] Text to prepend to every piece except the first + * @property {string} [append=''] Text to append to every piece except the last + */ + + /** + * Send a message to this channel + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options={}] Options for the message + * @returns {Promise<Message|Message[]>} + * @example + * // send a message + * channel.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + send(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + if (options.file) { + if (typeof options.file === 'string') options.file = { attachment: options.file }; + if (!options.file.name) { + if (typeof options.file.attachment === 'string') { + options.file.name = path.basename(options.file.attachment); + } else if (options.file.attachment && options.file.attachment.path) { + options.file.name = path.basename(options.file.attachment.path); + } else { + options.file.name = 'file.jpg'; + } + } + return this.client.resolver.resolveBuffer(options.file.attachment).then(file => + this.client.rest.methods.sendMessage(this, content, options, { + file, + name: options.file.name, + }) + ); + } + return this.client.rest.methods.sendMessage(this, content, options); + } + + /** + * Send a message to this channel + * @param {StringResolvable} content Text for the message + * @param {MessageOptions} [options={}] Options for the message + * @returns {Promise<Message|Message[]>} + * @example + * // send a message + * channel.sendMessage('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + sendMessage(content, options) { + return this.send(content, options); + } + + /** + * Send an embed to this channel + * @param {RichEmbed|Object} embed Embed for the message + * @param {string} [content] Text for the message + * @param {MessageOptions} [options] Options for the message + * @returns {Promise<Message>} + */ + sendEmbed(embed, content, options) { + if (!options && typeof content === 'object') { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.send(content, Object.assign(options, { embed })); + } + + /** + * Send a file to this channel + * @param {BufferResolvable} attachment File to send + * @param {string} [name='file.jpg'] Name and extension of the file + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options] Options for the message + * @returns {Promise<Message>} + */ + sendFile(attachment, name, content, options = {}) { + return this.send(content, Object.assign(options, { file: { attachment, name } })); + } + + /** + * Send a code block to this channel + * @param {string} lang Language for the code block + * @param {StringResolvable} content Content of the code block + * @param {MessageOptions} [options] Options for the message + * @returns {Promise<Message|Message[]>} + */ + sendCode(lang, content, options = {}) { + return this.send(content, Object.assign(options, { code: lang })); + } + + /** + * Gets a single message from this channel, regardless of it being cached or not. + * <warn>This is only available when using a bot account.</warn> + * @param {string} messageID ID of the message to get + * @returns {Promise<Message>} + * @example + * // get message + * channel.fetchMessage('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + */ + fetchMessage(messageID) { + return this.client.rest.methods.getChannelMessage(this, messageID).then(data => { + const msg = data instanceof Message ? data : new Message(this, data, this.client); + this._cacheMessage(msg); + return msg; + }); + } + + /** + * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and + * `after` are mutually exclusive. All the parameters are optional. + * @typedef {Object} ChannelLogsQueryOptions + * @property {number} [limit=50] Number of messages to acquire + * @property {string} [before] ID of a message to get the messages that were posted before it + * @property {string} [after] ID of a message to get the messages that were posted after it + * @property {string} [around] ID of a message to get the messages that were posted around it + */ + + /** + * Gets the past messages sent in this channel. Resolves with a collection mapping message ID's to Message objects. + * @param {ChannelLogsQueryOptions} [options={}] Query parameters to pass in + * @returns {Promise<Collection<string, Message>>} + * @example + * // get messages + * channel.fetchMessages({limit: 10}) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + fetchMessages(options = {}) { + return this.client.rest.methods.getChannelMessages(this, options).then(data => { + const messages = new Collection(); + for (const message of data) { + const msg = new Message(this, message, this.client); + messages.set(message.id, msg); + this._cacheMessage(msg); + } + return messages; + }); + } + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * @returns {Promise<Collection<string, Message>>} + */ + fetchPinnedMessages() { + return this.client.rest.methods.getChannelPinnedMessages(this).then(data => { + const messages = new Collection(); + for (const message of data) { + const msg = new Message(this, message, this.client); + messages.set(message.id, msg); + this._cacheMessage(msg); + } + return messages; + }); + } + + /** + * Starts a typing indicator in the channel. + * @param {number} [count] The number of times startTyping should be considered to have been called + * @example + * // start typing in a channel + * channel.startTyping(); + */ + startTyping(count) { + if (typeof count !== 'undefined' && count < 1) throw new RangeError('Count must be at least 1.'); + if (!this.client.user._typing.has(this.id)) { + this.client.user._typing.set(this.id, { + count: count || 1, + interval: this.client.setInterval(() => { + this.client.rest.methods.sendTyping(this.id); + }, 4000), + }); + this.client.rest.methods.sendTyping(this.id); + } else { + const entry = this.client.user._typing.get(this.id); + entry.count = count || entry.count + 1; + } + } + + /** + * Stops the typing indicator in the channel. + * The indicator will only stop if this is called as many times as startTyping(). + * <info>It can take a few seconds for the client user to stop typing.</info> + * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop + * @example + * // stop typing in a channel + * channel.stopTyping(); + * @example + * // force typing to fully stop in a channel + * channel.stopTyping(true); + */ + stopTyping(force = false) { + if (this.client.user._typing.has(this.id)) { + const entry = this.client.user._typing.get(this.id); + entry.count--; + if (entry.count <= 0 || force) { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + } + } + } + + /** + * Whether or not the typing indicator is being shown in the channel. + * @type {boolean} + * @readonly + */ + get typing() { + return this.client.user._typing.has(this.id); + } + + /** + * Number of times `startTyping` has been called. + * @type {number} + * @readonly + */ + get typingCount() { + if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count; + return 0; + } + + /** + * Creates a Message Collector + * @param {CollectorFilterFunction} filter The filter to create the collector with + * @param {CollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @example + * // create a message collector + * const collector = channel.createCollector( + * m => m.content.includes('discord'), + * { time: 15000 } + * ); + * collector.on('message', m => console.log(`Collected ${m.content}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createCollector(filter, options = {}) { + return new MessageCollector(this, filter, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {CollectorOptions} AwaitMessagesOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createCollector but in promise form. Resolves with a collection of messages that pass the specified + * filter. + * @param {CollectorFilterFunction} filter The filter function to use + * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise<Collection<string, Message>>} + * @example + * // await !vote messages + * const filter = m => m.content.startsWith('!vote'); + * // errors: ['time'] treats ending because of the time limit as an error + * channel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] }) + * .then(collected => console.log(collected.size)) + * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); + */ + awaitMessages(filter, options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createCollector(filter, options); + collector.on('end', (collection, reason) => { + if (options.errors && options.errors.includes(reason)) { + reject(collection); + } else { + resolve(collection); + } + }); + }); + } + + /** + * Bulk delete given messages. + * <warn>This is only available when using a bot account.</warn> + * @param {Collection<string, Message>|Message[]|number} messages Messages to delete, or number of messages to delete + * @returns {Promise<Collection<string, Message>>} Deleted messages + */ + bulkDelete(messages) { + if (!isNaN(messages)) return this.fetchMessages({ limit: messages }).then(msgs => this.bulkDelete(msgs)); + if (messages instanceof Array || messages instanceof Collection) { + const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id); + return this.client.rest.methods.bulkDeleteMessages(this, messageIDs); + } + throw new TypeError('The messages must be an Array, Collection, or number.'); + } + + _cacheMessage(message) { + const maxSize = this.client.options.messageCacheMaxSize; + if (maxSize === 0) return null; + if (this.messages.size >= maxSize && maxSize > 0) this.messages.delete(this.messages.firstKey()); + this.messages.set(message.id, message); + return message; + } +} + +exports.applyToClass = (structure, full = false) => { + const props = ['send', 'sendMessage', 'sendEmbed', 'sendFile', 'sendCode']; + if (full) { + props.push( + '_cacheMessage', + 'fetchMessages', + 'fetchMessage', + 'bulkDelete', + 'startTyping', + 'stopTyping', + 'typing', + 'typingCount', + 'fetchPinnedMessages', + 'createCollector', + 'awaitMessages' + ); + } + for (const prop of props) { + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop)); + } +}; diff --git a/node_modules/discord.js/src/util/ArraysEqual.js b/node_modules/discord.js/src/util/ArraysEqual.js new file mode 100644 index 0000000..efd8275 --- /dev/null +++ b/node_modules/discord.js/src/util/ArraysEqual.js @@ -0,0 +1,14 @@ +module.exports = function arraysEqual(a, b) { + if (a === b) return true; + if (a.length !== b.length) return false; + + for (const itemInd in a) { + const item = a[itemInd]; + const ind = b.indexOf(item); + if (ind) { + b.splice(ind, 1); + } + } + + return b.length === 0; +}; diff --git a/node_modules/discord.js/src/util/CloneObject.js b/node_modules/discord.js/src/util/CloneObject.js new file mode 100644 index 0000000..13366a7 --- /dev/null +++ b/node_modules/discord.js/src/util/CloneObject.js @@ -0,0 +1,5 @@ +module.exports = function cloneObject(obj) { + const cloned = Object.create(obj); + Object.assign(cloned, obj); + return cloned; +}; diff --git a/node_modules/discord.js/src/util/Collection.js b/node_modules/discord.js/src/util/Collection.js new file mode 100644 index 0000000..bafe710 --- /dev/null +++ b/node_modules/discord.js/src/util/Collection.js @@ -0,0 +1,365 @@ +/** + * A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has + * an ID, for significantly improved performance and ease-of-use. + * @extends {Map} + */ +class Collection extends Map { + constructor(iterable) { + super(iterable); + + /** + * Cached array for the `array()` method - will be reset to `null` whenever `set()` or `delete()` are called. + * @type {?Array} + * @private + */ + this._array = null; + + /** + * Cached array for the `keyArray()` method - will be reset to `null` whenever `set()` or `delete()` are called. + * @type {?Array} + * @private + */ + this._keyArray = null; + } + + set(key, val) { + this._array = null; + this._keyArray = null; + return super.set(key, val); + } + + delete(key) { + this._array = null; + this._keyArray = null; + return super.delete(key); + } + + /** + * Creates an ordered array of the values of this collection, and caches it internally. The array will only be + * reconstructed if an item is added to or removed from the collection, or if you change the length of the array + * itself. If you don't want this caching behaviour, use `Array.from(collection.values())` instead. + * @returns {Array} + */ + array() { + if (!this._array || this._array.length !== this.size) this._array = Array.from(this.values()); + return this._array; + } + + /** + * Creates an ordered array of the keys of this collection, and caches it internally. The array will only be + * reconstructed if an item is added to or removed from the collection, or if you change the length of the array + * itself. If you don't want this caching behaviour, use `Array.from(collection.keys())` instead. + * @returns {Array} + */ + keyArray() { + if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = Array.from(this.keys()); + return this._keyArray; + } + + /** + * Obtains the first item in this collection. + * @returns {*} + */ + first() { + return this.values().next().value; + } + + /** + * Obtains the first key in this collection. + * @returns {*} + */ + firstKey() { + return this.keys().next().value; + } + + /** + * Obtains the last item in this collection. This relies on the `array()` method, and thus the caching mechanism + * applies here as well. + * @returns {*} + */ + last() { + const arr = this.array(); + return arr[arr.length - 1]; + } + + /** + * Obtains the last key in this collection. This relies on the `keyArray()` method, and thus the caching mechanism + * applies here as well. + * @returns {*} + */ + lastKey() { + const arr = this.keyArray(); + return arr[arr.length - 1]; + } + + /** + * Obtains a random item from this collection. This relies on the `array()` method, and thus the caching mechanism + * applies here as well. + * @returns {*} + */ + random() { + const arr = this.array(); + return arr[Math.floor(Math.random() * arr.length)]; + } + + /** + * Obtains a random key from this collection. This relies on the `keyArray()` method, and thus the caching mechanism + * applies here as well. + * @returns {*} + */ + randomKey() { + const arr = this.keyArray(); + return arr[Math.floor(Math.random() * arr.length)]; + } + + /** + * Searches for all items where their specified property's value is identical to the given value + * (`item[prop] === value`). + * @param {string} prop The property to test against + * @param {*} value The expected value + * @returns {Array} + * @example + * collection.findAll('username', 'Bob'); + */ + findAll(prop, value) { + if (typeof prop !== 'string') throw new TypeError('Key must be a string.'); + if (typeof value === 'undefined') throw new Error('Value must be specified.'); + const results = []; + for (const item of this.values()) { + if (item[prop] === value) results.push(item); + } + return results; + } + + /** + * Searches for a single item where its specified property's value is identical to the given value + * (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to + * [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find). + * <warn>Do not use this to obtain an item by its ID. Instead, use `collection.get(id)`. See + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details.</warn> + * @param {string|Function} propOrFn The property to test against, or the function to test with + * @param {*} [value] The expected value - only applicable and required if using a property for the first argument + * @returns {*} + * @example + * collection.find('username', 'Bob'); + * @example + * collection.find(val => val.username === 'Bob'); + */ + find(propOrFn, value) { + if (typeof propOrFn === 'string') { + if (typeof value === 'undefined') throw new Error('Value must be specified.'); + if (propOrFn === 'id') throw new RangeError('Don\'t use .find() with IDs. Instead, use .get(id).'); + for (const item of this.values()) { + if (item[propOrFn] === value) return item; + } + return null; + } else if (typeof propOrFn === 'function') { + for (const [key, val] of this) { + if (propOrFn(val, key, this)) return val; + } + return null; + } else { + throw new Error('First argument must be a property string or a function.'); + } + } + + /* eslint-disable max-len */ + /** + * Searches for the key of a single item where its specified property's value is identical to the given value + * (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to + * [Array.findIndex()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex). + * @param {string|Function} propOrFn The property to test against, or the function to test with + * @param {*} [value] The expected value - only applicable and required if using a property for the first argument + * @returns {*} + * @example + * collection.findKey('username', 'Bob'); + * @example + * collection.findKey(val => val.username === 'Bob'); + */ + /* eslint-enable max-len */ + findKey(propOrFn, value) { + if (typeof propOrFn === 'string') { + if (typeof value === 'undefined') throw new Error('Value must be specified.'); + for (const [key, val] of this) { + if (val[propOrFn] === value) return key; + } + return null; + } else if (typeof propOrFn === 'function') { + for (const [key, val] of this) { + if (propOrFn(val, key, this)) return key; + } + return null; + } else { + throw new Error('First argument must be a property string or a function.'); + } + } + + /** + * Searches for the existence of a single item where its specified property's value is identical to the given value + * (`item[prop] === value`). + * <warn>Do not use this to check for an item by its ID. Instead, use `collection.has(id)`. See + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) for details.</warn> + * @param {string} prop The property to test against + * @param {*} value The expected value + * @returns {boolean} + * @example + * if (collection.exists('username', 'Bob')) { + * console.log('user here!'); + * } + */ + exists(prop, value) { + if (prop === 'id') throw new RangeError('Don\'t use .exists() with IDs. Instead, use .has(id).'); + return Boolean(this.find(prop, value)); + } + + /** + * Identical to + * [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter), + * but returns a Collection instead of an Array. + * @param {Function} fn Function used to test (should return a boolean) + * @param {Object} [thisArg] Value to use as `this` when executing function + * @returns {Collection} + */ + filter(fn, thisArg) { + if (thisArg) fn = fn.bind(thisArg); + const results = new Collection(); + for (const [key, val] of this) { + if (fn(val, key, this)) results.set(key, val); + } + return results; + } + + /** + * Identical to + * [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). + * @param {Function} fn Function used to test (should return a boolean) + * @param {Object} [thisArg] Value to use as `this` when executing function + * @returns {Array} + */ + filterArray(fn, thisArg) { + if (thisArg) fn = fn.bind(thisArg); + const results = []; + for (const [key, val] of this) { + if (fn(val, key, this)) results.push(val); + } + return results; + } + + /** + * Identical to + * [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). + * @param {Function} fn Function that produces an element of the new array, taking three arguments + * @param {*} [thisArg] Value to use as `this` when executing function + * @returns {Array} + */ + map(fn, thisArg) { + if (thisArg) fn = fn.bind(thisArg); + const arr = new Array(this.size); + let i = 0; + for (const [key, val] of this) arr[i++] = fn(val, key, this); + return arr; + } + + /** + * Identical to + * [Array.some()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some). + * @param {Function} fn Function used to test (should return a boolean) + * @param {Object} [thisArg] Value to use as `this` when executing function + * @returns {boolean} + */ + some(fn, thisArg) { + if (thisArg) fn = fn.bind(thisArg); + for (const [key, val] of this) { + if (fn(val, key, this)) return true; + } + return false; + } + + /** + * Identical to + * [Array.every()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every). + * @param {Function} fn Function used to test (should return a boolean) + * @param {Object} [thisArg] Value to use as `this` when executing function + * @returns {boolean} + */ + every(fn, thisArg) { + if (thisArg) fn = fn.bind(thisArg); + for (const [key, val] of this) { + if (!fn(val, key, this)) return false; + } + return true; + } + + /** + * Identical to + * [Array.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce). + * @param {Function} fn Function used to reduce, taking four arguments; `accumulator`, `currentValue`, `currentKey`, + * and `collection` + * @param {*} [initialValue] Starting value for the accumulator + * @returns {*} + */ + reduce(fn, initialValue) { + let accumulator; + if (typeof initialValue !== 'undefined') { + accumulator = initialValue; + for (const [key, val] of this) accumulator = fn(accumulator, val, key, this); + } else { + let first = true; + for (const [key, val] of this) { + if (first) { + accumulator = val; + first = false; + continue; + } + accumulator = fn(accumulator, val, key, this); + } + } + return accumulator; + } + + /** + * Combines this collection with others into a new collection. None of the source collections are modified. + * @param {...Collection} collections Collections to merge + * @returns {Collection} + * @example const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl); + */ + concat(...collections) { + const newColl = new this.constructor(); + for (const [key, val] of this) newColl.set(key, val); + for (const coll of collections) { + for (const [key, val] of coll) newColl.set(key, val); + } + return newColl; + } + + /** + * Calls the `delete()` method on all items that have it. + * @returns {Promise[]} + */ + deleteAll() { + const returns = []; + for (const item of this.values()) { + if (item.delete) returns.push(item.delete()); + } + return returns; + } + + /** + * Checks if this collection shares identical key-value pairings with another. + * This is different to checking for equality using equal-signs, because + * the collections may be different objects, but contain the same data. + * @param {Collection} collection Collection to compare with + * @returns {boolean} Whether the collections have identical contents + */ + equals(collection) { + if (!collection) return false; + if (this === collection) return true; + if (this.size !== collection.size) return false; + return !this.find((value, key) => { + const testVal = collection.get(key); + return testVal !== value || (testVal === undefined && !collection.has(key)); + }); + } +} + +module.exports = Collection; diff --git a/node_modules/discord.js/src/util/Constants.js b/node_modules/discord.js/src/util/Constants.js new file mode 100644 index 0000000..cee2192 --- /dev/null +++ b/node_modules/discord.js/src/util/Constants.js @@ -0,0 +1,371 @@ +exports.Package = require('../../package.json'); + +/** + * Options for a Client. + * @typedef {Object} ClientOptions + * @property {string} [apiRequestMethod='sequential'] 'sequential' or 'burst'. Sequential executes all requests in + * the order they are triggered, whereas burst runs multiple at a time, and doesn't guarantee a particular order. + * @property {number} [shardId=0] The ID of this shard + * @property {number} [shardCount=0] The number of shards + * @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel + * (-1 or Infinity for unlimited - don't do this without message sweeping, otherwise memory usage will climb + * indefinitely) + * @property {number} [messageCacheLifetime=0] How long until a message should be uncached by the message sweeping + * (in seconds, 0 for forever) + * @property {number} [messageSweepInterval=0] How frequently to remove messages from the cache that are older than + * the message cache lifetime (in seconds, 0 for never) + * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as + * upon joining a guild + * @property {boolean} [disableEveryone=false] Default value for MessageOptions.disableEveryone + * @property {boolean} [sync=false] Whether to periodically sync guilds (for userbots) + * @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their + * corresponding websocket events + * @property {number} [restTimeOffset=500] The extra time in millseconds to wait before continuing to make REST + * requests (higher values will reduce rate-limiting errors on bad connections) + * @property {WSEventType[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be + * processed, potentially resulting in performance improvements for larger bots. Only disable events you are + * 100% certain you don't need, as many are important, but not obviously so. The safest one to disable with the + * most impact is typically `TYPING_START`. + * @property {WebsocketOptions} [ws] Options for the websocket + */ +exports.DefaultOptions = { + apiRequestMethod: 'sequential', + shardId: 0, + shardCount: 0, + messageCacheMaxSize: 200, + messageCacheLifetime: 0, + messageSweepInterval: 0, + fetchAllMembers: false, + disableEveryone: false, + sync: false, + restWsBridgeTimeout: 5000, + disabledEvents: [], + restTimeOffset: 500, + + /** + * Websocket options. These are left as snake_case to match the API. + * @typedef {Object} WebsocketOptions + * @property {number} [large_threshold=250] Number of members in a guild to be considered large + * @property {boolean} [compress=true] Whether to compress data sent on the connection. + * Defaults to `false` for browsers. + */ + ws: { + large_threshold: 250, + compress: typeof window === 'undefined', + properties: { + $os: process ? process.platform : 'discord.js', + $browser: 'discord.js', + $device: 'discord.js', + $referrer: '', + $referring_domain: '', + }, + }, +}; + +exports.Errors = { + NO_TOKEN: 'Request to use token, but token was unavailable to the client.', + NO_BOT_ACCOUNT: 'Only bot accounts are able to make use of this feature.', + NO_USER_ACCOUNT: 'Only user accounts are able to make use of this feature.', + BAD_WS_MESSAGE: 'A bad message was received from the websocket; either bad compression, or not JSON.', + TOOK_TOO_LONG: 'Something took too long to do.', + NOT_A_PERMISSION: 'Invalid permission string or number.', + INVALID_RATE_LIMIT_METHOD: 'Unknown rate limiting method.', + BAD_LOGIN: 'Incorrect login details were provided.', + INVALID_SHARD: 'Invalid shard settings were provided.', + INVALID_TOKEN: 'An invalid token was provided.', +}; + +const PROTOCOL_VERSION = exports.PROTOCOL_VERSION = 6; +const HOST = exports.HOST = `https://discordapp.com`; +const API = exports.API = `${HOST}/api/v${PROTOCOL_VERSION}`; +const Endpoints = exports.Endpoints = { + // general + login: `${API}/auth/login`, + logout: `${API}/auth/logout`, + gateway: `${API}/gateway`, + botGateway: `${API}/gateway/bot`, + invite: (id) => `${API}/invite/${id}`, + inviteLink: (id) => `https://discord.gg/${id}`, + assets: (asset) => `${HOST}/assets/${asset}`, + CDN: 'https://cdn.discordapp.com', + + // users + user: (userID) => `${API}/users/${userID}`, + userChannels: (userID) => `${Endpoints.user(userID)}/channels`, + userProfile: (userID) => `${Endpoints.user(userID)}/profile`, + avatar: (userID, avatar) => { + if (userID === '1') return avatar; + return `${Endpoints.CDN}/avatars/${userID}/${avatar}.${avatar.startsWith('a_') ? 'gif' : 'jpg'}?size=1024`; + }, + me: `${API}/users/@me`, + meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`, + meMentions: (limit, roles, everyone, guildID) => + `users/@me/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`, + relationships: (userID) => `${Endpoints.user(userID)}/relationships`, + note: (userID) => `${Endpoints.me}/notes/${userID}`, + + // guilds + guilds: `${API}/guilds`, + guild: (guildID) => `${Endpoints.guilds}/${guildID}`, + guildIcon: (guildID, hash) => `${Endpoints.CDN}/icons/${guildID}/${hash}.jpg`, + guildSplash: (guildID, hash) => `${Endpoints.CDN}/splashes/${guildID}/${hash}.jpg`, + guildPrune: (guildID) => `${Endpoints.guild(guildID)}/prune`, + guildEmbed: (guildID) => `${Endpoints.guild(guildID)}/embed`, + guildInvites: (guildID) => `${Endpoints.guild(guildID)}/invites`, + guildRoles: (guildID) => `${Endpoints.guild(guildID)}/roles`, + guildRole: (guildID, roleID) => `${Endpoints.guildRoles(guildID)}/${roleID}`, + guildBans: (guildID) => `${Endpoints.guild(guildID)}/bans`, + guildIntegrations: (guildID) => `${Endpoints.guild(guildID)}/integrations`, + guildMembers: (guildID) => `${Endpoints.guild(guildID)}/members`, + guildMember: (guildID, memberID) => `${Endpoints.guildMembers(guildID)}/${memberID}`, + guildMemberRole: (guildID, memberID, roleID) => `${Endpoints.guildMember(guildID, memberID)}/roles/${roleID}`, + guildMemberNickname: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`, + guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`, + guildEmojis: (guildID) => `${Endpoints.guild(guildID)}/emojis`, + + // channels + channels: `${API}/channels`, + channel: (channelID) => `${Endpoints.channels}/${channelID}`, + channelMessages: (channelID) => `${Endpoints.channel(channelID)}/messages`, + channelInvites: (channelID) => `${Endpoints.channel(channelID)}/invites`, + channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`, + channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`, + channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`, + channelWebhooks: (channelID) => `${Endpoints.channel(channelID)}/webhooks`, + + // message reactions + messageReactions: (channelID, messageID) => `${Endpoints.channelMessage(channelID, messageID)}/reactions`, + messageReaction: + (channel, msg, emoji, limit) => + `${Endpoints.messageReactions(channel, msg)}/${emoji}` + + `${limit ? `?limit=${limit}` : ''}`, + selfMessageReaction: (channel, msg, emoji, limit) => + `${Endpoints.messageReaction(channel, msg, emoji, limit)}/@me`, + userMessageReaction: (channel, msg, emoji, limit, id) => + `${Endpoints.messageReaction(channel, msg, emoji, limit)}/${id}`, + + // webhooks + webhook: (webhookID, token) => `${API}/webhooks/${webhookID}${token ? `/${token}` : ''}`, + + // oauth + myApplication: `${API}/oauth2/applications/@me`, + getApp: (id) => `${API}/oauth2/authorize?client_id=${id}`, + + // emoji + emoji: (emojiID) => `${Endpoints.CDN}/emojis/${emojiID}.png`, +}; + +exports.Status = { + READY: 0, + CONNECTING: 1, + RECONNECTING: 2, + IDLE: 3, + NEARLY: 4, + DISCONNECTED: 5, +}; + +exports.ChannelTypes = { + text: 0, + DM: 1, + voice: 2, + groupDM: 3, +}; + +exports.OPCodes = { + DISPATCH: 0, + HEARTBEAT: 1, + IDENTIFY: 2, + STATUS_UPDATE: 3, + VOICE_STATE_UPDATE: 4, + VOICE_GUILD_PING: 5, + RESUME: 6, + RECONNECT: 7, + REQUEST_GUILD_MEMBERS: 8, + INVALID_SESSION: 9, + HELLO: 10, + HEARTBEAT_ACK: 11, +}; + +exports.VoiceOPCodes = { + IDENTIFY: 0, + SELECT_PROTOCOL: 1, + READY: 2, + HEARTBEAT: 3, + SESSION_DESCRIPTION: 4, + SPEAKING: 5, +}; + +exports.Events = { + READY: 'ready', + GUILD_CREATE: 'guildCreate', + GUILD_DELETE: 'guildDelete', + GUILD_UPDATE: 'guildUpdate', + GUILD_UNAVAILABLE: 'guildUnavailable', + GUILD_AVAILABLE: 'guildAvailable', + GUILD_MEMBER_ADD: 'guildMemberAdd', + GUILD_MEMBER_REMOVE: 'guildMemberRemove', + GUILD_MEMBER_UPDATE: 'guildMemberUpdate', + GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable', + GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking', + GUILD_MEMBERS_CHUNK: 'guildMembersChunk', + GUILD_ROLE_CREATE: 'roleCreate', + GUILD_ROLE_DELETE: 'roleDelete', + GUILD_ROLE_UPDATE: 'roleUpdate', + GUILD_EMOJI_CREATE: 'emojiCreate', + GUILD_EMOJI_DELETE: 'emojiDelete', + GUILD_EMOJI_UPDATE: 'emojiUpdate', + GUILD_BAN_ADD: 'guildBanAdd', + GUILD_BAN_REMOVE: 'guildBanRemove', + CHANNEL_CREATE: 'channelCreate', + CHANNEL_DELETE: 'channelDelete', + CHANNEL_UPDATE: 'channelUpdate', + CHANNEL_PINS_UPDATE: 'channelPinsUpdate', + MESSAGE_CREATE: 'message', + MESSAGE_DELETE: 'messageDelete', + MESSAGE_UPDATE: 'messageUpdate', + MESSAGE_BULK_DELETE: 'messageDeleteBulk', + MESSAGE_REACTION_ADD: 'messageReactionAdd', + MESSAGE_REACTION_REMOVE: 'messageReactionRemove', + MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', + USER_UPDATE: 'userUpdate', + USER_NOTE_UPDATE: 'userNoteUpdate', + PRESENCE_UPDATE: 'presenceUpdate', + VOICE_STATE_UPDATE: 'voiceStateUpdate', + TYPING_START: 'typingStart', + TYPING_STOP: 'typingStop', + DISCONNECT: 'disconnect', + RECONNECTING: 'reconnecting', + ERROR: 'error', + WARN: 'warn', + DEBUG: 'debug', +}; + +/** + * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: + * - READY + * - GUILD_SYNC + * - GUILD_CREATE + * - GUILD_DELETE + * - GUILD_UPDATE + * - GUILD_MEMBER_ADD + * - GUILD_MEMBER_REMOVE + * - GUILD_MEMBER_UPDATE + * - GUILD_MEMBERS_CHUNK + * - GUILD_ROLE_CREATE + * - GUILD_ROLE_DELETE + * - GUILD_ROLE_UPDATE + * - GUILD_BAN_ADD + * - GUILD_BAN_REMOVE + * - CHANNEL_CREATE + * - CHANNEL_DELETE + * - CHANNEL_UPDATE + * - CHANNEL_PINS_UPDATE + * - MESSAGE_CREATE + * - MESSAGE_DELETE + * - MESSAGE_UPDATE + * - MESSAGE_DELETE_BULK + * - MESSAGE_REACTION_ADD + * - MESSAGE_REACTION_REMOVE + * - MESSAGE_REACTION_REMOVE_ALL + * - USER_UPDATE + * - USER_NOTE_UPDATE + * - PRESENCE_UPDATE + * - VOICE_STATE_UPDATE + * - TYPING_START + * - VOICE_SERVER_UPDATE + * - RELATIONSHIP_ADD + * - RELATIONSHIP_REMOVE + * @typedef {string} WSEventType + */ +exports.WSEvents = { + READY: 'READY', + GUILD_SYNC: 'GUILD_SYNC', + GUILD_CREATE: 'GUILD_CREATE', + GUILD_DELETE: 'GUILD_DELETE', + GUILD_UPDATE: 'GUILD_UPDATE', + GUILD_MEMBER_ADD: 'GUILD_MEMBER_ADD', + GUILD_MEMBER_REMOVE: 'GUILD_MEMBER_REMOVE', + GUILD_MEMBER_UPDATE: 'GUILD_MEMBER_UPDATE', + GUILD_MEMBERS_CHUNK: 'GUILD_MEMBERS_CHUNK', + GUILD_ROLE_CREATE: 'GUILD_ROLE_CREATE', + GUILD_ROLE_DELETE: 'GUILD_ROLE_DELETE', + GUILD_ROLE_UPDATE: 'GUILD_ROLE_UPDATE', + GUILD_BAN_ADD: 'GUILD_BAN_ADD', + GUILD_BAN_REMOVE: 'GUILD_BAN_REMOVE', + GUILD_EMOJIS_UPDATE: 'GUILD_EMOJIS_UPDATE', + CHANNEL_CREATE: 'CHANNEL_CREATE', + CHANNEL_DELETE: 'CHANNEL_DELETE', + CHANNEL_UPDATE: 'CHANNEL_UPDATE', + CHANNEL_PINS_UPDATE: 'CHANNEL_PINS_UPDATE', + MESSAGE_CREATE: 'MESSAGE_CREATE', + MESSAGE_DELETE: 'MESSAGE_DELETE', + MESSAGE_UPDATE: 'MESSAGE_UPDATE', + MESSAGE_DELETE_BULK: 'MESSAGE_DELETE_BULK', + MESSAGE_REACTION_ADD: 'MESSAGE_REACTION_ADD', + MESSAGE_REACTION_REMOVE: 'MESSAGE_REACTION_REMOVE', + MESSAGE_REACTION_REMOVE_ALL: 'MESSAGE_REACTION_REMOVE_ALL', + USER_UPDATE: 'USER_UPDATE', + USER_NOTE_UPDATE: 'USER_NOTE_UPDATE', + PRESENCE_UPDATE: 'PRESENCE_UPDATE', + VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE', + TYPING_START: 'TYPING_START', + VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE', + RELATIONSHIP_ADD: 'RELATIONSHIP_ADD', + RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE', +}; + +exports.MessageTypes = { + 0: 'DEFAULT', + 1: 'RECIPIENT_ADD', + 2: 'RECIPIENT_REMOVE', + 3: 'CALL', + 4: 'CHANNEL_NAME_CHANGE', + 5: 'CHANNEL_ICON_CHANGE', + 6: 'PINS_ADD', +}; + +exports.DefaultAvatars = { + BLURPLE: '6debd47ed13483642cf09e832ed0bc1b', + GREY: '322c936a8c8be1b803cd94861bdfa868', + GREEN: 'dd4dbc0016779df1378e7812eabaa04d', + ORANGE: '0e291f67c9274a1abdddeb3fd919cbaa', + RED: '1cbd08c76f8af6dddce02c5138971129', +}; + +const PermissionFlags = exports.PermissionFlags = { + CREATE_INSTANT_INVITE: 1 << 0, + KICK_MEMBERS: 1 << 1, + BAN_MEMBERS: 1 << 2, + ADMINISTRATOR: 1 << 3, + MANAGE_CHANNELS: 1 << 4, + MANAGE_GUILD: 1 << 5, + ADD_REACTIONS: 1 << 6, + + READ_MESSAGES: 1 << 10, + SEND_MESSAGES: 1 << 11, + SEND_TTS_MESSAGES: 1 << 12, + MANAGE_MESSAGES: 1 << 13, + EMBED_LINKS: 1 << 14, + ATTACH_FILES: 1 << 15, + READ_MESSAGE_HISTORY: 1 << 16, + MENTION_EVERYONE: 1 << 17, + EXTERNAL_EMOJIS: 1 << 18, + + CONNECT: 1 << 20, + SPEAK: 1 << 21, + MUTE_MEMBERS: 1 << 22, + DEAFEN_MEMBERS: 1 << 23, + MOVE_MEMBERS: 1 << 24, + USE_VAD: 1 << 25, + + CHANGE_NICKNAME: 1 << 26, + MANAGE_NICKNAMES: 1 << 27, + MANAGE_ROLES_OR_PERMISSIONS: 1 << 28, + MANAGE_WEBHOOKS: 1 << 29, + MANAGE_EMOJIS: 1 << 30, +}; + +let _ALL_PERMISSIONS = 0; +for (const key in PermissionFlags) _ALL_PERMISSIONS |= PermissionFlags[key]; +exports.ALL_PERMISSIONS = _ALL_PERMISSIONS; +exports.DEFAULT_PERMISSIONS = 104324097; diff --git a/node_modules/discord.js/src/util/ConvertArrayBuffer.js b/node_modules/discord.js/src/util/ConvertArrayBuffer.js new file mode 100644 index 0000000..26b1cc8 --- /dev/null +++ b/node_modules/discord.js/src/util/ConvertArrayBuffer.js @@ -0,0 +1,18 @@ +function arrayBufferToBuffer(ab) { + const buffer = new Buffer(ab.byteLength); + const view = new Uint8Array(ab); + for (var i = 0; i < buffer.length; ++i) buffer[i] = view[i]; + return buffer; +} + +function str2ab(str) { + const buffer = new ArrayBuffer(str.length * 2); + const view = new Uint16Array(buffer); + for (var i = 0, strLen = str.length; i < strLen; i++) view[i] = str.charCodeAt(i); + return buffer; +} + +module.exports = function convertArrayBuffer(x) { + if (typeof x === 'string') x = str2ab(x); + return arrayBufferToBuffer(x); +}; diff --git a/node_modules/discord.js/src/util/EscapeMarkdown.js b/node_modules/discord.js/src/util/EscapeMarkdown.js new file mode 100644 index 0000000..9db8c13 --- /dev/null +++ b/node_modules/discord.js/src/util/EscapeMarkdown.js @@ -0,0 +1,5 @@ +module.exports = function escapeMarkdown(text, onlyCodeBlock = false, onlyInlineCode = false) { + if (onlyCodeBlock) return text.replace(/```/g, '`\u200b``'); + if (onlyInlineCode) return text.replace(/\\(`|\\)/g, '$1').replace(/(`|\\)/g, '\\$1'); + return text.replace(/\\(\*|_|`|~|\\)/g, '$1').replace(/(\*|_|`|~|\\)/g, '\\$1'); +}; diff --git a/node_modules/discord.js/src/util/FetchRecommendedShards.js b/node_modules/discord.js/src/util/FetchRecommendedShards.js new file mode 100644 index 0000000..a60f510 --- /dev/null +++ b/node_modules/discord.js/src/util/FetchRecommendedShards.js @@ -0,0 +1,19 @@ +const superagent = require('superagent'); +const botGateway = require('./Constants').Endpoints.botGateway; + +/** + * Gets the recommended shard count from Discord + * @param {number} token Discord auth token + * @returns {Promise<number>} the recommended number of shards + */ +module.exports = function fetchRecommendedShards(token) { + return new Promise((resolve, reject) => { + if (!token) throw new Error('A token must be provided.'); + superagent.get(botGateway) + .set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`) + .end((err, res) => { + if (err) reject(err); + resolve(res.body.shards); + }); + }); +}; diff --git a/node_modules/discord.js/src/util/MakeError.js b/node_modules/discord.js/src/util/MakeError.js new file mode 100644 index 0000000..bbc84db --- /dev/null +++ b/node_modules/discord.js/src/util/MakeError.js @@ -0,0 +1,6 @@ +module.exports = function makeError(obj) { + const err = new Error(obj.message); + err.name = obj.name; + err.stack = obj.stack; + return err; +}; diff --git a/node_modules/discord.js/src/util/MakePlainError.js b/node_modules/discord.js/src/util/MakePlainError.js new file mode 100644 index 0000000..b409462 --- /dev/null +++ b/node_modules/discord.js/src/util/MakePlainError.js @@ -0,0 +1,7 @@ +module.exports = function makePlainError(err) { + const obj = {}; + obj.name = err.name; + obj.message = err.message; + obj.stack = err.stack; + return obj; +}; diff --git a/node_modules/discord.js/src/util/MergeDefault.js b/node_modules/discord.js/src/util/MergeDefault.js new file mode 100644 index 0000000..b09f970 --- /dev/null +++ b/node_modules/discord.js/src/util/MergeDefault.js @@ -0,0 +1,12 @@ +module.exports = function merge(def, given) { + if (!given) return def; + for (const key in def) { + if (!{}.hasOwnProperty.call(given, key)) { + given[key] = def[key]; + } else if (given[key] === Object(given[key])) { + given[key] = merge(def[key], given[key]); + } + } + + return given; +}; diff --git a/node_modules/discord.js/src/util/ParseEmoji.js b/node_modules/discord.js/src/util/ParseEmoji.js new file mode 100644 index 0000000..d9f7b22 --- /dev/null +++ b/node_modules/discord.js/src/util/ParseEmoji.js @@ -0,0 +1,14 @@ +module.exports = function parseEmoji(text) { + if (text.includes('%')) { + text = decodeURIComponent(text); + } + if (text.includes(':')) { + const [name, id] = text.split(':'); + return { name, id }; + } else { + return { + name: text, + id: null, + }; + } +}; diff --git a/node_modules/discord.js/src/util/SplitMessage.js b/node_modules/discord.js/src/util/SplitMessage.js new file mode 100644 index 0000000..3833f00 --- /dev/null +++ b/node_modules/discord.js/src/util/SplitMessage.js @@ -0,0 +1,16 @@ +module.exports = function splitMessage(text, { maxLength = 1950, char = '\n', prepend = '', append = '' } = {}) { + if (text.length <= maxLength) return text; + const splitText = text.split(char); + if (splitText.length === 1) throw new Error('Message exceeds the max length and contains no split characters.'); + const messages = ['']; + let msg = 0; + for (let i = 0; i < splitText.length; i++) { + if (messages[msg].length + splitText[i].length + 1 > maxLength) { + messages[msg] += append; + messages.push(prepend); + msg++; + } + messages[msg] += (messages[msg].length > 0 && messages[msg] !== prepend ? char : '') + splitText[i]; + } + return messages; +}; |
