diff options
Diffstat (limited to 'node_modules/discord.js')
175 files changed, 24352 insertions, 0 deletions
diff --git a/node_modules/discord.js/.tern-project b/node_modules/discord.js/.tern-project new file mode 100644 index 0000000..8f37bf0 --- /dev/null +++ b/node_modules/discord.js/.tern-project @@ -0,0 +1,21 @@ +{ + "ecmaVersion": 7, + "libs": [], + "loadEagerly": [ + "./src/*.js" + ], + "dontLoad": [ + "node_modules/**" + ], + "plugins": { + "es_modules": {}, + "node": {}, + "doc_comment": { + "fullDocs": true, + "strong": true + }, + "webpack": { + "configPath": "./webpack.config.js", + } + } +} diff --git a/node_modules/discord.js/LICENSE b/node_modules/discord.js/LICENSE new file mode 100644 index 0000000..90cf110 --- /dev/null +++ b/node_modules/discord.js/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2017 Amish Shah + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/node_modules/discord.js/README.md b/node_modules/discord.js/README.md new file mode 100644 index 0000000..abb18fb --- /dev/null +++ b/node_modules/discord.js/README.md @@ -0,0 +1,86 @@ +<div align="center"> + <br /> + <p> + <a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a> + </p> + <br /> + <p> + <a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a> + <a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a> + <a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a> + <a href="https://travis-ci.org/discordjs/discord.js"><img src="https://travis-ci.org/discordjs/discord.js.svg" alt="Build status" /></a> + <a href="https://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a> + </p> + <p> + <a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a> + </p> +</div> + +## About +discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the +[Discord API](https://discordapp.com/developers/docs/intro) very easily. + +- Object-oriented +- Predictable abstractions +- Performant +- 100% coverage of the Discord API + +## Installation +**Node.js 6.0.0 or newer is required.** +Ignore any warnings about unmet peer dependencies, as they're all optional. + +Without voice support: `npm install discord.js` +With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discord.js @discordjs/opus` +With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript` + +### Audio engines +The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus. +Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working. +For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. + +### Optional packages +- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil`) +- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack`) +- One of the following packages can be installed for faster voice packet encryption and decryption: + - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) + - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) + +## Example usage +```js +const Discord = require('discord.js'); +const client = new Discord.Client(); + +client.on('ready', () => { + console.log(`Logged in as ${client.user.tag}!`); +}); + +client.on('message', msg => { + if (msg.content === 'ping') { + msg.reply('pong'); + } +}); + +client.login('token'); +``` + +## Links +* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) +* [Documentation](https://discord.js.org/#/docs) +* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) +* [Discord.js Discord server](https://discord.gg/bRCvFy9) +* [Discord API Discord server](https://discord.gg/discord-api) +* [GitHub](https://github.com/discordjs/discord.js) +* [NPM](https://www.npmjs.com/package/discord.js) +* [Related libraries](https://discordapi.com/unofficial/libs.html) + +### Extensions +* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC)) + +## Contributing +Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the +[documentation](https://discord.js.org/#/docs). +See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR. + +## Help +If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle +nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9). diff --git a/node_modules/discord.js/browser.js b/node_modules/discord.js/browser.js new file mode 100644 index 0000000..9f9341e --- /dev/null +++ b/node_modules/discord.js/browser.js @@ -0,0 +1,9 @@ +const browser = typeof window !== 'undefined'; +const webpack = !!process.env.__DISCORD_WEBPACK__; + +const Discord = require('./'); + +module.exports = Discord; +if (browser && webpack) window.Discord = Discord; // eslint-disable-line no-undef +// eslint-disable-next-line no-console +else if (!browser) console.warn('Warning: Attempting to use browser version of Discord.js in a non-browser environment!'); diff --git a/node_modules/discord.js/package.json b/node_modules/discord.js/package.json new file mode 100644 index 0000000..961d988 --- /dev/null +++ b/node_modules/discord.js/package.json @@ -0,0 +1,155 @@ +{ + "_from": "discord.js@^11.6.3", + "_id": "[email protected]", + "_inBundle": false, + "_integrity": "sha512-dwIzHB5GPU9O/WiNw42d0yco0JED5bOjCPiHwU9VSSCkEhnyOdx5iqt7Dl2PQZAMNzWPI/t9oyCziOuzFlwm3Q==", + "_location": "/discord.js", + "_phantomChildren": {}, + "_requested": { + "type": "range", + "registry": true, + "raw": "discord.js@^11.6.3", + "name": "discord.js", + "escapedName": "discord.js", + "rawSpec": "^11.6.3", + "saveSpec": null, + "fetchSpec": "^11.6.3" + }, + "_requiredBy": [ + "/" + ], + "_resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.6.3.tgz", + "_shasum": "134dbc6b4878a1dd4e32c85a670010799bf8d33e", + "_spec": "discord.js@^11.6.3", + "_where": "E:\\Documents\\GitHub\\s5nical", + "author": { + "name": "Amish Shah", + "email": "[email protected]" + }, + "browser": { + "ws": false, + "uws": false, + "@discordjs/uws": false, + "erlpack": false, + "prism-media": false, + "opusscript": false, + "node-opus": false, + "@discordjs/opus": false, + "tweetnacl": false, + "sodium": false, + "src/sharding/Shard.js": false, + "src/sharding/ShardClientUtil.js": false, + "src/sharding/ShardingManager.js": false, + "src/client/voice/dispatcher/StreamDispatcher.js": false, + "src/client/voice/opus/BaseOpusEngine.js": false, + "src/client/voice/opus/NodeOpusEngine.js": false, + "src/client/voice/opus/DiscordJsOpusEngine.js": false, + "src/client/voice/opus/OpusEngineList.js": false, + "src/client/voice/opus/OpusScriptEngine.js": false, + "src/client/voice/pcm/ConverterEngine.js": false, + "src/client/voice/pcm/ConverterEngineList.js": false, + "src/client/voice/pcm/FfmpegConverterEngine.js": false, + "src/client/voice/player/AudioPlayer.js": false, + "src/client/voice/receiver/VoiceReadable.js": false, + "src/client/voice/receiver/VoiceReceiver.js": false, + "src/client/voice/util/Secretbox.js": false, + "src/client/voice/util/SecretKey.js": false, + "src/client/voice/util/VolumeInterface.js": false, + "src/client/voice/ClientVoiceManager.js": false, + "src/client/voice/VoiceBroadcast.js": false, + "src/client/voice/VoiceConnection.js": false, + "src/client/voice/VoiceUDPClient.js": false, + "src/client/voice/VoiceWebSocket.js": false + }, + "bugs": { + "url": "https://github.com/discordjs/discord.js/issues" + }, + "bundleDependencies": false, + "dependencies": { + "long": "^4.0.0", + "prism-media": "^0.0.4", + "snekfetch": "^3.6.4", + "tweetnacl": "^1.0.0", + "ws": "^6.0.0" + }, + "deprecated": false, + "description": "A powerful library for interacting with the Discord API", + "devDependencies": { + "@types/node": "^9.4.6", + "discord.js-docgen": "github:discordjs/docgen", + "eslint": "^5.4.0", + "parallel-webpack": "^2.3.0", + "tslint": "^3.15.1", + "tslint-config-typings": "^0.2.4", + "typescript": "^3.0.1", + "uglifyjs-webpack-plugin": "^1.3.0", + "webpack": "^4.17.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "homepage": "https://github.com/discordjs/discord.js#readme", + "keywords": [ + "discord", + "api", + "bot", + "client", + "node", + "discordapp" + ], + "license": "Apache-2.0", + "main": "./src/index", + "name": "discord.js", + "peerDependencies": { + "@discordjs/uws": "^10.149.0", + "bufferutil": "^4.0.0", + "erlpack": "discordapp/erlpack", + "libsodium-wrappers": "^0.7.3", + "@discordjs/opus": "^0.1.0", + "node-opus": "^0.2.7", + "opusscript": "^0.0.6", + "sodium": "^2.0.3" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "erlpack": { + "optional": true + }, + "@discordjs/opus": { + "optional": true + }, + "node-opus": { + "optional": true + }, + "opusscript": { + "optional": true + }, + "sodium": { + "optional": true + }, + "libsodium-wrappers": { + "optional": true + }, + "uws": { + "optional": true + } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/discordjs/discord.js.git" + }, + "runkitExampleFilename": "./docs/examples/ping.js", + "scripts": { + "docs": "docgen --source src --custom docs/index.yml --output docs/docs.json", + "docs:test": "docgen --source src --custom docs/index.yml", + "lint": "eslint src", + "lint:fix": "eslint --fix src", + "lint:typings": "tslint typings/index.d.ts typings/discord.js-test.ts", + "test": "npm run lint && npm run docs:test", + "webpack": "parallel-webpack" + }, + "types": "./typings/index.d.ts", + "version": "11.6.3" +} diff --git a/node_modules/discord.js/src/client/Client.js b/node_modules/discord.js/src/client/Client.js new file mode 100644 index 0000000..9ad7c95 --- /dev/null +++ b/node_modules/discord.js/src/client/Client.js @@ -0,0 +1,564 @@ +const EventEmitter = require('events'); +const Constants = require('../util/Constants'); +const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); +const RESTManager = require('./rest/RESTManager'); +const ClientDataManager = require('./ClientDataManager'); +const ClientManager = require('./ClientManager'); +const ClientDataResolver = require('./ClientDataResolver'); +const ClientVoiceManager = require('./voice/ClientVoiceManager'); +const WebSocketManager = require('./websocket/WebSocketManager'); +const ActionsManager = require('./actions/ActionsManager'); +const Collection = require('../util/Collection'); +const Presence = require('../structures/Presence').Presence; +const ShardClientUtil = require('../sharding/ShardClientUtil'); +const VoiceBroadcast = require('./voice/VoiceBroadcast'); + +/** + * The main hub for interacting with the Discord API, and the starting point for any bot. + * @extends {EventEmitter} + */ +class Client extends EventEmitter { + /** + * @param {ClientOptions} [options] Options for the client + */ + constructor(options = {}) { + super(); + + // Obtain shard details from environment + if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID); + if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT); + + /** + * The options the client was instantiated with + * @type {ClientOptions} + */ + this.options = Util.mergeDefault(Constants.DefaultOptions, options); + this._validateOptions(); + + /** + * The REST manager of the client + * @type {RESTManager} + * @private + */ + this.rest = new RESTManager(this); + + /** + * The data manager of the client + * @type {ClientDataManager} + * @private + */ + this.dataManager = new ClientDataManager(this); + + /** + * The manager of the client + * @type {ClientManager} + * @private + */ + this.manager = new ClientManager(this); + + /** + * The WebSocket manager of the client + * @type {WebSocketManager} + * @private + */ + this.ws = new WebSocketManager(this); + + /** + * The data resolver of the client + * @type {ClientDataResolver} + * @private + */ + this.resolver = new ClientDataResolver(this); + + /** + * The action manager of the client + * @type {ActionsManager} + * @private + */ + this.actions = new ActionsManager(this); + + /** + * The voice manager of the client (`null` in browsers) + * @type {?ClientVoiceManager} + * @private + */ + this.voice = !this.browser ? new ClientVoiceManager(this) : null; + + /** + * The shard helpers for the client + * (only if the process was spawned as a child, such as from a {@link ShardingManager}) + * @type {?ShardClientUtil} + */ + this.shard = process.send ? ShardClientUtil.singleton(this) : null; + + /** + * All of the {@link User} objects that have been cached at any point, mapped by their IDs + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(); + + /** + * All of the guilds the client is currently handling, mapped by their IDs - + * as long as sharding isn't being used, this will be *every* guild the bot is a member of + * @type {Collection<Snowflake, Guild>} + */ + this.guilds = new Collection(); + + /** + * All of the {@link Channel}s that the client is currently handling, mapped by their IDs - + * as long as sharding isn't being used, this will be *every* channel in *every* guild, and all DM channels + * @type {Collection<Snowflake, Channel>} + */ + this.channels = new Collection(); + + /** + * Presences that have been received for the client user's friends, mapped by user IDs + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<Snowflake, Presence>} + * @deprecated + */ + this.presences = new Collection(); + + Object.defineProperty(this, 'token', { writable: true }); + if (!this.token && 'CLIENT_TOKEN' in process.env) { + /** + * Authorization token for the logged in user/bot + * <warn>This should be kept private at all times.</warn> + * @type {?string} + */ + this.token = process.env.CLIENT_TOKEN; + } else { + this.token = null; + } + + /** + * User that the client is logged in as + * @type {?ClientUser} + */ + this.user = null; + + /** + * Time at which the client was last regarded as being in the `READY` state + * (each time the client disconnects and successfully reconnects, this will be overwritten) + * @type {?Date} + */ + this.readyAt = null; + + /** + * Active voice broadcasts that have been created + * @type {VoiceBroadcast[]} + */ + this.broadcasts = []; + + /** + * Previous heartbeat pings of the websocket (most recent first, limited to three elements) + * @type {number[]} + */ + this.pings = []; + + /** + * Timeouts set by {@link Client#setTimeout} that are still active + * @type {Set<Timeout>} + * @private + */ + this._timeouts = new Set(); + + /** + * Intervals set by {@link Client#setInterval} that are still active + * @type {Set<Timeout>} + * @private + */ + this._intervals = new Set(); + + if (this.options.messageSweepInterval > 0) { + this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); + } + } + + /** + * Timestamp of the latest ping's start time + * @type {number} + * @private + */ + get _pingTimestamp() { + return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0; + } + + /** + * Current status of the client's connection to Discord + * @type {Status} + * @readonly + */ + get status() { + return this.ws.connection ? this.ws.connection.status : Constants.Status.IDLE; + } + + /** + * How long it has been since the client last entered the `READY` state in milliseconds + * @type {?number} + * @readonly + */ + get uptime() { + return this.readyAt ? Date.now() - this.readyAt : null; + } + + /** + * Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property + * @type {number} + * @readonly + */ + get ping() { + return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; + } + + /** + * All active voice connections that have been established, mapped by guild ID + * @type {Collection<Snowflake, VoiceConnection>} + * @readonly + */ + get voiceConnections() { + if (this.browser) return new Collection(); + return this.voice.connections; + } + + /** + * All custom emojis that the client has access to, mapped by their IDs + * @type {Collection<Snowflake, Emoji>} + * @readonly + */ + get emojis() { + const emojis = new Collection(); + for (const guild of this.guilds.values()) { + for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji); + } + return emojis; + } + + /** + * Timestamp of the time the client was last `READY` at + * @type {?number} + * @readonly + */ + get readyTimestamp() { + return this.readyAt ? this.readyAt.getTime() : null; + } + + /** + * Whether the client is in a browser environment + * @type {boolean} + * @readonly + */ + get browser() { + return typeof window !== 'undefined'; + } + + /** + * Creates a voice broadcast. + * @returns {VoiceBroadcast} + */ + createVoiceBroadcast() { + const broadcast = new VoiceBroadcast(this); + this.broadcasts.push(broadcast); + return broadcast; + } + + /** + * Logs the client in, establishing a websocket connection to Discord. + * <info>Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever + * possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts. + * Bot accounts also have access to many features that user accounts cannot utilise. Automating a user account is + * considered a violation of Discord's ToS.</info> + * @param {string} token Token of the account to log in with + * @returns {Promise<string>} Token of the account used + * @example + * client.login('my token') + * .then(console.log) + * .catch(console.error); + */ + login(token = this.token) { + return this.rest.methods.login(token); + } + + /** + * Logs out, terminates the connection to Discord, and destroys the client. + * @returns {Promise} + */ + destroy() { + for (const t of this._timeouts) clearTimeout(t); + for (const i of this._intervals) clearInterval(i); + this._timeouts.clear(); + this._intervals.clear(); + return this.manager.destroy(); + } + + /** + * Requests a sync of guild data with Discord. + * <info>This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}.</info> + * <warn>This is only available when using a user account.</warn> + * @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] An array or collection of guilds to sync + * @deprecated + */ + syncGuilds(guilds = this.guilds) { + if (this.user.bot) return; + this.ws.send({ + op: 12, + d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id), + }); + } + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * <warn>This is only available when using a bot account.</warn> + * @param {Snowflake} id ID of the user + * @param {boolean} [cache=true] Whether to cache the new user object if it isn't already + * @returns {Promise<User>} + */ + fetchUser(id, cache = true) { + if (this.users.has(id)) return Promise.resolve(this.users.get(id)); + return this.rest.methods.getUser(id, cache); + } + + /** + * Obtains an invite from Discord. + * @param {InviteResolvable} invite Invite code or URL + * @returns {Promise<Invite>} + * @example + * client.fetchInvite('https://discord.gg/bRCvFy9') + * .then(invite => console.log(`Obtained invite with code: ${invite.code}`)) + * .catch(console.error); + */ + fetchInvite(invite) { + const code = this.resolver.resolveInviteCode(invite); + return this.rest.methods.getInvite(code); + } + + /** + * Obtains a webhook from Discord. + * @param {Snowflake} id ID of the webhook + * @param {string} [token] Token for the webhook + * @returns {Promise<Webhook>} + * @example + * client.fetchWebhook('id', 'token') + * .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`)) + * .catch(console.error); + */ + fetchWebhook(id, token) { + return this.rest.methods.getWebhook(id, token); + } + + /** + * Obtains the available voice regions from Discord. + * @returns {Promise<Collection<string, VoiceRegion>>} + * @example + * client.fetchVoiceRegions() + * .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`)) + * .catch(console.error); + */ + fetchVoiceRegions() { + return this.rest.methods.fetchVoiceRegions(); + } + + /** + * Sweeps all text-based channels' messages and removes the ones older than the max message lifetime. + * If the message has been edited, the time of the edit is used rather than the time of the original message. + * @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds) + * will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime} + * @returns {number} Amount of messages that were removed from the caches, + * or -1 if the message cache lifetime is unlimited + */ + sweepMessages(lifetime = this.options.messageCacheLifetime) { + if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.'); + if (lifetime <= 0) { + this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited'); + return -1; + } + + const lifetimeMs = lifetime * 1000; + const now = Date.now(); + let channels = 0; + let messages = 0; + + for (const channel of this.channels.values()) { + if (!channel.messages) continue; + channels++; + + messages += channel.messages.sweep( + message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs + ); + } + + this.emit('debug', `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`); + return messages; + } + + /** + * Obtains the OAuth Application of the bot from Discord. + * <warn>Bots can only fetch their own profile.</warn> + * @param {Snowflake} [id='@me'] ID of application to fetch + * @returns {Promise<OAuth2Application>} + * @example + * client.fetchApplication() + * .then(application => console.log(`Obtained application with name: ${application.name}`)) + * .catch(console.error); + */ + fetchApplication(id = '@me') { + if (id !== '@me') process.emitWarning('fetchApplication: use "@me" as an argument', 'DeprecationWarning'); + return this.rest.methods.getApplication(id); + } + + /** + * Generates a link that can be used to invite the bot to a guild. + * <warn>This is only available when using a bot account.</warn> + * @param {PermissionResolvable} [permissions] Permissions to request + * @returns {Promise<string>} + * @example + * client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE']) + * .then(link => console.log(`Generated bot invite link: ${link}`)) + * .catch(console.error); + */ + generateInvite(permissions) { + permissions = Permissions.resolve(permissions); + return this.fetchApplication().then(application => + `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot` + ); + } + + /** + * Sets a timeout that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {number} delay Time to wait before executing (in milliseconds) + * @param {...*} args Arguments for the function + * @returns {Timeout} + */ + setTimeout(fn, delay, ...args) { + const timeout = setTimeout(() => { + fn(...args); + this._timeouts.delete(timeout); + }, delay); + this._timeouts.add(timeout); + return timeout; + } + + /** + * Clears a timeout. + * @param {Timeout} timeout Timeout to cancel + */ + clearTimeout(timeout) { + clearTimeout(timeout); + this._timeouts.delete(timeout); + } + + /** + * Sets an interval that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {number} delay Time to wait before executing (in milliseconds) + * @param {...*} args Arguments for the function + * @returns {Timeout} + */ + setInterval(fn, delay, ...args) { + const interval = setInterval(fn, delay, ...args); + this._intervals.add(interval); + return interval; + } + + /** + * Clears an interval. + * @param {Timeout} interval Interval to cancel + */ + clearInterval(interval) { + clearInterval(interval); + this._intervals.delete(interval); + } + + /** + * Adds a ping to {@link Client#pings}. + * @param {number} startTime Starting time of the ping + * @private + */ + _pong(startTime) { + this.pings.unshift(Date.now() - startTime); + if (this.pings.length > 3) this.pings.length = 3; + this.ws.lastHeartbeatAck = true; + } + + /** + * Adds/updates a friend's presence in {@link Client#presences}. + * @param {Snowflake} id ID of the user + * @param {Object} presence Raw presence object from Discord + * @private + */ + _setPresence(id, presence) { + if (this.presences.has(id)) { + this.presences.get(id).update(presence); + return; + } + this.presences.set(id, new Presence(presence, this)); + } + + /** + * Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script + * with the client as `this`. + * @param {string} script Script to eval + * @returns {*} + * @private + */ + _eval(script) { + return eval(script); + } + + /** + * Validates the client options. + * @param {ClientOptions} [options=this.options] Options to validate + * @private + */ + _validateOptions(options = this.options) { // eslint-disable-line complexity + if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) { + throw new TypeError('The shardCount option must be a number.'); + } + if (typeof options.shardId !== 'number' || isNaN(options.shardId)) { + throw new TypeError('The shardId option must be a number.'); + } + if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.'); + if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.'); + if (options.shardId !== 0 && options.shardId >= options.shardCount) { + throw new RangeError('The shardId option must be less than shardCount.'); + } + if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { + throw new TypeError('The messageCacheMaxSize option must be a number.'); + } + if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) { + throw new TypeError('The messageCacheLifetime option must be a number.'); + } + if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) { + throw new TypeError('The messageSweepInterval option must be a number.'); + } + if (typeof options.fetchAllMembers !== 'boolean') { + throw new TypeError('The fetchAllMembers option must be a boolean.'); + } + if (typeof options.disableEveryone !== 'boolean') { + throw new TypeError('The disableEveryone option must be a boolean.'); + } + if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { + throw new TypeError('The restWsBridgeTimeout option must be a number.'); + } + if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.'); + if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { + throw new TypeError('The retryLimit options must be a number.'); + } + } +} + +module.exports = Client; + +/** + * Emitted for general warnings. + * @event Client#warn + * @param {string} info The warning + */ + +/** + * Emitted for general debugging information. + * @event Client#debug + * @param {string} info The debug information + */ diff --git a/node_modules/discord.js/src/client/ClientDataManager.js b/node_modules/discord.js/src/client/ClientDataManager.js new file mode 100644 index 0000000..4f0f2d7 --- /dev/null +++ b/node_modules/discord.js/src/client/ClientDataManager.js @@ -0,0 +1,149 @@ +const Constants = require('../util/Constants'); +const Util = require('../util/Util'); +const Guild = require('../structures/Guild'); +const User = require('../structures/User'); +const Emoji = require('../structures/Emoji'); +const GuildChannel = require('../structures/GuildChannel'); +const TextChannel = require('../structures/TextChannel'); +const VoiceChannel = require('../structures/VoiceChannel'); +const CategoryChannel = require('../structures/CategoryChannel'); +const NewsChannel = require('../structures/NewsChannel'); +const StoreChannel = require('../structures/StoreChannel'); +const DMChannel = require('../structures/DMChannel'); +const GroupDMChannel = require('../structures/GroupDMChannel'); + +class ClientDataManager { + constructor(client) { + this.client = client; + } + + get pastReady() { + return this.client.ws.connection.status === Constants.Status.READY; + } + + newGuild(data) { + const already = this.client.guilds.has(data.id); + const guild = new Guild(this.client, data); + this.client.guilds.set(guild.id, guild); + if (this.pastReady && !already) { + /** + * Emitted whenever the client joins a guild. + * @event Client#guildCreate + * @param {Guild} guild The created guild + */ + if (this.client.options.fetchAllMembers) { + guild.fetchMembers().then(() => { this.client.emit(Constants.Events.GUILD_CREATE, guild); }); + } else { + this.client.emit(Constants.Events.GUILD_CREATE, guild); + } + } + + return guild; + } + + newUser(data, cache = true) { + if (this.client.users.has(data.id)) return this.client.users.get(data.id); + const user = new User(this.client, data); + if (cache) this.client.users.set(user.id, user); + return user; + } + + newChannel(data, guild) { + const already = this.client.channels.has(data.id); + let channel; + if (data.type === Constants.ChannelTypes.DM) { + channel = new DMChannel(this.client, data); + } else if (data.type === Constants.ChannelTypes.GROUP_DM) { + channel = new GroupDMChannel(this.client, data); + } else { + guild = guild || this.client.guilds.get(data.guild_id); + if (already) { + channel = this.client.channels.get(data.id); + } else if (guild) { + switch (data.type) { + case Constants.ChannelTypes.TEXT: + channel = new TextChannel(guild, data); + break; + case Constants.ChannelTypes.VOICE: + channel = new VoiceChannel(guild, data); + break; + case Constants.ChannelTypes.CATEGORY: + channel = new CategoryChannel(guild, data); + break; + case Constants.ChannelTypes.NEWS: + channel = new NewsChannel(guild, data); + break; + case Constants.ChannelTypes.STORE: + channel = new StoreChannel(guild, data); + break; + } + + guild.channels.set(channel.id, channel); + } + } + + if (channel && !already) { + if (this.pastReady) this.client.emit(Constants.Events.CHANNEL_CREATE, channel); + this.client.channels.set(channel.id, channel); + return channel; + } else if (already) { + return channel; + } + + return null; + } + + newEmoji(data, guild) { + const already = guild.emojis.has(data.id); + if (data && !already) { + let emoji = new Emoji(guild, data); + this.client.emit(Constants.Events.GUILD_EMOJI_CREATE, emoji); + guild.emojis.set(emoji.id, emoji); + return emoji; + } else if (already) { + return guild.emojis.get(data.id); + } + + return null; + } + + killEmoji(emoji) { + if (!(emoji instanceof Emoji && emoji.guild)) return; + this.client.emit(Constants.Events.GUILD_EMOJI_DELETE, emoji); + emoji.guild.emojis.delete(emoji.id); + } + + killGuild(guild) { + const already = this.client.guilds.has(guild.id); + this.client.guilds.delete(guild.id); + if (already && this.pastReady) this.client.emit(Constants.Events.GUILD_DELETE, guild); + } + + killUser(user) { + this.client.users.delete(user.id); + } + + killChannel(channel) { + this.client.channels.delete(channel.id); + if (channel instanceof GuildChannel) channel.guild.channels.delete(channel.id); + } + + updateGuild(currentGuild, newData) { + const oldGuild = Util.cloneObject(currentGuild); + currentGuild.setup(newData); + if (this.pastReady) this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild); + } + + updateChannel(currentChannel, newData) { + currentChannel.setup(newData); + } + + updateEmoji(currentEmoji, newData) { + const oldEmoji = Util.cloneObject(currentEmoji); + currentEmoji.setup(newData); + this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji); + return currentEmoji; + } +} + +module.exports = ClientDataManager; diff --git a/node_modules/discord.js/src/client/ClientDataResolver.js b/node_modules/discord.js/src/client/ClientDataResolver.js new file mode 100644 index 0000000..50a7bb9 --- /dev/null +++ b/node_modules/discord.js/src/client/ClientDataResolver.js @@ -0,0 +1,376 @@ +const path = require('path'); +const fs = require('fs'); +const snekfetch = require('snekfetch'); + +const Constants = require('../util/Constants'); +const convertToBuffer = require('../util/Util').convertToBuffer; +const User = require('../structures/User'); +const Message = require('../structures/Message'); +const Guild = require('../structures/Guild'); +const Channel = require('../structures/Channel'); +const GuildMember = require('../structures/GuildMember'); +const Emoji = require('../structures/Emoji'); +const ReactionEmoji = require('../structures/ReactionEmoji'); +const Role = require('../structures/Role'); + +/** + * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g. + * extracting a User from a Message object. + * @private + */ +class ClientDataResolver { + /** + * @param {Client} client The client the resolver is for + */ + constructor(client) { + this.client = client; + } + + /** + * Data that resolves to give a User object. This can be: + * * A User object + * * A Snowflake + * * A Message object (resolves to the message author) + * * A Guild object (owner of the guild) + * * A GuildMember object + * @typedef {User|Snowflake|Message|Guild|GuildMember} UserResolvable + */ + + /** + * Resolves a UserResolvable to a User object. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?User} + */ + resolveUser(user) { + if (user instanceof User) return user; + if (typeof user === 'string') return this.client.users.get(user) || null; + if (user instanceof GuildMember) return user.user; + if (user instanceof Message) return user.author; + if (user instanceof Guild) return this.resolveUser(user.ownerID); + return null; + } + + /** + * Resolves a UserResolvable to a user ID string. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?Snowflake} + */ + resolveUserID(user) { + if (user instanceof User || user instanceof GuildMember) return user.id; + if (typeof user === 'string') return user || null; + if (user instanceof Message) return user.author.id; + if (user instanceof Guild) return user.ownerID; + return null; + } + + /** + * Data that resolves to give a Guild object. This can be: + * * A Guild object + * * A Snowflake + * @typedef {Guild|Snowflake} GuildResolvable + */ + + /** + * Resolves a GuildResolvable to a Guild object. + * @param {GuildResolvable} guild The GuildResolvable to identify + * @returns {?Guild} + */ + resolveGuild(guild) { + if (guild instanceof Guild) return guild; + if (typeof guild === 'string') return this.client.guilds.get(guild) || null; + return null; + } + + /** + * Data that resolves to give a GuildMember object. This can be: + * * A GuildMember object + * * A User object + * @typedef {GuildMember|User} GuildMemberResolvable + */ + + /** + * Resolves a GuildMemberResolvable to a GuildMember object. + * @param {GuildResolvable} guild The guild that the member is part of + * @param {UserResolvable} user The user that is part of the guild + * @returns {?GuildMember} + */ + resolveGuildMember(guild, user) { + if (user instanceof GuildMember) return user; + guild = this.resolveGuild(guild); + user = this.resolveUser(user); + if (!guild || !user) return null; + return guild.members.get(user.id) || null; + } + + /** + * Data that can be resolved to a Role object. This can be: + * * A Role + * * A Snowflake + * @typedef {Role|Snowflake} RoleResolvable + */ + + /** + * Resolves a RoleResolvable to a Role object. + * @param {GuildResolvable} guild The guild that this role is part of + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Role} + */ + resolveRole(guild, role) { + if (role instanceof Role) return role; + guild = this.resolveGuild(guild); + if (!guild) return null; + if (typeof role === 'string') return guild.roles.get(role); + return null; + } + + /** + * Data that can be resolved to give a Channel object. This can be: + * * A Channel object + * * A Message object (the channel the message was sent in) + * * A Guild object (the #general channel) + * * A Snowflake + * @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable + */ + + /** + * Resolves a ChannelResolvable to a Channel object. + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?Channel} + */ + resolveChannel(channel) { + if (channel instanceof Channel) return channel; + if (typeof channel === 'string') return this.client.channels.get(channel) || null; + if (channel instanceof Message) return channel.channel; + if (channel instanceof Guild) return channel.channels.get(channel.id) || null; + return null; + } + + /** + * Resolves a ChannelResolvable to a channel ID. + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?Snowflake} + */ + resolveChannelID(channel) { + if (channel instanceof Channel) return channel.id; + if (typeof channel === 'string') return channel; + if (channel instanceof Message) return channel.channel.id; + if (channel instanceof Guild) return channel.defaultChannel.id; + return null; + } + + /** + * Data that can be resolved to give an invite code. This can be: + * * An invite code + * * An invite URL + * @typedef {string} InviteResolvable + */ + + /** + * Resolves InviteResolvable to an invite code. + * @param {InviteResolvable} data The invite resolvable to resolve + * @returns {string} + */ + resolveInviteCode(data) { + const inviteRegex = /discord(?:app\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/i; + const match = inviteRegex.exec(data); + if (match && match[1]) return match[1]; + return data; + } + + /** + * Data that can be resolved to give a string. This can be: + * * A string + * * An array (joined with a new line delimiter to give a string) + * * Any value + * @typedef {string|Array|*} StringResolvable + */ + + /** + * Resolves a StringResolvable to a string. + * @param {StringResolvable} data The string resolvable to resolve + * @returns {string} + */ + resolveString(data) { + if (typeof data === 'string') return data; + if (data instanceof Array) return data.join('\n'); + return String(data); + } + + + /** + * Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image. + * @param {BufferResolvable|Base64Resolvable} image The image to be resolved + * @returns {Promise<?string>} + */ + resolveImage(image) { + if (!image) return Promise.resolve(null); + if (typeof image === 'string' && image.startsWith('data:')) { + return Promise.resolve(image); + } + return this.resolveFile(image).then(this.resolveBase64); + } + + /** + * Data that resolves to give a Base64 string, typically for image uploading. This can be: + * * A Buffer + * * A base64 string + * @typedef {Buffer|string} Base64Resolvable + */ + + /** + * Resolves a Base64Resolvable to a Base 64 image. + * @param {Base64Resolvable} data The base 64 resolvable you want to resolve + * @returns {?string} + */ + resolveBase64(data) { + if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`; + return data; + } + + /** + * Data that can be resolved to give a Buffer. This can be: + * * A Buffer + * * The path to a local file + * * A URL + * * A Stream + * @typedef {string|Buffer} BufferResolvable + */ + + /** + * @external Stream + * @see {@link https://nodejs.org/api/stream.html} + */ + + /** + * Resolves a BufferResolvable to a Buffer. + * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve + * @returns {Promise<Buffer>} + */ + resolveFile(resource) { + if (resource instanceof Buffer) return Promise.resolve(resource); + if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource)); + + if (typeof resource === 'string') { + if (/^https?:\/\//.test(resource)) { + return snekfetch.get(resource).then(res => res.body instanceof Buffer ? res.body : Buffer.from(res.text)); + } + return new Promise((resolve, reject) => { + const file = path.resolve(resource); + fs.stat(file, (err, stats) => { + if (err) return reject(err); + if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`)); + fs.readFile(file, (err2, data) => { + if (err2) reject(err2); + else resolve(data); + }); + return null; + }); + }); + } else if (resource && resource.pipe && typeof resource.pipe === 'function') { + return new Promise((resolve, reject) => { + const buffers = []; + resource.once('error', reject); + resource.on('data', data => buffers.push(data)); + resource.once('end', () => resolve(Buffer.concat(buffers))); + }); + } + + return Promise.reject(new TypeError('The resource must be a string or Buffer.')); + } + + /** + * Data that can be resolved to give an emoji identifier. This can be: + * * The unicode representation of an emoji + * * A custom emoji ID + * * An Emoji object + * * A ReactionEmoji object + * @typedef {string|Emoji|ReactionEmoji} EmojiIdentifierResolvable + */ + + /** + * Resolves an EmojiResolvable to an emoji identifier. + * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve + * @returns {?string} + */ + resolveEmojiIdentifier(emoji) { + if (emoji instanceof Emoji || emoji instanceof ReactionEmoji) return emoji.identifier; + if (typeof emoji === 'string') { + if (this.client.emojis.has(emoji)) return this.client.emojis.get(emoji).identifier; + else if (!emoji.includes('%')) return encodeURIComponent(emoji); + else return emoji; + } + return null; + } + + /** + * Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following + * ``` + * [ + * 'DEFAULT', + * 'WHITE', + * 'AQUA', + * 'GREEN', + * 'BLUE', + * 'PURPLE', + * 'LUMINOUS_VIVID_PINK', + * 'GOLD', + * 'ORANGE', + * 'RED', + * 'GREY', + * 'DARKER_GREY', + * 'NAVY', + * 'DARK_AQUA', + * 'DARK_GREEN', + * 'DARK_BLUE', + * 'DARK_PURPLE', + * 'DARK_VIVID_PINK', + * 'DARK_GOLD', + * 'DARK_ORANGE', + * 'DARK_RED', + * 'DARK_GREY', + * 'LIGHT_GREY', + * 'DARK_NAVY', + * 'RANDOM', + * ] + * ``` + * or something like + * ``` + * [255, 0, 255] + * ``` + * for purple + * @typedef {string|number|Array} ColorResolvable + */ + + /** + * Resolves a ColorResolvable into a color number. + * @param {ColorResolvable} color Color to resolve + * @returns {number} A color + */ + static resolveColor(color) { + if (typeof color === 'string') { + if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1)); + if (color === 'DEFAULT') return 0; + color = Constants.Colors[color] || parseInt(color.replace('#', ''), 16); + } else if (color instanceof Array) { + color = (color[0] << 16) + (color[1] << 8) + color[2]; + } + + if (color < 0 || color > 0xFFFFFF) { + throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).'); + } else if (color && isNaN(color)) { + throw new TypeError('Unable to convert color to a number.'); + } + + return color; + } + + /** + * @param {ColorResolvable} color Color to resolve + * @returns {number} A color + */ + resolveColor(color) { + return this.constructor.resolveColor(color); + } +} + +module.exports = ClientDataResolver; diff --git a/node_modules/discord.js/src/client/ClientManager.js b/node_modules/discord.js/src/client/ClientManager.js new file mode 100644 index 0000000..0f2480c --- /dev/null +++ b/node_modules/discord.js/src/client/ClientManager.js @@ -0,0 +1,74 @@ +const Constants = require('../util/Constants'); +const WebSocketConnection = require('./websocket/WebSocketConnection'); + +/** + * Manages the state and background tasks of the client. + * @private + */ +class ClientManager { + constructor(client) { + /** + * The client that instantiated this Manager + * @type {Client} + */ + this.client = client; + + /** + * The heartbeat interval + * @type {?number} + */ + this.heartbeatInterval = null; + } + + /** + * The status of the client + * @type {number} + */ + get status() { + return this.connection ? this.connection.status : Constants.Status.IDLE; + } + + /** + * Connects the client to the WebSocket. + * @param {string} token The authorization token + * @param {Function} resolve Function to run when connection is successful + * @param {Function} reject Function to run when connection fails + */ + connectToWebSocket(token, resolve, reject) { + this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`); + this.client.token = token; + const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300); + this.client.rest.methods.getGateway().then(res => { + const protocolVersion = Constants.DefaultOptions.ws.version; + const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`; + this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`); + this.client.ws.connect(gateway); + this.client.ws.connection.once('error', reject); + this.client.ws.connection.once('close', event => { + if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN)); + if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD)); + if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED)); + }); + this.client.once(Constants.Events.READY, () => { + resolve(token); + this.client.clearTimeout(timeout); + }); + }, reject); + } + + destroy() { + this.client.ws.destroy(); + this.client.rest.destroy(); + if (!this.client.user) return Promise.resolve(); + if (this.client.user.bot) { + this.client.token = null; + return Promise.resolve(); + } else { + return this.client.rest.methods.logout().then(() => { + this.client.token = null; + }); + } + } +} + +module.exports = ClientManager; diff --git a/node_modules/discord.js/src/client/WebhookClient.js b/node_modules/discord.js/src/client/WebhookClient.js new file mode 100644 index 0000000..99291b5 --- /dev/null +++ b/node_modules/discord.js/src/client/WebhookClient.js @@ -0,0 +1,118 @@ +const Webhook = require('../structures/Webhook'); +const RESTManager = require('./rest/RESTManager'); +const ClientDataResolver = require('./ClientDataResolver'); +const Constants = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * The webhook client. + * @extends {Webhook} + */ +class WebhookClient extends Webhook { + /** + * @param {Snowflake} id ID of the webhook + * @param {string} token Token of the webhook + * @param {ClientOptions} [options] Options for the client + * @example + * // Create a new webhook and send a message + * const hook = new Discord.WebhookClient('1234', 'abcdef'); + * hook.sendMessage('This will send a message').catch(console.error); + */ + constructor(id, token, options) { + super(null, id, token); + + /** + * The options the client was instantiated with + * @type {ClientOptions} + */ + this.options = Util.mergeDefault(Constants.DefaultOptions, options); + + /** + * The REST manager of the client + * @type {RESTManager} + * @private + */ + this.rest = new RESTManager(this); + + /** + * The data resolver of the client + * @type {ClientDataResolver} + * @private + */ + this.resolver = new ClientDataResolver(this); + + /** + * Timeouts set by {@link WebhookClient#setTimeout} that are still active + * @type {Set<Timeout>} + * @private + */ + this._timeouts = new Set(); + + /** + * Intervals set by {@link WebhookClient#setInterval} that are still active + * @type {Set<Timeout>} + * @private + */ + this._intervals = new Set(); + } + + /** + * Sets a timeout that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {number} delay Time to wait before executing (in milliseconds) + * @param {...*} args Arguments for the function + * @returns {Timeout} + */ + setTimeout(fn, delay, ...args) { + const timeout = setTimeout(() => { + fn(...args); + this._timeouts.delete(timeout); + }, delay); + this._timeouts.add(timeout); + return timeout; + } + + /** + * Clears a timeout. + * @param {Timeout} timeout Timeout to cancel + */ + clearTimeout(timeout) { + clearTimeout(timeout); + this._timeouts.delete(timeout); + } + + /** + * Sets an interval that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {number} delay Time to wait before executing (in milliseconds) + * @param {...*} args Arguments for the function + * @returns {Timeout} + */ + setInterval(fn, delay, ...args) { + const interval = setInterval(fn, delay, ...args); + this._intervals.add(interval); + return interval; + } + + /** + * Clears an interval. + * @param {Timeout} interval Interval to cancel + */ + clearInterval(interval) { + clearInterval(interval); + this._intervals.delete(interval); + } + + + /** + * Destroys the client. + */ + destroy() { + for (const t of this._timeouts) clearTimeout(t); + for (const i of this._intervals) clearInterval(i); + this._timeouts.clear(); + this._intervals.clear(); + } +} + +module.exports = WebhookClient; diff --git a/node_modules/discord.js/src/client/actions/Action.js b/node_modules/discord.js/src/client/actions/Action.js new file mode 100644 index 0000000..8fdadc9 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/Action.js @@ -0,0 +1,23 @@ +/* + +ABOUT ACTIONS + +Actions are similar to WebSocket Packet Handlers, but since introducing +the REST API methods, in order to prevent rewriting code to handle data, +"actions" have been introduced. They're basically what Packet Handlers +used to be but they're strictly for manipulating data and making sure +that WebSocket events don't clash with REST methods. + +*/ + +class GenericAction { + constructor(client) { + this.client = client; + } + + handle(data) { + return data; + } +} + +module.exports = GenericAction; diff --git a/node_modules/discord.js/src/client/actions/ActionsManager.js b/node_modules/discord.js/src/client/actions/ActionsManager.js new file mode 100644 index 0000000..f9739cc --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ActionsManager.js @@ -0,0 +1,43 @@ +class ActionsManager { + constructor(client) { + this.client = client; + + this.register(require('./MessageCreate')); + this.register(require('./MessageDelete')); + this.register(require('./MessageDeleteBulk')); + this.register(require('./MessageUpdate')); + this.register(require('./MessageReactionAdd')); + this.register(require('./MessageReactionRemove')); + this.register(require('./MessageReactionRemoveEmoji')); + this.register(require('./MessageReactionRemoveAll')); + this.register(require('./ChannelCreate')); + this.register(require('./ChannelDelete')); + this.register(require('./ChannelUpdate')); + this.register(require('./GuildDelete')); + this.register(require('./GuildUpdate')); + this.register(require('./GuildMemberGet')); + this.register(require('./GuildMemberRemove')); + this.register(require('./GuildBanRemove')); + this.register(require('./GuildRoleCreate')); + this.register(require('./GuildRoleDelete')); + this.register(require('./GuildRoleUpdate')); + this.register(require('./InviteCreate')); + this.register(require('./InviteDelete')); + this.register(require('./UserGet')); + this.register(require('./UserUpdate')); + this.register(require('./UserNoteUpdate')); + this.register(require('./GuildSync')); + this.register(require('./GuildEmojiCreate')); + this.register(require('./GuildEmojiDelete')); + this.register(require('./GuildEmojiUpdate')); + this.register(require('./GuildEmojisUpdate')); + this.register(require('./GuildRolesPositionUpdate')); + this.register(require('./GuildChannelsPositionUpdate')); + } + + register(Action) { + this[Action.name.replace(/Action$/, '')] = new Action(this.client); + } +} + +module.exports = ActionsManager; diff --git a/node_modules/discord.js/src/client/actions/ChannelCreate.js b/node_modules/discord.js/src/client/actions/ChannelCreate.js new file mode 100644 index 0000000..83b1aa0 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelCreate.js @@ -0,0 +1,11 @@ +const Action = require('./Action'); + +class ChannelCreateAction extends Action { + handle(data) { + const client = this.client; + const channel = client.dataManager.newChannel(data); + return { channel }; + } +} + +module.exports = ChannelCreateAction; diff --git a/node_modules/discord.js/src/client/actions/ChannelDelete.js b/node_modules/discord.js/src/client/actions/ChannelDelete.js new file mode 100644 index 0000000..6def629 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelDelete.js @@ -0,0 +1,38 @@ +const Action = require('./Action'); +const DMChannel = require('../../structures/DMChannel'); + +class ChannelDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + + let channel = client.channels.get(data.id); + if (channel) { + client.dataManager.killChannel(channel); + this.deleted.set(channel.id, channel); + this.scheduleForDeletion(channel.id); + } else { + channel = this.deleted.get(data.id) || null; + } + if (channel) { + if (channel.messages && !(channel instanceof DMChannel)) { + for (const message of channel.messages.values()) { + message.deleted = true; + } + } + channel.deleted = true; + } + + return { channel }; + } + + scheduleForDeletion(id) { + this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout); + } +} + +module.exports = ChannelDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/ChannelUpdate.js b/node_modules/discord.js/src/client/actions/ChannelUpdate.js new file mode 100644 index 0000000..ba1c4ef --- /dev/null +++ b/node_modules/discord.js/src/client/actions/ChannelUpdate.js @@ -0,0 +1,74 @@ +const Action = require('./Action'); +const TextChannel = require('../../structures/TextChannel'); +const VoiceChannel = require('../../structures/VoiceChannel'); +const CategoryChannel = require('../../structures/CategoryChannel'); +const NewsChannel = require('../../structures/NewsChannel'); +const StoreChannel = require('../../structures/StoreChannel'); +const Constants = require('../../util/Constants'); +const ChannelTypes = Constants.ChannelTypes; +const Util = require('../../util/Util'); + +class ChannelUpdateAction extends Action { + handle(data) { + const client = this.client; + + let channel = client.channels.get(data.id); + if (channel) { + const oldChannel = Util.cloneObject(channel); + + // If the channel is changing types, we need to follow a different process + if (ChannelTypes[channel.type.toUpperCase()] !== data.type) { + // Determine which channel class we're changing to + let channelClass; + switch (data.type) { + case ChannelTypes.TEXT: + channelClass = TextChannel; + break; + case ChannelTypes.VOICE: + channelClass = VoiceChannel; + break; + case ChannelTypes.CATEGORY: + channelClass = CategoryChannel; + break; + case ChannelTypes.NEWS: + channelClass = NewsChannel; + break; + case ChannelTypes.STORE: + channelClass = StoreChannel; + break; + } + + // Create the new channel instance and copy over cached data + const newChannel = new channelClass(channel.guild, data); + if (channel.messages && newChannel.messages) { + for (const [id, message] of channel.messages) newChannel.messages.set(id, message); + } + + channel = newChannel; + this.client.channels.set(channel.id, channel); + } else { + channel.setup(data); + } + + client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel); + return { + old: oldChannel, + updated: channel, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * Emitted whenever a channel is updated - e.g. name change, topic change. + * @event Client#channelUpdate + * @param {Channel} oldChannel The channel before the update + * @param {Channel} newChannel The channel after the update + */ + +module.exports = ChannelUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildBanRemove.js b/node_modules/discord.js/src/client/actions/GuildBanRemove.js new file mode 100644 index 0000000..0276a52 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildBanRemove.js @@ -0,0 +1,13 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class GuildBanRemove extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + const user = client.dataManager.newUser(data.user); + if (guild && user) client.emit(Constants.Events.GUILD_BAN_REMOVE, guild, user); + } +} + +module.exports = GuildBanRemove; diff --git a/node_modules/discord.js/src/client/actions/GuildChannelsPositionUpdate.js b/node_modules/discord.js/src/client/actions/GuildChannelsPositionUpdate.js new file mode 100644 index 0000000..ea184ef --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildChannelsPositionUpdate.js @@ -0,0 +1,19 @@ +const Action = require('./Action'); + +class GuildChannelsPositionUpdate extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + for (const partialChannel of data.channels) { + const channel = guild.channels.get(partialChannel.id); + if (channel) channel.position = partialChannel.position; + } + } + + return { guild }; + } +} + +module.exports = GuildChannelsPositionUpdate; diff --git a/node_modules/discord.js/src/client/actions/GuildDelete.js b/node_modules/discord.js/src/client/actions/GuildDelete.js new file mode 100644 index 0000000..8222323 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildDelete.js @@ -0,0 +1,57 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class GuildDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + + let guild = client.guilds.get(data.id); + if (guild) { + for (const channel of guild.channels.values()) { + if (channel.type === 'text') channel.stopTyping(true); + } + + if (guild.available && data.unavailable) { + // Guild is unavailable + guild.available = false; + client.emit(Constants.Events.GUILD_UNAVAILABLE, guild); + + // Stops the GuildDelete packet thinking a guild was actually deleted, + // handles emitting of event itself + return { + guild: null, + }; + } + + for (const channel of guild.channels.values()) this.client.channels.delete(channel.id); + if (guild.voiceConnection) guild.voiceConnection.disconnect(); + + // Delete guild + client.guilds.delete(guild.id); + this.deleted.set(guild.id, guild); + this.scheduleForDeletion(guild.id); + } else { + guild = this.deleted.get(data.id) || null; + } + if (guild) guild.deleted = true; + + return { guild }; + } + + scheduleForDeletion(id) { + this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout); + } +} + +/** + * Emitted whenever a guild becomes unavailable, likely due to a server outage. + * @event Client#guildUnavailable + * @param {Guild} guild The guild that has become unavailable + */ + +module.exports = GuildDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js b/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js new file mode 100644 index 0000000..79f55ba --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiCreate.js @@ -0,0 +1,17 @@ +const Action = require('./Action'); + +class GuildEmojiCreateAction extends Action { + handle(guild, createdEmoji) { + const client = this.client; + const emoji = client.dataManager.newEmoji(createdEmoji, guild); + return { emoji }; + } +} + +/** + * Emitted whenever a custom emoji is created in a guild. + * @event Client#emojiCreate + * @param {Emoji} emoji The emoji that was created + */ + +module.exports = GuildEmojiCreateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojiDelete.js b/node_modules/discord.js/src/client/actions/GuildEmojiDelete.js new file mode 100644 index 0000000..044679c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiDelete.js @@ -0,0 +1,18 @@ +const Action = require('./Action'); + +class GuildEmojiDeleteAction extends Action { + handle(emoji) { + const client = this.client; + client.dataManager.killEmoji(emoji); + emoji.deleted = true; + return { emoji }; + } +} + +/** + * Emitted whenever a custom guild emoji is deleted. + * @event Client#emojiDelete + * @param {Emoji} emoji The emoji that was deleted + */ + +module.exports = GuildEmojiDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js b/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js new file mode 100644 index 0000000..ff6ae65 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojiUpdate.js @@ -0,0 +1,17 @@ +const Action = require('./Action'); + +class GuildEmojiUpdateAction extends Action { + handle(oldEmoji, newEmoji) { + const emoji = this.client.dataManager.updateEmoji(oldEmoji, newEmoji); + return { emoji }; + } +} + +/** + * Emitted whenever a custom guild emoji is updated. + * @event Client#emojiUpdate + * @param {Emoji} oldEmoji The old emoji + * @param {Emoji} newEmoji The new emoji + */ + +module.exports = GuildEmojiUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildEmojisUpdate.js b/node_modules/discord.js/src/client/actions/GuildEmojisUpdate.js new file mode 100644 index 0000000..8656a34 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildEmojisUpdate.js @@ -0,0 +1,38 @@ +const Action = require('./Action'); + +function mappify(iterable) { + const map = new Map(); + for (const x of iterable) map.set(...x); + return map; +} + +class GuildEmojisUpdateAction extends Action { + handle(data) { + const guild = this.client.guilds.get(data.guild_id); + if (!guild || !guild.emojis) return; + + const deletions = mappify(guild.emojis.entries()); + + for (const emoji of data.emojis) { + // Determine type of emoji event + const cachedEmoji = guild.emojis.get(emoji.id); + if (cachedEmoji) { + deletions.delete(emoji.id); + if (!cachedEmoji.equals(emoji, true)) { + // Emoji updated + this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji); + } + } else { + // Emoji added + this.client.actions.GuildEmojiCreate.handle(guild, emoji); + } + } + + for (const emoji of deletions.values()) { + // Emoji deleted + this.client.actions.GuildEmojiDelete.handle(emoji); + } + } +} + +module.exports = GuildEmojisUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildMemberGet.js b/node_modules/discord.js/src/client/actions/GuildMemberGet.js new file mode 100644 index 0000000..ecb7a8f --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildMemberGet.js @@ -0,0 +1,10 @@ +const Action = require('./Action'); + +class GuildMemberGetAction extends Action { + handle(guild, data) { + const member = guild._addMember(data, false); + return { member }; + } +} + +module.exports = GuildMemberGetAction; diff --git a/node_modules/discord.js/src/client/actions/GuildMemberRemove.js b/node_modules/discord.js/src/client/actions/GuildMemberRemove.js new file mode 100644 index 0000000..6682b63 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildMemberRemove.js @@ -0,0 +1,41 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class GuildMemberRemoveAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + let member = null; + if (guild) { + member = guild.members.get(data.user.id); + guild.memberCount--; + if (member) { + guild._removeMember(member); + this.deleted.set(guild.id + data.user.id, member); + if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, member); + this.scheduleForDeletion(guild.id, data.user.id); + } else { + member = this.deleted.get(guild.id + data.user.id) || null; + } + if (member) member.deleted = true; + } + return { guild, member }; + } + + scheduleForDeletion(guildID, userID) { + this.client.setTimeout(() => this.deleted.delete(guildID + userID), this.client.options.restWsBridgeTimeout); + } +} + +/** + * Emitted whenever a member leaves a guild, or is kicked. + * @event Client#guildMemberRemove + * @param {GuildMember} member The member that has left/been kicked from the guild + */ + +module.exports = GuildMemberRemoveAction; diff --git a/node_modules/discord.js/src/client/actions/GuildRoleCreate.js b/node_modules/discord.js/src/client/actions/GuildRoleCreate.js new file mode 100644 index 0000000..b4f320f --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleCreate.js @@ -0,0 +1,26 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Role = require('../../structures/Role'); + +class GuildRoleCreate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + let role; + if (guild) { + const already = guild.roles.has(data.role.id); + role = new Role(guild, data.role); + guild.roles.set(role.id, role); + if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, role); + } + return { role }; + } +} + +/** + * Emitted whenever a role is created. + * @event Client#roleCreate + * @param {Role} role The role that was created + */ + +module.exports = GuildRoleCreate; diff --git a/node_modules/discord.js/src/client/actions/GuildRoleDelete.js b/node_modules/discord.js/src/client/actions/GuildRoleDelete.js new file mode 100644 index 0000000..deafc85 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleDelete.js @@ -0,0 +1,42 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class GuildRoleDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + let role; + + if (guild) { + role = guild.roles.get(data.role_id); + if (role) { + guild.roles.delete(data.role_id); + this.deleted.set(guild.id + data.role_id, role); + this.scheduleForDeletion(guild.id, data.role_id); + client.emit(Constants.Events.GUILD_ROLE_DELETE, role); + } else { + role = this.deleted.get(guild.id + data.role_id) || null; + } + if (role) role.deleted = true; + } + + return { role }; + } + + scheduleForDeletion(guildID, roleID) { + this.client.setTimeout(() => this.deleted.delete(guildID + roleID), this.client.options.restWsBridgeTimeout); + } +} + +/** + * Emitted whenever a guild role is deleted. + * @event Client#roleDelete + * @param {Role} role The role that was deleted + */ + +module.exports = GuildRoleDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/GuildRoleUpdate.js b/node_modules/discord.js/src/client/actions/GuildRoleUpdate.js new file mode 100644 index 0000000..7d127ab --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRoleUpdate.js @@ -0,0 +1,41 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Util = require('../../util/Util'); + +class GuildRoleUpdateAction extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + + if (guild) { + const roleData = data.role; + let oldRole = null; + + const role = guild.roles.get(roleData.id); + if (role) { + oldRole = Util.cloneObject(role); + role.setup(data.role); + client.emit(Constants.Events.GUILD_ROLE_UPDATE, oldRole, role); + } + + return { + old: oldRole, + updated: role, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * Emitted whenever a guild role is updated. + * @event Client#roleUpdate + * @param {Role} oldRole The role before the update + * @param {Role} newRole The role after the update + */ + +module.exports = GuildRoleUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js b/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js new file mode 100644 index 0000000..9094965 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildRolesPositionUpdate.js @@ -0,0 +1,19 @@ +const Action = require('./Action'); + +class GuildRolesPositionUpdate extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + for (const partialRole of data.roles) { + const role = guild.roles.get(partialRole.id); + if (role) role.position = partialRole.position; + } + } + + return { guild }; + } +} + +module.exports = GuildRolesPositionUpdate; diff --git a/node_modules/discord.js/src/client/actions/GuildSync.js b/node_modules/discord.js/src/client/actions/GuildSync.js new file mode 100644 index 0000000..3d3a47b --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildSync.js @@ -0,0 +1,29 @@ +const Action = require('./Action'); + +class GuildSync extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.id); + if (guild) { + if (data.presences) { + for (const presence of data.presences) guild._setPresence(presence.user.id, presence); + } + + if (data.members) { + for (const syncMember of data.members) { + const member = guild.members.get(syncMember.user.id); + if (member) { + guild._updateMember(member, syncMember); + } else { + guild._addMember(syncMember, false); + } + } + } + + if ('large' in data) guild.large = data.large; + } + } +} + +module.exports = GuildSync; diff --git a/node_modules/discord.js/src/client/actions/GuildUpdate.js b/node_modules/discord.js/src/client/actions/GuildUpdate.js new file mode 100644 index 0000000..6806d1c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/GuildUpdate.js @@ -0,0 +1,34 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Util = require('../../util/Util'); + +class GuildUpdateAction extends Action { + handle(data) { + const client = this.client; + + const guild = client.guilds.get(data.id); + if (guild) { + const oldGuild = Util.cloneObject(guild); + guild.setup(data); + client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild); + return { + old: oldGuild, + updated: guild, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * Emitted whenever a guild is updated - e.g. name change. + * @event Client#guildUpdate + * @param {Guild} oldGuild The guild before the update + * @param {Guild} newGuild The guild after the update + */ + +module.exports = GuildUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/InviteCreate.js b/node_modules/discord.js/src/client/actions/InviteCreate.js new file mode 100644 index 0000000..36e8cf1 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/InviteCreate.js @@ -0,0 +1,29 @@ +'use strict'; + +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteCreateAction extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + const channel = client.channels.get(data.channel_id); + if (guild && channel) { + const inviteData = Object.assign(data, { guild, channel }); + const invite = new Invite(client, inviteData); + /** + * Emitted when an invite is created. + * <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel.</info> + * @event Client#inviteCreate + * @param {Invite} invite The invite that was created + */ + client.emit(Events.INVITE_CREATE, invite); + return { invite }; + } + return { invite: null }; + } +} + +module.exports = InviteCreateAction; diff --git a/node_modules/discord.js/src/client/actions/InviteDelete.js b/node_modules/discord.js/src/client/actions/InviteDelete.js new file mode 100644 index 0000000..a25eb88 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/InviteDelete.js @@ -0,0 +1,25 @@ +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteDeleteAction extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + const channel = client.channels.get(data.channel_id); + if (guild && channel) { + const inviteData = Object.assign(data, { guild, channel }); + const invite = new Invite(client, inviteData); + /** + * Emitted when an invite is deleted. + * <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel.</info> + * @event Client#inviteDelete + * @param {Invite} invite The invite that was deleted + */ + client.emit(Events.INVITE_DELETE, invite); + } + } +} + +module.exports = InviteDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/MessageCreate.js b/node_modules/discord.js/src/client/actions/MessageCreate.js new file mode 100644 index 0000000..532f80c --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageCreate.js @@ -0,0 +1,53 @@ +const Action = require('./Action'); +const Message = require('../../structures/Message'); + +class MessageCreateAction extends Action { + handle(data) { + const client = this.client; + + const channel = client.channels.get((data instanceof Array ? data[0] : data).channel_id); + const user = client.users.get((data instanceof Array ? data[0] : data).author.id); + if (channel) { + const member = channel.guild ? channel.guild.member(user) : null; + if (data instanceof Array) { + const messages = new Array(data.length); + for (let i = 0; i < data.length; i++) { + messages[i] = channel._cacheMessage(new Message(channel, data[i], client)); + } + const lastMessage = messages[messages.length - 1]; + channel.lastMessageID = lastMessage.id; + if (user) { + user.lastMessageID = lastMessage.id; + user.lastMessage = lastMessage; + } + if (member) { + member.lastMessageID = lastMessage.id; + member.lastMessage = lastMessage; + } + return { + messages, + }; + } else { + const message = channel._cacheMessage(new Message(channel, data, client)); + channel.lastMessageID = data.id; + if (user) { + user.lastMessageID = data.id; + user.lastMessage = message; + } + if (member) { + member.lastMessageID = data.id; + member.lastMessage = message; + } + return { + message, + }; + } + } + + return { + message: null, + }; + } +} + +module.exports = MessageCreateAction; diff --git a/node_modules/discord.js/src/client/actions/MessageDelete.js b/node_modules/discord.js/src/client/actions/MessageDelete.js new file mode 100644 index 0000000..9dc0828 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDelete.js @@ -0,0 +1,35 @@ +const Action = require('./Action'); + +class MessageDeleteAction extends Action { + constructor(client) { + super(client); + this.deleted = new Map(); + } + + handle(data) { + const client = this.client; + const channel = client.channels.get(data.channel_id); + let message; + + if (channel) { + message = channel.messages.get(data.id); + if (message) { + channel.messages.delete(message.id); + this.deleted.set(channel.id + message.id, message); + this.scheduleForDeletion(channel.id, message.id); + } else { + message = this.deleted.get(channel.id + data.id) || null; + } + if (message) message.deleted = true; + } + + return { message }; + } + + scheduleForDeletion(channelID, messageID) { + this.client.setTimeout(() => this.deleted.delete(channelID + messageID), + this.client.options.restWsBridgeTimeout); + } +} + +module.exports = MessageDeleteAction; diff --git a/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js b/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js new file mode 100644 index 0000000..7ee2455 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageDeleteBulk.js @@ -0,0 +1,26 @@ +const Action = require('./Action'); +const Collection = require('../../util/Collection'); +const Constants = require('../../util/Constants'); + +class MessageDeleteBulkAction extends Action { + handle(data) { + const messages = new Collection(); + const channel = this.client.channels.get(data.channel_id); + + if (channel) { + for (const id of data.ids) { + const message = channel.messages.get(id); + if (message) { + message.deleted = true; + messages.set(message.id, message); + channel.messages.delete(id); + } + } + } + + if (messages.size > 0) this.client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages); + return { messages }; + } +} + +module.exports = MessageDeleteBulkAction; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionAdd.js b/node_modules/discord.js/src/client/actions/MessageReactionAdd.js new file mode 100644 index 0000000..d962953 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionAdd.js @@ -0,0 +1,37 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +/* +{ user_id: 'id', + message_id: 'id', + emoji: { name: '�', id: null }, + channel_id: 'id' } } +*/ + +class MessageReactionAdd extends Action { + handle(data) { + const user = this.client.users.get(data.user_id); + if (!user) return false; + // Verify channel + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + // Verify message + const message = channel.messages.get(data.message_id); + if (!message) return false; + if (!data.emoji) return false; + // Verify reaction + const reaction = message._addReaction(data.emoji, user); + if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_ADD, reaction, user); + + return { message, reaction, user }; + } +} + +/** + * Emitted whenever a reaction is added to a cached message. + * @event Client#messageReactionAdd + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user that applied the emoji or reaction emoji + */ + +module.exports = MessageReactionAdd; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionRemove.js b/node_modules/discord.js/src/client/actions/MessageReactionRemove.js new file mode 100644 index 0000000..2403c3b --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemove.js @@ -0,0 +1,37 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +/* +{ user_id: 'id', + message_id: 'id', + emoji: { name: '�', id: null }, + channel_id: 'id' } } +*/ + +class MessageReactionRemove extends Action { + handle(data) { + const user = this.client.users.get(data.user_id); + if (!user) return false; + // Verify channel + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + // Verify message + const message = channel.messages.get(data.message_id); + if (!message) return false; + if (!data.emoji) return false; + // Verify reaction + const reaction = message._removeReaction(data.emoji, user); + if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE, reaction, user); + + return { message, reaction, user }; + } +} + +/** + * Emitted whenever a reaction is removed from a cached message. + * @event Client#messageReactionRemove + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user whose emoji or reaction emoji was removed + */ + +module.exports = MessageReactionRemove; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionRemoveAll.js b/node_modules/discord.js/src/client/actions/MessageReactionRemoveAll.js new file mode 100644 index 0000000..e365646 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemoveAll.js @@ -0,0 +1,25 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class MessageReactionRemoveAll extends Action { + handle(data) { + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + + const message = channel.messages.get(data.message_id); + if (!message) return false; + + message._clearReactions(); + this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_ALL, message); + + return { message }; + } +} + +/** + * Emitted whenever all reactions are removed from a cached message. + * @event Client#messageReactionRemoveAll + * @param {Message} message The message the reactions were removed from + */ + +module.exports = MessageReactionRemoveAll; diff --git a/node_modules/discord.js/src/client/actions/MessageReactionRemoveEmoji.js b/node_modules/discord.js/src/client/actions/MessageReactionRemoveEmoji.js new file mode 100644 index 0000000..12b4445 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageReactionRemoveEmoji.js @@ -0,0 +1,27 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class MessageReactionRemoveEmoji extends Action { + handle(data) { + // Verify channel + const channel = this.client.channels.get(data.channel_id); + if (!channel || channel.type === 'voice') return false; + // Verify message + const message = channel.messages.get(data.message_id); + if (!message) return false; + if (!data.emoji) return false; + // Verify reaction + const reaction = message._removeReaction(data.emoji); + if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction); + + return { message, reaction }; + } +} + +/** + * Emitted whenever a reaction emoji is removed from a cached message. + * @event Client#messageReactionRemoveEmoji + * @param {MessageReaction} messageReaction The reaction object + */ + +module.exports = MessageReactionRemoveEmoji; diff --git a/node_modules/discord.js/src/client/actions/MessageUpdate.js b/node_modules/discord.js/src/client/actions/MessageUpdate.js new file mode 100644 index 0000000..4e0392d --- /dev/null +++ b/node_modules/discord.js/src/client/actions/MessageUpdate.js @@ -0,0 +1,40 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class MessageUpdateAction extends Action { + handle(data) { + const client = this.client; + + const channel = client.channels.get(data.channel_id); + if (channel) { + const message = channel.messages.get(data.id); + if (message) { + message.patch(data); + client.emit(Constants.Events.MESSAGE_UPDATE, message._edits[0], message); + return { + old: message._edits[0], + updated: message, + }; + } + + return { + old: message, + updated: message, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +/** + * Emitted whenever a message is updated - e.g. embed or content change. + * @event Client#messageUpdate + * @param {Message} oldMessage The message before the update + * @param {Message} newMessage The message after the update + */ + +module.exports = MessageUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/UserGet.js b/node_modules/discord.js/src/client/actions/UserGet.js new file mode 100644 index 0000000..0ee85ae --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserGet.js @@ -0,0 +1,11 @@ +const Action = require('./Action'); + +class UserGetAction extends Action { + handle(data) { + const client = this.client; + const user = client.dataManager.newUser(data); + return { user }; + } +} + +module.exports = UserGetAction; diff --git a/node_modules/discord.js/src/client/actions/UserNoteUpdate.js b/node_modules/discord.js/src/client/actions/UserNoteUpdate.js new file mode 100644 index 0000000..4c2cc21 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserNoteUpdate.js @@ -0,0 +1,30 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); + +class UserNoteUpdateAction extends Action { + handle(data) { + const client = this.client; + + const oldNote = client.user.notes.get(data.id); + const note = data.note.length ? data.note : null; + + client.user.notes.set(data.id, note); + + client.emit(Constants.Events.USER_NOTE_UPDATE, data.id, oldNote, note); + + return { + old: oldNote, + updated: note, + }; + } +} + +/** + * Emitted whenever a note is updated. + * @event Client#userNoteUpdate + * @param {User} user The user the note belongs to + * @param {string} oldNote The note content before the update + * @param {string} newNote The note content after the update + */ + +module.exports = UserNoteUpdateAction; diff --git a/node_modules/discord.js/src/client/actions/UserUpdate.js b/node_modules/discord.js/src/client/actions/UserUpdate.js new file mode 100644 index 0000000..6b917d1 --- /dev/null +++ b/node_modules/discord.js/src/client/actions/UserUpdate.js @@ -0,0 +1,33 @@ +const Action = require('./Action'); +const Constants = require('../../util/Constants'); +const Util = require('../../util/Util'); + +class UserUpdateAction extends Action { + handle(data) { + const client = this.client; + + if (client.user) { + if (client.user.equals(data)) { + return { + old: client.user, + updated: client.user, + }; + } + + const oldUser = Util.cloneObject(client.user); + client.user.patch(data); + client.emit(Constants.Events.USER_UPDATE, oldUser, client.user); + return { + old: oldUser, + updated: client.user, + }; + } + + return { + old: null, + updated: null, + }; + } +} + +module.exports = UserUpdateAction; diff --git a/node_modules/discord.js/src/client/rest/APIRequest.js b/node_modules/discord.js/src/client/rest/APIRequest.js new file mode 100644 index 0000000..3492fa7 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/APIRequest.js @@ -0,0 +1,52 @@ +const snekfetch = require('snekfetch'); +const Constants = require('../../util/Constants'); + +class APIRequest { + constructor(rest, method, path, auth, data, files, reason) { + this.rest = rest; + this.client = rest.client; + this.method = method; + this.path = path.toString(); + this.auth = auth; + this.data = data; + this.files = files; + this.route = this.getRoute(this.path); + this.reason = reason; + } + + getRoute(url) { + let route = url.split('?')[0]; + if (route.includes('/channels/') || route.includes('/guilds/')) { + const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/'); + const majorID = route.substring(startInd).split('/')[2]; + route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID); + } + return route; + } + + getAuth() { + if (this.client.token && this.client.user && this.client.user.bot) { + return `Bot ${this.client.token}`; + } else if (this.client.token) { + return this.client.token; + } + throw new Error(Constants.Errors.NO_TOKEN); + } + + gen() { + const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`; + const request = snekfetch[this.method](`${API}${this.path}`); + if (this.auth) request.set('Authorization', this.getAuth()); + if (this.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.reason)); + if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent); + if (this.files) { + for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name); + if (typeof this.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.data)); + } else if (this.data) { + request.send(this.data); + } + return request; + } +} + +module.exports = APIRequest; diff --git a/node_modules/discord.js/src/client/rest/DiscordAPIError.js b/node_modules/discord.js/src/client/rest/DiscordAPIError.js new file mode 100644 index 0000000..f5f8336 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/DiscordAPIError.js @@ -0,0 +1,60 @@ +/** + * Represents an error from the Discord API. + * @extends Error + */ +class DiscordAPIError extends Error { + constructor(path, error, method) { + super(); + const flattened = this.constructor.flattenErrors(error.errors || error).join('\n'); + this.name = 'DiscordAPIError'; + this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened; + + /** + * The path of the request relative to the HTTP endpoint + * @type {string} + */ + this.path = path; + + /** + * HTTP error code returned by Discord + * @type {number} + */ + this.code = error.code; + + /** + * The HTTP method used for the request + * @type {string} + */ + this.method = method; + } + + /** + * Flattens an errors object returned from the API into an array. + * @param {Object} obj Discord errors object + * @param {string} [key] Used internally to determine key names of nested fields + * @returns {string[]} + * @private + */ + static flattenErrors(obj, key = '') { + let messages = []; + + for (const k of Object.keys(obj)) { + if (k === 'message') continue; + const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k; + + if (obj[k]._errors) { + messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`); + } else if (obj[k].code || obj[k].message) { + messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}: ${obj[k].message}`.trim()); + } else if (typeof obj[k] === 'string') { + messages.push(obj[k]); + } else { + messages = messages.concat(this.flattenErrors(obj[k], newKey)); + } + } + + return messages; + } +} + +module.exports = DiscordAPIError; diff --git a/node_modules/discord.js/src/client/rest/RESTManager.js b/node_modules/discord.js/src/client/rest/RESTManager.js new file mode 100644 index 0000000..ee64712 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RESTManager.js @@ -0,0 +1,58 @@ +const UserAgentManager = require('./UserAgentManager'); +const RESTMethods = require('./RESTMethods'); +const SequentialRequestHandler = require('./RequestHandlers/Sequential'); +const BurstRequestHandler = require('./RequestHandlers/Burst'); +const APIRequest = require('./APIRequest'); +const Constants = require('../../util/Constants'); + +class RESTManager { + constructor(client) { + this.client = client; + this.handlers = {}; + this.userAgentManager = new UserAgentManager(this); + this.methods = new RESTMethods(this); + this.rateLimitedEndpoints = {}; + this.globallyRateLimited = false; + } + + destroy() { + for (const handlerKey of Object.keys(this.handlers)) { + const handler = this.handlers[handlerKey]; + if (handler.destroy) handler.destroy(); + } + } + + push(handler, apiRequest) { + return new Promise((resolve, reject) => { + handler.push({ + request: apiRequest, + resolve, + reject, + retries: 0, + }); + }); + } + + getRequestHandler() { + switch (this.client.options.apiRequestMethod) { + case 'sequential': + return SequentialRequestHandler; + case 'burst': + return BurstRequestHandler; + default: + throw new Error(Constants.Errors.INVALID_RATE_LIMIT_METHOD); + } + } + + makeRequest(method, url, auth, data, file, reason) { + const apiRequest = new APIRequest(this, method, url, auth, data, file, reason); + if (!this.handlers[apiRequest.route]) { + const RequestHandlerType = this.getRequestHandler(); + this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route); + } + + return this.push(this.handlers[apiRequest.route], apiRequest); + } +} + +module.exports = RESTManager; diff --git a/node_modules/discord.js/src/client/rest/RESTMethods.js b/node_modules/discord.js/src/client/rest/RESTMethods.js new file mode 100644 index 0000000..59f3c65 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RESTMethods.js @@ -0,0 +1,1067 @@ +const querystring = require('querystring'); +const long = require('long'); +const Permissions = require('../../util/Permissions'); +const Constants = require('../../util/Constants'); +const Endpoints = Constants.Endpoints; +const Collection = require('../../util/Collection'); +const Util = require('../../util/Util'); +const resolvePermissions = require('../../structures/shared/resolvePermissions'); + +const RichEmbed = require('../../structures/RichEmbed'); +const User = require('../../structures/User'); +const GuildMember = require('../../structures/GuildMember'); +const Message = require('../../structures/Message'); +const Role = require('../../structures/Role'); +const Invite = require('../../structures/Invite'); +const Webhook = require('../../structures/Webhook'); +const UserProfile = require('../../structures/UserProfile'); +const OAuth2Application = require('../../structures/OAuth2Application'); +const Channel = require('../../structures/Channel'); +const GroupDMChannel = require('../../structures/GroupDMChannel'); +const Guild = require('../../structures/Guild'); +const VoiceRegion = require('../../structures/VoiceRegion'); +const GuildAuditLogs = require('../../structures/GuildAuditLogs'); + +const MessageFlags = require('../../util/MessageFlags'); + +class RESTMethods { + constructor(restManager) { + this.rest = restManager; + this.client = restManager.client; + this._ackToken = null; + } + + login(token = this.client.token) { + return new Promise((resolve, reject) => { + if (!token || typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); + token = token.replace(/^Bot\s*/i, ''); + this.client.manager.connectToWebSocket(token, resolve, reject); + }).catch(e => { + this.client.destroy(); + return Promise.reject(e); + }); + } + + logout() { + return this.rest.makeRequest('post', Endpoints.logout, true, {}); + } + + getGateway(bot = false) { + return this.rest.makeRequest('get', bot ? Endpoints.gateway.bot : Endpoints.gateway, true); + } + + fetchVoiceRegions(guildID) { + let endpoint; + if (guildID) endpoint = Endpoints.Guild(guildID).voiceRegions; + else endpoint = Endpoints.voiceRegions; + return this.rest.makeRequest('get', endpoint, true).then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); + } + + fetchEmbed(guildID) { + return this.rest.makeRequest('get', Endpoints.Guild(guildID).embed, true).then(data => ({ + enabled: data.enabled, + channel: data.channel_id ? this.client.channels.get(data.channel_id) : null, + })); + } + + sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code, reply } = {}, files = null) { + return new Promise((resolve, reject) => { // eslint-disable-line complexity + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + // The nonce has to be a uint64 :< + if (typeof nonce !== 'undefined') { + nonce = parseInt(nonce); + if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.'); + } + + if (content) { + if (split && typeof split !== 'object') split = {}; + + // Wrap everything in a code block + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + if (split) { + split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; + split.append = '\n```'; + } + } + + // Add zero-width spaces to @everyone/@here + if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + // Add the reply prefix + if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + if (split) split.prepend = `${mention}, ${split.prepend || ''}`; + } + + // Split the content + if (split) content = Util.splitMessage(content, split); + } else if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + content = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + } + + const send = chan => { + if (content instanceof Array) { + const messages = []; + (function sendChunk(list, index) { + const options = index === list.length - 1 ? { tts, embed, files } : { tts }; + chan.send(list[index], options).then(message => { + messages.push(message); + if (index >= list.length - 1) return resolve(messages); + return sendChunk(list, ++index); + }).catch(reject); + }(content, 0)); + } else { + this.rest.makeRequest('post', Endpoints.Channel(chan).messages, true, { + content, tts, nonce, embed, + }, files).then(data => resolve(this.client.actions.MessageCreate.handle(data).message), reject); + } + }; + + if (channel instanceof User || channel instanceof GuildMember) this.createDM(channel).then(send, reject); + else send(channel); + }); + } + + updateMessage(message, content, { flags, embed, code, reply } = {}) { + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + if (typeof flags !== 'undefined') flags = MessageFlags.resolve(flags); + + // Wrap everything in a code block + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + } + + // Add the reply prefix + if (reply && message.channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + } + + if (embed instanceof RichEmbed) embed = embed.toJSON(); + + return this.rest.makeRequest('patch', Endpoints.Message(message), true, { + content, embed, flags, + }).then(data => this.client.actions.MessageUpdate.handle(data).updated); + } + + deleteMessage(message) { + return this.rest.makeRequest('delete', Endpoints.Message(message), true) + .then(() => + this.client.actions.MessageDelete.handle({ + id: message.id, + channel_id: message.channel.id, + }).message + ); + } + + ackMessage(message) { + return this.rest.makeRequest('post', Endpoints.Message(message).ack, true, { token: this._ackToken }).then(res => { + if (res.token) this._ackToken = res.token; + return message; + }); + } + + ackTextChannel(channel) { + return this.rest.makeRequest('post', Endpoints.Channel(channel).Message(channel.lastMessageID).ack, true, { + token: this._ackToken, + }).then(res => { + if (res.token) this._ackToken = res.token; + return channel; + }); + } + + ackGuild(guild) { + return this.rest.makeRequest('post', Endpoints.Guild(guild).ack, true).then(() => guild); + } + + bulkDeleteMessages(channel, messages) { + return this.rest.makeRequest('post', Endpoints.Channel(channel).messages.bulkDelete, true, { + messages: messages, + }).then(() => + this.client.actions.MessageDeleteBulk.handle({ + channel_id: channel.id, + ids: messages, + }).messages + ); + } + + search(target, options) { + if (typeof options === 'string') options = { content: options }; + if (options.before) { + if (!(options.before instanceof Date)) options.before = new Date(options.before); + options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString(); + } + if (options.after) { + if (!(options.after instanceof Date)) options.after = new Date(options.after); + options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString(); + } + if (options.during) { + if (!(options.during instanceof Date)) options.during = new Date(options.during); + const t = options.during.getTime() - 14200704e5; + options.minID = long.fromNumber(t).shiftLeft(22).toString(); + options.maxID = long.fromNumber(t + 86400000).shiftLeft(22).toString(); + } + if (options.channel) options.channel = this.client.resolver.resolveChannelID(options.channel); + if (options.author) options.author = this.client.resolver.resolveUserID(options.author); + if (options.mentions) options.mentions = this.client.resolver.resolveUserID(options.options.mentions); + options = { + content: options.content, + max_id: options.maxID, + min_id: options.minID, + has: options.has, + channel_id: options.channel, + author_id: options.author, + author_type: options.authorType, + context_size: options.contextSize, + sort_by: options.sortBy, + sort_order: options.sortOrder, + limit: options.limit, + offset: options.offset, + mentions: options.mentions, + mentions_everyone: options.mentionsEveryone, + link_hostname: options.linkHostname, + embed_provider: options.embedProvider, + embed_type: options.embedType, + attachment_filename: options.attachmentFilename, + attachment_extension: options.attachmentExtension, + include_nsfw: options.nsfw, + }; + + for (const key of Object.keys(options)) if (options[key] === undefined) delete options[key]; + const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + + let endpoint; + if (target instanceof Channel) { + endpoint = Endpoints.Channel(target).search; + } else if (target instanceof Guild) { + endpoint = Endpoints.Guild(target).search; + } else { + throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); + } + return this.rest.makeRequest('get', `${endpoint}?${queryString}`, true).then(body => { + const messages = body.messages.map(x => + x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)) + ); + return { + totalResults: body.total_results, + messages, + }; + }); + } + + createChannel(guild, name, options) { + const { + type, + topic, + nsfw, + bitrate, + userLimit, + parent, + permissionOverwrites, + position, + rateLimitPerUser, + reason, + } = options; + return this.rest.makeRequest('post', Endpoints.Guild(guild).channels, true, { + name, + topic, + type: type ? Constants.ChannelTypes[type.toUpperCase()] : Constants.ChannelTypes.TEXT, + nsfw, + bitrate, + user_limit: userLimit, + parent_id: parent instanceof Channel ? parent.id : parent, + permission_overwrites: resolvePermissions.call(this, permissionOverwrites, guild), + position, + rate_limit_per_user: rateLimitPerUser, + }, + undefined, + reason).then(data => this.client.actions.ChannelCreate.handle(data).channel); + } + + createDM(recipient) { + const dmChannel = this.getExistingDM(recipient); + if (dmChannel) return Promise.resolve(dmChannel); + return this.rest.makeRequest('post', Endpoints.User(this.client.user).channels, true, { + recipient_id: recipient.id, + }).then(data => this.client.actions.ChannelCreate.handle(data).channel); + } + + createGroupDM(options) { + const data = this.client.user.bot ? + { access_tokens: options.accessTokens, nicks: options.nicks } : + { recipients: options.recipients }; + return this.rest.makeRequest('post', Endpoints.User('@me').channels, true, data) + .then(res => new GroupDMChannel(this.client, res)); + } + + addUserToGroupDM(channel, options) { + const data = this.client.user.bot ? + { nick: options.nick, access_token: options.accessToken } : + { recipient: options.id }; + return this.rest.makeRequest('put', Endpoints.Channel(channel).Recipient(options.id), true, data) + .then(() => channel); + } + + removeUserFromGroupDM(channel, userId) { + return this.rest.makeRequest('delete', Endpoints.Channel(channel).Recipient(userId), true) + .then(() => channel); + } + + updateGroupDMChannel(channel, _data) { + const data = {}; + data.name = _data.name; + data.icon = _data.icon; + return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data).then(() => channel); + } + + getExistingDM(recipient) { + return this.client.channels.find(channel => + channel.recipient && channel.recipient.id === recipient.id + ); + } + + deleteChannel(channel, reason) { + if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel); + if (!channel) return Promise.reject(new Error('No channel to delete.')); + return this.rest.makeRequest('delete', Endpoints.Channel(channel), true, undefined, undefined, reason) + .then(data => { + data.id = channel.id; + return this.client.actions.ChannelDelete.handle(data).channel; + }); + } + + updateChannel(channel, _data, reason) { + const data = {}; + data.name = (_data.name || channel.name).trim(); + data.topic = typeof _data.topic === 'undefined' ? channel.topic : _data.topic; + data.nsfw = typeof _data.nsfw === 'undefined' ? channel.nsfw : _data.nsfw; + data.position = _data.position || channel.position; + data.bitrate = _data.bitrate || (channel.bitrate ? channel.bitrate * 1000 : undefined); + data.user_limit = typeof _data.userLimit !== 'undefined' ? _data.userLimit : channel.userLimit; + data.parent_id = _data.parent instanceof Channel ? _data.parent.id : _data.parent; + data.permission_overwrites = _data.permissionOverwrites ? + resolvePermissions.call(this, _data.permissionOverwrites, channel.guild) : undefined; + data.rate_limit_per_user = typeof _data.rateLimitPerUser !== 'undefined' ? + _data.rateLimitPerUser : channel.rateLimitPerUser; + return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data, undefined, reason).then(newData => + this.client.actions.ChannelUpdate.handle(newData).updated + ); + } + + leaveGuild(guild) { + if (guild.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); + return this.rest.makeRequest('delete', Endpoints.User('@me').Guild(guild.id), true).then(() => + this.client.actions.GuildDelete.handle({ id: guild.id }).guild + ); + } + + createGuild(options) { + options.icon = this.client.resolver.resolveBase64(options.icon) || null; + options.region = options.region || 'us-central'; + return new Promise((resolve, reject) => { + this.rest.makeRequest('post', Endpoints.guilds, true, options).then(data => { + if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id)); + + const handleGuild = guild => { + if (guild.id === data.id) { + this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); + this.client.clearTimeout(timeout); + resolve(guild); + } + }; + this.client.on(Constants.Events.GUILD_CREATE, handleGuild); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); + reject(new Error('Took too long to receive guild data.')); + }, 10000); + return undefined; + }, reject); + }); + } + + // Untested but probably will work + deleteGuild(guild) { + return this.rest.makeRequest('delete', Endpoints.Guild(guild), true).then(() => + this.client.actions.GuildDelete.handle({ id: guild.id }).guild + ); + } + + getUser(userID, cache) { + return this.rest.makeRequest('get', Endpoints.User(userID), true).then(data => { + if (cache) return this.client.actions.UserGet.handle(data).user; + else return new User(this.client, data); + }); + } + + updateCurrentUser(_data, password) { + const user = this.client.user; + const data = {}; + data.username = _data.username || user.username; + data.avatar = typeof _data.avatar === 'undefined' ? user.avatar : this.client.resolver.resolveBase64(_data.avatar); + if (!user.bot) { + data.email = _data.email || user.email; + data.password = password; + if (_data.new_password) data.new_password = _data.newPassword; + } + return this.rest.makeRequest('patch', Endpoints.User('@me'), true, data).then(newData => + this.client.actions.UserUpdate.handle(newData).updated + ); + } + + updateGuild(guild, data, reason) { + return this.rest.makeRequest('patch', Endpoints.Guild(guild), true, data, undefined, reason).then(newData => + this.client.actions.GuildUpdate.handle(newData).updated + ); + } + + kickGuildMember(guild, member, reason) { + return this.rest.makeRequest( + 'delete', Endpoints.Guild(guild).Member(member), true, + undefined, undefined, reason) + .then(() => member); + } + + createGuildRole(guild, data, reason) { + if (data.color) data.color = this.client.resolver.resolveColor(data.color); + if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + return this.rest.makeRequest('post', Endpoints.Guild(guild).roles, true, data, undefined, reason).then(r => { + const { role } = this.client.actions.GuildRoleCreate.handle({ + guild_id: guild.id, + role: r, + }); + if (data.position) return role.setPosition(data.position, reason); + return role; + }); + } + + deleteGuildRole(role, reason) { + return this.rest.makeRequest( + 'delete', Endpoints.Guild(role.guild).Role(role.id), true, + undefined, undefined, reason) + .then(() => + this.client.actions.GuildRoleDelete.handle({ + guild_id: role.guild.id, + role_id: role.id, + }).role + ); + } + + setChannelOverwrite(channel, payload) { + return this.rest.makeRequest('put', `${Endpoints.Channel(channel).permissions}/${payload.id}`, true, payload); + } + + deletePermissionOverwrites(overwrite, reason) { + return this.rest.makeRequest( + 'delete', `${Endpoints.Channel(overwrite.channel).permissions}/${overwrite.id}`, + true, undefined, undefined, reason + ).then(() => overwrite); + } + + getChannelMessages(channel, payload = {}) { + const params = []; + if (payload.limit) params.push(`limit=${payload.limit}`); + if (payload.around) params.push(`around=${payload.around}`); + else if (payload.before) params.push(`before=${payload.before}`); + else if (payload.after) params.push(`after=${payload.after}`); + + let endpoint = Endpoints.Channel(channel).messages; + if (params.length > 0) endpoint += `?${params.join('&')}`; + return this.rest.makeRequest('get', endpoint, true); + } + + getChannelMessage(channel, messageID) { + const msg = channel.messages.get(messageID); + if (msg) return Promise.resolve(msg); + return this.rest.makeRequest('get', Endpoints.Channel(channel).Message(messageID), true); + } + + putGuildMember(guild, userID, options) { + options.access_token = options.accessToken; + if (options.roles) { + const roles = options.roles; + if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { + options.roles = roles.map(role => role.id); + } + } + return this.rest.makeRequest('put', Endpoints.Guild(guild).Member(userID), true, options) + .then(data => this.client.actions.GuildMemberGet.handle(guild, data).member); + } + + getGuild(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild), true); + } + + getGuildMember(guild, userID, cache) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).Member(userID), true).then(data => { + if (cache) return this.client.actions.GuildMemberGet.handle(guild, data).member; + else return new GuildMember(guild, data); + }); + } + + updateGuildMember(member, data, reason) { + if (data.channel) { + const channel = this.client.resolver.resolveChannel(data.channel); + if (!channel || channel.guild.id !== member.guild.id || channel.type !== 'voice') { + return Promise.reject(new Error('Could not resolve channel to a guild voice channel.')); + } + data.channel_id = channel.id; + data.channel = undefined; + } else if (data.channel === null) { + data.channel_id = null; + data.channel = undefined; + } + if (data.roles) data.roles = [...new Set(data.roles.map(role => role instanceof Role ? role.id : role))]; + + let endpoint = Endpoints.Member(member); + // Fix your endpoints, discord ;-; + if (member.id === this.client.user.id) { + const keys = Object.keys(data); + if (keys.length === 1 && keys[0] === 'nick') { + endpoint = Endpoints.Member(member).nickname; + } + } + + return this.rest.makeRequest('patch', endpoint, true, data, undefined, reason).then(newData => + member.guild._updateMember(member, newData).mem + ); + } + + addMemberRole(member, role, reason) { + return new Promise((resolve, reject) => { + if (member._roles.includes(role.id)) return resolve(member); + + const listener = (oldMember, newMember) => { + if (newMember.id === member.id && !oldMember._roles.includes(role.id) && newMember._roles.includes(role.id)) { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + resolve(newMember); + } + }; + + this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + reject(new Error('Adding the role timed out.')); + }, 10e3); + + return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason) + .catch(err => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + this.client.clearTimeout(timeout); + reject(err); + }); + }); + } + + removeMemberRole(member, role, reason) { + return new Promise((resolve, reject) => { + if (!member._roles.includes(role.id)) return resolve(member); + + const listener = (oldMember, newMember) => { + if (newMember.id === member.id && oldMember._roles.includes(role.id) && !newMember._roles.includes(role.id)) { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + resolve(newMember); + } + }; + + this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + reject(new Error('Removing the role timed out.')); + }, 10e3); + + return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason) + .catch(err => { + this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); + this.client.clearTimeout(timeout); + reject(err); + }); + }); + } + + sendTyping(channelID) { + return this.rest.makeRequest('post', Endpoints.Channel(channelID).typing, true); + } + + banGuildMember(guild, member, options) { + const id = this.client.resolver.resolveUserID(member); + if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); + + const url = `${Endpoints.Guild(guild).bans}/${id}?${querystring.stringify(options)}`; + return this.rest.makeRequest('put', url, true).then(() => { + if (member instanceof GuildMember) return member; + const user = this.client.resolver.resolveUser(id); + if (user) { + member = this.client.resolver.resolveGuildMember(guild, user); + return member || user; + } + return id; + }); + } + + unbanGuildMember(guild, member, reason) { + return new Promise((resolve, reject) => { + const id = this.client.resolver.resolveUserID(member); + if (!id) throw new Error('Couldn\'t resolve the user ID to unban.'); + + const listener = (eGuild, eUser) => { + if (eGuild.id === guild.id && eUser.id === id) { + this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); + this.client.clearTimeout(timeout); + resolve(eUser); + } + }; + this.client.on(Constants.Events.GUILD_BAN_REMOVE, listener); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); + reject(new Error('Took too long to receive the ban remove event.')); + }, 10000); + + this.rest.makeRequest('delete', `${Endpoints.Guild(guild).bans}/${id}`, true, undefined, undefined, reason) + .catch(err => { + this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); + this.client.clearTimeout(timeout); + reject(err); + }); + }); + } + + getGuildBan(guild, user) { + const id = this.client.resolver.resolveUserID(user); + return this.rest.makeRequest('get', `${Endpoints.Guild(guild).bans}/${id}`, true).then(ban => ({ + reason: ban.reason, + user: this.client.dataManager.newUser(ban.user), + })); + } + + getGuildBans(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(bans => + bans.reduce((collection, ban) => { + collection.set(ban.user.id, { + reason: ban.reason, + user: this.client.dataManager.newUser(ban.user), + }); + return collection; + }, new Collection()) + ); + } + + updateGuildRole(role, _data, reason) { + const data = {}; + data.name = _data.name || role.name; + data.position = typeof _data.position !== 'undefined' ? _data.position : role.position; + data.color = _data.color === null ? null : this.client.resolver.resolveColor(_data.color || role.color); + data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist; + data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable; + + if (typeof _data.permissions !== 'undefined') data.permissions = Permissions.resolve(_data.permissions); + else data.permissions = role.permissions; + + return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data, undefined, reason) + .then(_role => + this.client.actions.GuildRoleUpdate.handle({ + role: _role, + guild_id: role.guild.id, + }).updated + ); + } + + pinMessage(message) { + return this.rest.makeRequest('put', Endpoints.Channel(message.channel).Pin(message.id), true) + .then(() => message); + } + + unpinMessage(message) { + return this.rest.makeRequest('delete', Endpoints.Channel(message.channel).Pin(message.id), true) + .then(() => message); + } + + getChannelPinnedMessages(channel) { + return this.rest.makeRequest('get', Endpoints.Channel(channel).pins, true); + } + + createChannelInvite(channel, options, reason) { + const payload = {}; + payload.temporary = options.temporary; + payload.max_age = options.maxAge; + payload.max_uses = options.maxUses; + payload.unique = options.unique; + return this.rest.makeRequest('post', Endpoints.Channel(channel).invites, true, payload, undefined, reason) + .then(invite => new Invite(this.client, invite)); + } + + deleteInvite(invite, reason) { + return this.rest.makeRequest('delete', Endpoints.Invite(invite.code), true, undefined, undefined, reason) + .then(() => invite); + } + + getInvite(code) { + return this.rest.makeRequest('get', Endpoints.Invite(code), true).then(invite => + new Invite(this.client, invite) + ); + } + + getGuildInvites(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).invites, true).then(inviteItems => { + const invites = new Collection(); + for (const inviteItem of inviteItems) { + const invite = new Invite(this.client, inviteItem); + invites.set(invite.code, invite); + } + return invites; + }); + } + + getGuildVanityCode(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).vanityURL, true) + .then(res => res.code); + } + + pruneGuildMembers(guild, days, dry, reason) { + return this.rest.makeRequest(dry ? + 'get' : + 'post', + `${Endpoints.Guild(guild).prune}?days=${days}`, true, undefined, undefined, reason) + .then(data => data.pruned); + } + + createEmoji(guild, image, name, roles, reason) { + const data = { image, name }; + if (roles) data.roles = roles.map(r => r.id ? r.id : r); + return this.rest.makeRequest('post', Endpoints.Guild(guild).emojis, true, data, undefined, reason) + .then(emoji => this.client.actions.GuildEmojiCreate.handle(guild, emoji).emoji); + } + + updateEmoji(emoji, _data, reason) { + const data = {}; + if (_data.name) data.name = _data.name; + if (_data.roles) data.roles = _data.roles.map(r => r.id ? r.id : r); + return this.rest.makeRequest('patch', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, data, undefined, reason) + .then(newEmoji => this.client.actions.GuildEmojiUpdate.handle(emoji, newEmoji).emoji); + } + + deleteEmoji(emoji, reason) { + return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, undefined, reason) + .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).emoji); + } + + getGuildAuditLogs(guild, options = {}) { + if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; + if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; + if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; + + const queryString = (querystring.stringify({ + before: options.before, + after: options.after, + limit: options.limit, + user_id: this.client.resolver.resolveUserID(options.user), + action_type: options.type, + }).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + + return this.rest.makeRequest('get', `${Endpoints.Guild(guild).auditLogs}?${queryString}`, true) + .then(data => GuildAuditLogs.build(guild, data)); + } + + getWebhook(id, token) { + return this.rest.makeRequest('get', Endpoints.Webhook(id, token), !token).then(data => + new Webhook(this.client, data) + ); + } + + getGuildWebhooks(guild) { + return this.rest.makeRequest('get', Endpoints.Guild(guild).webhooks, true).then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); + } + + getChannelWebhooks(channel) { + return this.rest.makeRequest('get', Endpoints.Channel(channel).webhooks, true).then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); + } + + createWebhook(channel, name, avatar, reason) { + return this.rest.makeRequest('post', Endpoints.Channel(channel).webhooks, true, { name, avatar }, undefined, reason) + .then(data => new Webhook(this.client, data)); + } + + editWebhook(webhook, options, reason) { + let endpoint; + let auth; + + // Changing the channel of a webhook or specifying a reason requires a bot token + if (options.channel_id || reason) { + endpoint = Endpoints.Webhook(webhook.id); + auth = true; + } else { + endpoint = Endpoints.Webhook(webhook.id, webhook.token); + auth = false; + } + + return this.rest.makeRequest('patch', endpoint, auth, options, undefined, reason).then(data => { + webhook.name = data.name; + webhook.avatar = data.avatar; + webhook.channelID = data.channel_id; + return webhook; + }); + } + + deleteWebhook(webhook, reason) { + return this.rest.makeRequest( + 'delete', Endpoints.Webhook(webhook.id, webhook.token), + false, undefined, undefined, reason); + } + + sendWebhookMessage(webhook, content, { avatarURL, tts, embeds, username } = {}, files = null) { + return new Promise((resolve, reject) => { + username = username || webhook.name; + + if (content instanceof Array) { + const messages = []; + (function sendChunk(list, index) { + const options = index === list.length - 1 ? { tts, embeds, files } : { tts }; + webhook.send(list[index], options).then(message => { + messages.push(message); + if (index >= list.length - 1) return resolve(messages); + return sendChunk(list, ++index); + }).catch(reject); + }(content, 0)); + } else { + this.rest.makeRequest('post', `${Endpoints.Webhook(webhook.id, webhook.token)}?wait=true`, false, { + username, + avatar_url: avatarURL, + content, + tts, + embeds, + }, files).then(data => { + if (!this.client.channels) resolve(data); + else resolve(this.client.actions.MessageCreate.handle(data).message); + }, reject); + } + }); + } + + sendSlackWebhookMessage(webhook, body) { + return this.rest.makeRequest( + 'post', `${Endpoints.Webhook(webhook.id, webhook.token)}/slack?wait=true`, false, body + ); + } + + fetchUserProfile(user) { + return this.rest.makeRequest('get', Endpoints.User(user).profile, true).then(data => + new UserProfile(user, data) + ); + } + + fetchMentions(options) { + if (options.guild instanceof Guild) options.guild = options.guild.id; + Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); + + return this.rest.makeRequest( + 'get', Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild), true + ).then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); + } + + addFriend(user) { + return this.rest.makeRequest('post', Endpoints.User('@me'), true, { + username: user.username, + discriminator: user.discriminator, + }).then(() => user); + } + + removeFriend(user) { + return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) + .then(() => user); + } + + blockUser(user) { + return this.rest.makeRequest('put', Endpoints.User('@me').Relationship(user.id), true, { type: 2 }) + .then(() => user); + } + + unblockUser(user) { + return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) + .then(() => user); + } + + updateEmbed(guildID, embed, reason) { + return this.rest.makeRequest('patch', Endpoints.Guild(guildID).embed, true, { + enabled: embed.enabled, + channel_id: this.client.resolver.resolveChannelID(embed.channel), + }, undefined, reason); + } + + setRolePositions(guildID, roles) { + return this.rest.makeRequest('patch', Endpoints.Guild(guildID).roles, true, roles).then(() => + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: guildID, + roles, + }).guild + ); + } + + setChannelPositions(guildID, channels) { + return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, channels).then(() => + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: guildID, + channels, + }).guild + ); + } + + addMessageReaction(message, emoji) { + return this.rest.makeRequest( + 'put', Endpoints.Message(message).Reaction(emoji).User('@me'), true + ).then(() => + message._addReaction(Util.parseEmoji(emoji), message.client.user) + ); + } + + removeMessageReaction(message, emoji, userID) { + const endpoint = Endpoints.Message(message).Reaction(emoji).User(userID === this.client.user.id ? '@me' : userID); + return this.rest.makeRequest('delete', endpoint, true).then(() => + this.client.actions.MessageReactionRemove.handle({ + user_id: userID, + message_id: message.id, + emoji: Util.parseEmoji(emoji), + channel_id: message.channel.id, + }).reaction + ); + } + + removeMessageReactionEmoji(message, emoji) { + const endpoint = Endpoints.Message(message).Reaction(emoji); + return this.rest.makeRequest('delete', endpoint, true).then(() => + this.client.actions.MessageReactionRemoveEmoji.handle({ + message_id: message.id, + emoji: Util.parseEmoji(emoji), + channel_id: message.channel.id, + }).reaction + ); + } + + removeMessageReactions(message) { + return this.rest.makeRequest('delete', Endpoints.Message(message).reactions, true) + .then(() => message); + } + + getMessageReactionUsers(message, emoji, options) { + const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + + return this.rest.makeRequest('get', `${Endpoints.Message(message).Reaction(emoji)}?${queryString}`, true); + } + + getApplication(id) { + return this.rest.makeRequest('get', Endpoints.OAUTH2.Application(id), true).then(app => + new OAuth2Application(this.client, app) + ); + } + + resetApplication(id) { + return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetToken, true) + .then(() => this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetSecret, true)) + .then(app => new OAuth2Application(this.client, app)); + } + + setNote(user, note) { + return this.rest.makeRequest('put', Endpoints.User(user).note, true, { note }).then(() => user); + } + + acceptInvite(code) { + if (code.id) code = code.id; + return new Promise((resolve, reject) => + this.rest.makeRequest('post', Endpoints.Invite(code), true).then(res => { + const handler = guild => { + if (guild.id === res.id) { + resolve(guild); + this.client.removeListener(Constants.Events.GUILD_CREATE, handler); + } + }; + this.client.on(Constants.Events.GUILD_CREATE, handler); + this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_CREATE, handler); + reject(new Error('Accepting invite timed out')); + }, 120e3); + }) + ); + } + + patchUserSettings(data) { + return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data); + } + + patchClientUserGuildSettings(guildID, data) { + return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').Guild(guildID).settings, true, data); + } + + getIntegrations(guild) { + return this.rest.makeRequest( + 'get', + Constants.Endpoints.Guild(guild.id).integrations, + true + ); + } + + createIntegration(guild, data, reason) { + return this.rest.makeRequest( + 'post', + Constants.Endpoints.Guild(guild.id).integrations, + true, + data, + undefined, + reason + ); + } + + syncIntegration(integration) { + return this.rest.makeRequest( + 'post', + Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id), + true + ); + } + + editIntegration(integration, data, reason) { + return this.rest.makeRequest( + 'patch', + Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id), + true, + data, + undefined, + reason + ); + } + + deleteIntegration(integration, reason) { + return this.rest.makeRequest( + 'delete', + Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id), + true, + undefined, + undefined, + reason + ); + } +} + +module.exports = RESTMethods; diff --git a/node_modules/discord.js/src/client/rest/RequestHandlers/Burst.js b/node_modules/discord.js/src/client/rest/RequestHandlers/Burst.js new file mode 100644 index 0000000..eceee65 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/Burst.js @@ -0,0 +1,90 @@ +const RequestHandler = require('./RequestHandler'); +const DiscordAPIError = require('../DiscordAPIError'); +const { Events: { RATE_LIMIT } } = require('../../../util/Constants'); + +class BurstRequestHandler extends RequestHandler { + constructor(restManager, endpoint) { + super(restManager, endpoint); + + this.client = restManager.client; + + this.limit = Infinity; + this.resetTime = null; + this.remaining = 1; + this.timeDifference = 0; + + this.resetTimeout = null; + } + + push(request) { + super.push(request); + this.handle(); + } + + execute(item) { + if (!item) return; + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.limit = Number(res.headers['x-ratelimit-limit']); + this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.remaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + if (this.resetTimeout) return; + this.resetTimeout = this.client.setTimeout(() => { + this.remaining = this.limit; + this.globalLimit = false; + this.handle(); + this.resetTimeout = null; + }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else if (err.status >= 500 && err.status < 600) { + if (item.retries === this.client.options.retryLimit) { + item.reject(err); + this.handle(); + } else { + item.retries++; + this.queue.unshift(item); + this.resetTimeout = this.client.setTimeout(() => { + this.handle(); + this.resetTimeout = null; + }, 1e3 + this.client.options.restTimeOffset); + } + } else { + item.reject(err.status >= 400 && err.status < 500 ? + new DiscordAPIError(res.request.path, res.body, res.request.method) : err); + this.handle(); + } + } else { + if (this.remaining === 0) { + if (this.client.listenerCount(RATE_LIMIT)) { + this.client.emit(RATE_LIMIT, { + limit: this.limit, + timeDifference: this.timeDifference, + path: item.request.path, + method: item.request.method, + }); + } + } + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + this.handle(); + } + }); + } + + handle() { + super.handle(); + if (this.queue.length === 0) return; + if ((this.remaining <= 0 || this.globalLimit) && Date.now() - this.timeDifference < this.resetTime) return; + this.execute(this.queue.shift()); + this.remaining--; + this.handle(); + } +} + +module.exports = BurstRequestHandler; diff --git a/node_modules/discord.js/src/client/rest/RequestHandlers/RequestHandler.js b/node_modules/discord.js/src/client/rest/RequestHandlers/RequestHandler.js new file mode 100644 index 0000000..c5bad20 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/RequestHandler.js @@ -0,0 +1,54 @@ +/** + * A base class for different types of rate limiting handlers for the REST API. + * @private + */ +class RequestHandler { + /** + * @param {RESTManager} restManager The REST manager to use + */ + constructor(restManager) { + /** + * The RESTManager that instantiated this RequestHandler + * @type {RESTManager} + */ + this.restManager = restManager; + + /** + * A list of requests that have yet to be processed + * @type {APIRequest[]} + */ + this.queue = []; + } + + /** + * Whether or not the client is being rate limited on every endpoint + * @type {boolean} + * @readonly + */ + get globalLimit() { + return this.restManager.globallyRateLimited; + } + + set globalLimit(value) { + this.restManager.globallyRateLimited = value; + } + + /** + * Push a new API request into this bucket. + * @param {APIRequest} request The new request to push into the queue + */ + push(request) { + this.queue.push(request); + } + + /** + * Attempts to get this RequestHandler to process its current queue. + */ + handle() {} // eslint-disable-line no-empty-function + + destroy() { + this.queue = []; + } +} + +module.exports = RequestHandler; diff --git a/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js b/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js new file mode 100644 index 0000000..9d25bdb --- /dev/null +++ b/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js @@ -0,0 +1,132 @@ +const RequestHandler = require('./RequestHandler'); +const DiscordAPIError = require('../DiscordAPIError'); +const { Events: { RATE_LIMIT } } = require('../../../util/Constants'); + +/** + * Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto + * the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account, + * but it can be slower. + * @extends {RequestHandler} + * @private + */ +class SequentialRequestHandler extends RequestHandler { + /** + * @param {RESTManager} restManager The REST manager to use + * @param {string} endpoint The endpoint to handle + */ + constructor(restManager, endpoint) { + super(restManager, endpoint); + + /** + * The client that instantiated this handler + * @type {Client} + */ + this.client = restManager.client; + + /** + * The endpoint that this handler is handling + * @type {string} + */ + this.endpoint = endpoint; + + /** + * The time difference between Discord's Dates and the local computer's Dates. A positive number means the local + * computer's time is ahead of Discord's + * @type {number} + */ + this.timeDifference = 0; + + /** + * Whether the queue is being processed or not + * @type {boolean} + */ + this.busy = false; + } + + push(request) { + super.push(request); + this.handle(); + } + + /** + * Performs a request then resolves a promise to indicate its readiness for a new request. + * @param {APIRequest} item The item to execute + * @returns {Promise<?Object|Error>} + */ + execute(item) { + this.busy = true; + return new Promise(resolve => { + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.requestLimit = Number(res.headers['x-ratelimit-limit']); + this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.requestRemaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + this.client.setTimeout(() => { + this.globalLimit = false; + resolve(); + }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + } else if (err.status >= 500 && err.status < 600) { + if (item.retries === this.client.options.retryLimit) { + item.reject(err); + resolve(); + } else { + item.retries++; + this.queue.unshift(item); + this.client.setTimeout(resolve, 1e3 + this.client.options.restTimeOffset); + } + } else { + item.reject(err.status >= 400 && err.status < 500 ? + new DiscordAPIError(res.request.path, res.body, res.request.method) : err); + resolve(err); + } + } else { + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + if (this.requestRemaining === 0) { + if (this.client.listenerCount(RATE_LIMIT)) { + /** + * Emitted when the client hits a rate limit while making a request + * @event Client#rateLimit + * @param {Object} rateLimitInfo Object containing the rate limit info + * @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint + * @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers + * @param {string} rateLimitInfo.path Path used for request that triggered this event + * @param {string} rateLimitInfo.method HTTP method used for request that triggered this event + */ + this.client.emit(RATE_LIMIT, { + limit: this.requestLimit, + timeDifference: this.timeDifference, + path: item.request.path, + method: item.request.method, + }); + } + this.client.setTimeout( + () => resolve(data), + this.requestResetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset + ); + } else { + resolve(data); + } + } + }); + }); + } + + handle() { + super.handle(); + if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return; + this.execute(this.queue.shift()).then(() => { + this.busy = false; + this.handle(); + }); + } +} + +module.exports = SequentialRequestHandler; diff --git a/node_modules/discord.js/src/client/rest/UserAgentManager.js b/node_modules/discord.js/src/client/rest/UserAgentManager.js new file mode 100644 index 0000000..13e5528 --- /dev/null +++ b/node_modules/discord.js/src/client/rest/UserAgentManager.js @@ -0,0 +1,25 @@ +const Constants = require('../../util/Constants'); + +class UserAgentManager { + constructor() { + this.build(this.constructor.DEFAULT); + } + + set({ url, version } = {}) { + this.build({ + url: url || this.constructor.DFEAULT.url, + version: version || this.constructor.DEFAULT.version, + }); + } + + build(ua) { + this.userAgent = `DiscordBot (${ua.url}, ${ua.version}) Node.js/${process.version}`; + } +} + +UserAgentManager.DEFAULT = { + url: Constants.Package.homepage.split('#')[0], + version: Constants.Package.version, +}; + +module.exports = UserAgentManager; diff --git a/node_modules/discord.js/src/client/voice/ClientVoiceManager.js b/node_modules/discord.js/src/client/voice/ClientVoiceManager.js new file mode 100644 index 0000000..d8d3f9f --- /dev/null +++ b/node_modules/discord.js/src/client/voice/ClientVoiceManager.js @@ -0,0 +1,86 @@ +const Collection = require('../../util/Collection'); +const VoiceConnection = require('./VoiceConnection'); + +/** + * Manages all the voice stuff for the client. + * @private + */ +class ClientVoiceManager { + constructor(client) { + /** + * The client that instantiated this voice manager + * @type {Client} + */ + this.client = client; + + /** + * A collection mapping connection IDs to the Connection objects + * @type {Collection<Snowflake, VoiceConnection>} + */ + this.connections = new Collection(); + + this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); + this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); + } + + onVoiceServer({ guild_id, token, endpoint }) { + const connection = this.connections.get(guild_id); + if (connection) connection.setTokenAndEndpoint(token, endpoint); + } + + onVoiceStateUpdate({ guild_id, session_id, channel_id }) { + const connection = this.connections.get(guild_id); + if (!connection) return; + if (!channel_id) { + connection._disconnect(); + this.connections.delete(guild_id); + return; + } + + connection.channel = this.client.channels.get(channel_id); + connection.setSessionID(session_id); + } + + /** + * Sets up a request to join a voice channel. + * @param {VoiceChannel} channel The voice channel to join + * @returns {Promise<VoiceConnection>} + */ + joinChannel(channel) { + return new Promise((resolve, reject) => { + if (!channel.joinable) { + if (channel.full) { + throw new Error('You do not have permission to join this voice channel; it is full.'); + } else { + throw new Error('You do not have permission to join this voice channel.'); + } + } + + let connection = this.connections.get(channel.guild.id); + + if (connection) { + if (connection.channel.id !== channel.id) { + this.connections.get(channel.guild.id).updateChannel(channel); + } + resolve(connection); + return; + } else { + connection = new VoiceConnection(this, channel); + this.connections.set(channel.guild.id, connection); + } + + connection.once('failed', reason => { + this.connections.delete(channel.guild.id); + reject(reason); + }); + + connection.once('authenticated', () => { + connection.once('ready', () => resolve(connection)); + connection.once('error', reject); + connection.once('disconnect', () => this.connections.delete(channel.guild.id)); + }); + }); + } +} + +module.exports = ClientVoiceManager; diff --git a/node_modules/discord.js/src/client/voice/VoiceBroadcast.js b/node_modules/discord.js/src/client/voice/VoiceBroadcast.js new file mode 100644 index 0000000..3a78d87 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceBroadcast.js @@ -0,0 +1,366 @@ +const VolumeInterface = require('./util/VolumeInterface'); +const Prism = require('prism-media'); +const OpusEncoders = require('./opus/OpusEngineList'); +const Collection = require('../../util/Collection'); + +const ffmpegArguments = [ + '-analyzeduration', '0', + '-loglevel', '0', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', +]; + +/** + * A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency. + * + * Example usage: + * ```js + * const broadcast = client.createVoiceBroadcast(); + * broadcast.playFile('./music.mp3'); + * // Play "music.mp3" in all voice connections that the client is in + * for (const connection of client.voiceConnections.values()) { + * connection.playBroadcast(broadcast); + * } + * ``` + * @implements {VolumeInterface} + */ +class VoiceBroadcast extends VolumeInterface { + constructor(client) { + super(); + /** + * The client that created the broadcast + * @type {Client} + */ + this.client = client; + this._dispatchers = new Collection(); + this._encoders = new Collection(); + /** + * The audio transcoder that this broadcast uses + * @type {Prism} + */ + this.prism = new Prism(); + /** + * The current audio transcoder that is being used + * @type {Object} + */ + this.currentTranscoder = null; + this.tickInterval = null; + this._volume = 1; + } + + /** + * An array of subscribed dispatchers + * @type {StreamDispatcher[]} + * @readonly + */ + get dispatchers() { + let d = []; + for (const container of this._dispatchers.values()) { + d = d.concat(Array.from(container.values())); + } + return d; + } + + get _playableStream() { + const currentTranscoder = this.currentTranscoder; + if (!currentTranscoder) return null; + const transcoder = currentTranscoder.transcoder; + const options = currentTranscoder.options; + return (transcoder && transcoder.output) || options.stream; + } + + unregisterDispatcher(dispatcher, old) { + const volume = old || dispatcher.volume; + + /** + * Emitted whenever a stream dispatcher unsubscribes from the broadcast. + * @event VoiceBroadcast#unsubscribe + * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher + */ + this.emit('unsubscribe', dispatcher); + for (const container of this._dispatchers.values()) { + container.delete(dispatcher); + + if (!container.size) { + this._encoders.get(volume).destroy(); + this._dispatchers.delete(volume); + this._encoders.delete(volume); + } + } + } + + registerDispatcher(dispatcher) { + if (!this._dispatchers.has(dispatcher.volume)) { + this._dispatchers.set(dispatcher.volume, new Set()); + this._encoders.set(dispatcher.volume, OpusEncoders.fetch()); + } + const container = this._dispatchers.get(dispatcher.volume); + if (!container.has(dispatcher)) { + container.add(dispatcher); + dispatcher.once('end', () => this.unregisterDispatcher(dispatcher)); + dispatcher.on('volumeChange', (o, n) => { + this.unregisterDispatcher(dispatcher, o); + if (!this._dispatchers.has(n)) { + this._dispatchers.set(n, new Set()); + this._encoders.set(n, OpusEncoders.fetch()); + } + this._dispatchers.get(n).add(dispatcher); + }); + /** + * Emitted whenever a stream dispatcher subscribes to the broadcast. + * @event VoiceBroadcast#subscribe + * @param {StreamDispatcher} dispatcher The subscribed dispatcher + */ + this.emit('subscribe', dispatcher); + } + } + + killCurrentTranscoder() { + if (this.currentTranscoder) { + if (this.currentTranscoder.transcoder) this.currentTranscoder.transcoder.kill(); + this.currentTranscoder = null; + this.emit('end'); + } + } + + /** + * Plays any audio stream across the broadcast. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {VoiceBroadcast} + * @example + * // Play streams using ytdl-core + * const ytdl = require('ytdl-core'); + * const streamOptions = { seek: 0, volume: 1 }; + * const broadcast = client.createVoiceBroadcast(); + * + * voiceChannel.join() + * .then(connection => { + * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); + * broadcast.playStream(stream); + * const dispatcher = connection.playBroadcast(broadcast); + * }) + * .catch(console.error); + */ + playStream(stream, options = {}) { + this.setVolume(options.volume || 1); + return this._playTranscodable(stream, options); + } + + /** + * Play the given file in the voice connection. + * @param {string} file The absolute path to the file + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play files natively + * const broadcast = client.createVoiceBroadcast(); + * + * voiceChannel.join() + * .then(connection => { + * broadcast.playFile('C:/Users/Discord/Desktop/music.mp3'); + * const dispatcher = connection.playBroadcast(broadcast); + * }) + * .catch(console.error); + */ + playFile(file, options = {}) { + this.setVolume(options.volume || 1); + return this._playTranscodable(`file:${file}`, options); + } + + _playTranscodable(media, options) { + this.killCurrentTranscoder(); + const transcoder = this.prism.transcode({ + type: 'ffmpeg', + media, + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), + }); + /** + * Emitted whenever an error occurs. + * @event VoiceBroadcast#error + * @param {Error} error The error that occurred + */ + transcoder.once('error', e => { + if (this.listenerCount('error') > 0) this.emit('error', e); + /** + * Emitted whenever the VoiceBroadcast has any warnings. + * @event VoiceBroadcast#warn + * @param {string|Error} warning The warning that was raised + */ + else this.emit('warn', e); + }); + /** + * Emitted once the broadcast (the audio stream) ends. + * @event VoiceBroadcast#end + */ + transcoder.once('end', () => this.killCurrentTranscoder()); + this.currentTranscoder = { + transcoder, + options, + }; + transcoder.output.once('readable', () => this._startPlaying()); + return this; + } + + /** + * Plays a stream of 16-bit signed stereo PCM. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {VoiceBroadcast} + */ + playConvertedStream(stream, options = {}) { + this.killCurrentTranscoder(); + this.setVolume(options.volume || 1); + this.currentTranscoder = { options: { stream } }; + stream.once('readable', () => this._startPlaying()); + return this; + } + + /** + * Plays an Opus encoded stream. + * <warn>Note that inline volume is not compatible with this method.</warn> + * @param {ReadableStream} stream The Opus audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playOpusStream(stream) { + this.currentTranscoder = { options: { stream }, opus: true }; + stream.once('readable', () => this._startPlaying()); + return this; + } + + /** + * Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) + * @param {string} input The arbitrary input + * @param {StreamOptions} [options] Options for playing the stream + * @returns {VoiceBroadcast} + */ + playArbitraryInput(input, options = {}) { + this.setVolume(options.volume || 1); + options.input = input; + return this._playTranscodable(input, options); + } + + /** + * Pauses the entire broadcast - all dispatchers also pause. + */ + pause() { + this.paused = true; + for (const container of this._dispatchers.values()) { + for (const dispatcher of container.values()) { + dispatcher.pause(); + } + } + } + + /** + * Resumes the entire broadcast - all dispatchers also resume. + */ + resume() { + this.paused = false; + for (const container of this._dispatchers.values()) { + for (const dispatcher of container.values()) { + dispatcher.resume(); + } + } + } + + _startPlaying() { + if (this.tickInterval) clearInterval(this.tickInterval); + // Old code? + // this.tickInterval = this.client.setInterval(this.tick.bind(this), 20); + this._startTime = Date.now(); + this._count = 0; + this._pausedTime = 0; + this._missed = 0; + this.tick(); + } + + tick() { + if (!this._playableStream) return; + if (this.paused) { + this._pausedTime += 20; + setTimeout(() => this.tick(), 20); + return; + } + + const opus = this.currentTranscoder.opus; + const buffer = this.readStreamBuffer(); + + if (!buffer) { + this._missed++; + if (this._missed < 5) { + this._pausedTime += 200; + setTimeout(() => this.tick(), 200); + } else { + this.killCurrentTranscoder(); + } + return; + } + + this._missed = 0; + + let packetMatrix = {}; + + const getOpusPacket = volume => { + if (packetMatrix[volume]) return packetMatrix[volume]; + + const opusEncoder = this._encoders.get(volume); + const opusPacket = opusEncoder.encode(this.applyVolume(buffer, this._volume * volume)); + packetMatrix[volume] = opusPacket; + return opusPacket; + }; + + for (const dispatcher of this.dispatchers) { + if (opus) { + dispatcher.processPacket(buffer); + continue; + } + + const volume = dispatcher.volume; + dispatcher.processPacket(getOpusPacket(volume)); + } + + const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now()); + this._count++; + setTimeout(() => this.tick(), next); + } + + readStreamBuffer() { + const opus = this.currentTranscoder.opus; + const bufferLength = (opus ? 80 : 1920) * 2; + let buffer = this._playableStream.read(bufferLength); + if (opus) return buffer; + if (!buffer) return null; + + if (buffer.length !== bufferLength) { + const newBuffer = Buffer.alloc(bufferLength).fill(0); + buffer.copy(newBuffer); + buffer = newBuffer; + } + + return buffer; + } + + /** + * Stop the current stream from playing without unsubscribing dispatchers. + */ + end() { + this.killCurrentTranscoder(); + } + + /** + * End the current broadcast, all subscribed dispatchers will also end. + */ + destroy() { + this.end(); + for (const container of this._dispatchers.values()) { + for (const dispatcher of container.values()) { + dispatcher.destroy('end', 'broadcast ended'); + } + } + } +} + +module.exports = VoiceBroadcast; diff --git a/node_modules/discord.js/src/client/voice/VoiceConnection.js b/node_modules/discord.js/src/client/voice/VoiceConnection.js new file mode 100644 index 0000000..2fc6338 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceConnection.js @@ -0,0 +1,598 @@ +const VoiceWebSocket = require('./VoiceWebSocket'); +const VoiceUDP = require('./VoiceUDPClient'); +const Util = require('../../util/Util'); +const Constants = require('../../util/Constants'); +const AudioPlayer = require('./player/AudioPlayer'); +const VoiceReceiver = require('./receiver/VoiceReceiver'); +const SingleSilence = require('./util/SingleSilence'); +const EventEmitter = require('events').EventEmitter; +const Prism = require('prism-media'); + +// The delay between packets when a user is considered to have stopped speaking +// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200 +const DISCORD_SPEAKING_DELAY = 250; + +/** + * Represents a connection to a guild's voice server. + * ```js + * // Obtained using: + * voiceChannel.join() + * .then(connection => { + * + * }); + * ``` + * @extends {EventEmitter} + */ +class VoiceConnection extends EventEmitter { + constructor(voiceManager, channel) { + super(); + + /** + * The voice manager that instantiated this connection + * @type {ClientVoiceManager} + */ + this.voiceManager = voiceManager; + + /** + * The client that instantiated this connection + * @type {Client} + */ + this.client = voiceManager.client; + + /** + * @external Prism + * @see {@link https://github.com/hydrabolt/prism-media} + */ + + /** + * The audio transcoder for this connection + * @type {Prism} + */ + this.prism = new Prism(); + + /** + * The voice channel this connection is currently serving + * @type {VoiceChannel} + */ + this.channel = channel; + + /** + * The current status of the voice connection + * @type {VoiceStatus} + */ + this.status = Constants.VoiceStatus.AUTHENTICATING; + + /** + * Whether we're currently transmitting audio + * @type {boolean} + */ + this.speaking = false; + + /** + * An array of Voice Receivers that have been created for this connection + * @type {VoiceReceiver[]} + */ + this.receivers = []; + + /** + * The authentication data needed to connect to the voice server + * @type {Object} + * @private + */ + this.authentication = {}; + + /** + * The audio player for this voice connection + * @type {AudioPlayer} + */ + this.player = new AudioPlayer(this); + + this.player.on('debug', m => { + /** + * Debug info from the connection. + * @event VoiceConnection#debug + * @param {string} message The debug message + */ + this.emit('debug', `audio player - ${m}`); + }); + + this.player.on('error', e => { + /** + * Warning info from the connection. + * @event VoiceConnection#warn + * @param {string|Error} warning The warning + */ + this.emit('warn', e); + }); + + /** + * Map SSRC to user id + * @type {Map<number, Snowflake>} + * @private + */ + this.ssrcMap = new Map(); + + /** + * Map user id to speaking timeout + * @type {Map<Snowflake, Timeout>} + * @private + */ + this.speakingTimeouts = new Map(); + + /** + * Object that wraps contains the `ws` and `udp` sockets of this voice connection + * @type {Object} + * @private + */ + this.sockets = {}; + + this.authenticate(); + } + + /** + * The current stream dispatcher (if any) + * @type {?StreamDispatcher} + * @readonly + */ + get dispatcher() { + return this.player.dispatcher; + } + + /** + * Sets whether the voice connection should display as "speaking" or not. + * @param {boolean} value Whether or not to speak + * @private + */ + setSpeaking(value) { + if (this.speaking === value) return; + if (this.status !== Constants.VoiceStatus.CONNECTED) return; + this.speaking = value; + this.sockets.ws.sendPacket({ + op: Constants.VoiceOPCodes.SPEAKING, + d: { + speaking: true, + delay: 0, + }, + }).catch(e => { + this.emit('debug', e); + }); + } + + /** + * Sends a request to the main gateway to join a voice channel. + * @param {Object} [options] The options to provide + */ + sendVoiceStateUpdate(options = {}) { + options = Util.mergeDefault({ + guild_id: this.channel.guild.id, + channel_id: this.channel.id, + self_mute: false, + self_deaf: false, + }, options); + + this.client.ws.send({ + op: Constants.OPCodes.VOICE_STATE_UPDATE, + d: options, + }); + } + + /** + * Set the token and endpoint required to connect to the voice servers. + * @param {string} token The voice token + * @param {string} endpoint The voice endpoint + * @returns {void} + */ + setTokenAndEndpoint(token, endpoint) { + if (!endpoint) { + // Signifies awaiting endpoint stage + return; + } + + if (!token) { + this.authenticateFailed('Token not provided from voice server packet.'); + return; + } + + endpoint = endpoint.match(/([^:]*)/)[0]; + + if (!endpoint) { + this.authenticateFailed('Invalid endpoint received.'); + return; + } + + if (this.status === Constants.VoiceStatus.AUTHENTICATING) { + this.authentication.token = token; + this.authentication.endpoint = endpoint; + this.checkAuthenticated(); + } else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) { + this.reconnect(token, endpoint); + } + } + + /** + * Sets the Session ID for the connection. + * @param {string} sessionID The voice session ID + */ + setSessionID(sessionID) { + if (!sessionID) { + this.authenticateFailed('Session ID not supplied.'); + return; + } + + if (this.status === Constants.VoiceStatus.AUTHENTICATING) { + this.authentication.sessionID = sessionID; + this.checkAuthenticated(); + } else if (sessionID !== this.authentication.sessionID) { + this.authentication.sessionID = sessionID; + /** + * Emitted when a new session ID is received. + * @event VoiceConnection#newSession + * @private + */ + this.emit('newSession', sessionID); + } + } + + /** + * Checks whether the voice connection is authenticated. + * @private + */ + checkAuthenticated() { + const { token, endpoint, sessionID } = this.authentication; + + if (token && endpoint && sessionID) { + this.client.clearTimeout(this.connectTimeout); + this.status = Constants.VoiceStatus.CONNECTING; + /** + * Emitted when we successfully initiate a voice connection. + * @event VoiceConnection#authenticated + */ + this.emit('authenticated'); + this.connect(); + } + } + + /** + * Invoked when we fail to initiate a voice connection. + * @param {string} reason The reason for failure + * @private + */ + authenticateFailed(reason) { + this.client.clearTimeout(this.connectTimeout); + if (this.status === Constants.VoiceStatus.AUTHENTICATING) { + /** + * Emitted when we fail to initiate a voice connection. + * @event VoiceConnection#failed + * @param {Error} error The encountered error + */ + this.emit('failed', new Error(reason)); + } else { + /** + * Emitted whenever the connection encounters an error. + * @event VoiceConnection#error + * @param {Error} error The encountered error + */ + this.emit('error', new Error(reason)); + } + this.status = Constants.VoiceStatus.DISCONNECTED; + } + + /** + * Move to a different voice channel in the same guild. + * @param {VoiceChannel} channel The channel to move to + * @private + */ + updateChannel(channel) { + this.channel = channel; + this.sendVoiceStateUpdate(); + } + + /** + * Attempts to authenticate to the voice server. + * @private + */ + authenticate() { + this.sendVoiceStateUpdate(); + this.connectTimeout = this.client.setTimeout( + () => this.authenticateFailed(new Error('Connection not established within 15 seconds.')), 15000); + } + + /** + * Attempts to reconnect to the voice server (typically after a region change). + * @param {string} token The voice token + * @param {string} endpoint The voice endpoint + * @private + */ + reconnect(token, endpoint) { + this.authentication.token = token; + this.authentication.endpoint = endpoint; + + this.status = Constants.VoiceStatus.RECONNECTING; + /** + * Emitted when the voice connection is reconnecting (typically after a region change). + * @event VoiceConnection#reconnecting + */ + this.emit('reconnecting'); + this.connect(); + } + + /** + * Disconnect the voice connection, causing a disconnect and closing event to be emitted. + */ + disconnect() { + this.emit('closing'); + this.sendVoiceStateUpdate({ + channel_id: null, + }); + + this._disconnect(); + } + + /** + * Internally disconnects (doesn't send disconnect packet). + * @private + */ + _disconnect() { + this.player.destroy(); + this.cleanup(); + this.status = Constants.VoiceStatus.DISCONNECTED; + /** + * Emitted when the voice connection disconnects. + * @event VoiceConnection#disconnect + */ + this.emit('disconnect'); + } + + /** + * Cleans up after disconnect. + * @private + */ + cleanup() { + const { ws, udp } = this.sockets; + + if (ws) { + ws.removeAllListeners('error'); + ws.removeAllListeners('ready'); + ws.removeAllListeners('sessionDescription'); + ws.removeAllListeners('startSpeaking'); + ws.shutdown(); + } + + if (udp) udp.removeAllListeners('error'); + + this.sockets.ws = null; + this.sockets.udp = null; + } + + /** + * Connect the voice connection. + * @private + */ + connect() { + if (this.status !== Constants.VoiceStatus.RECONNECTING) { + if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.'); + if (this.sockets.udp) throw new Error('There is already an existing UDP connection.'); + } + + if (this.sockets.ws) this.sockets.ws.shutdown(); + if (this.sockets.udp) this.sockets.udp.shutdown(); + + this.sockets.ws = new VoiceWebSocket(this); + this.sockets.udp = new VoiceUDP(this); + + const { ws, udp } = this.sockets; + + ws.on('error', err => this.emit('error', err)); + udp.on('error', err => this.emit('error', err)); + ws.on('ready', this.onReady.bind(this)); + ws.on('sessionDescription', this.onSessionDescription.bind(this)); + ws.on('startSpeaking', this.onStartSpeaking.bind(this)); + } + + /** + * Invoked when the voice websocket is ready. + * @param {Object} data The received data + * @private + */ + onReady({ port, ssrc, ip }) { + this.authentication.port = port; + this.authentication.ssrc = ssrc; + this.sockets.udp.createUDPSocket(ip); + this.sockets.udp.socket.on('message', this.onUDPMessage.bind(this)); + } + + /** + * Invoked when a session description is received. + * @param {string} mode The encryption mode + * @param {string} secret The secret key + * @private + */ + onSessionDescription(mode, secret) { + this.authentication.encryptionMode = mode; + this.authentication.secretKey = secret; + + this.status = Constants.VoiceStatus.CONNECTED; + const ready = () => { + /** + * Emitted once the connection is ready, when a promise to join a voice channel resolves, + * the connection will already be ready. + * @event VoiceConnection#ready + */ + this.emit('ready'); + }; + if (this.dispatcher) { + ready(); + } else { + // This serves to provide support for voice receive, sending audio is required to receive it. + this.playOpusStream(new SingleSilence()).once('end', ready); + } + } + + /** + * Invoked whenever a user initially starts speaking. + * @param {Object} data The speaking data + * @private + */ + onStartSpeaking({ user_id, ssrc }) { + this.ssrcMap.set(+ssrc, user_id); + } + + /** + * Invoked when a speaking event is received. + * @param {Object} data The received data + * @private + */ + onSpeaking({ user_id, speaking }) { + const guild = this.channel.guild; + const user = this.client.users.get(user_id); + if (!speaking) { + for (const receiver of this.receivers) { + receiver.stoppedSpeaking(user); + } + } + /** + * Emitted whenever a user starts/stops speaking. + * @event VoiceConnection#speaking + * @param {User} user The user that has started/stopped speaking + * @param {boolean} speaking Whether or not the user is speaking + */ + if (this.status === Constants.VoiceStatus.CONNECTED) this.emit('speaking', user, speaking); + guild._memberSpeakUpdate(user_id, speaking); + } + + /** + * Handles synthesizing of the speaking event. + * @param {Buffer} buffer Received packet from the UDP socket + * @private + */ + onUDPMessage(buffer) { + const ssrc = +buffer.readUInt32BE(8).toString(10); + const user = this.client.users.get(this.ssrcMap.get(ssrc)); + if (!user) return; + + let speakingTimeout = this.speakingTimeouts.get(ssrc); + if (typeof speakingTimeout === 'undefined') { + this.onSpeaking({ user_id: user.id, ssrc, speaking: true }); + } else { + this.client.clearTimeout(speakingTimeout); + } + + speakingTimeout = this.client.setTimeout(() => { + try { + this.onSpeaking({ user_id: user.id, ssrc, speaking: false }); + this.client.clearTimeout(speakingTimeout); + this.speakingTimeouts.delete(ssrc); + } catch (ex) { + // Connection already closed, ignore + } + }, DISCORD_SPEAKING_DELAY); + this.speakingTimeouts.set(ssrc, speakingTimeout); + } + + /** + * Options that can be passed to stream-playing methods: + * @typedef {Object} StreamOptions + * @property {number} [seek=0] The time to seek to + * @property {number} [volume=1] The volume to play at + * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss + * @property {number|string} [bitrate=48000] The bitrate (quality) of the audio. + * If set to 'auto', the voice channel's bitrate will be used + */ + + /** + * Play the given file in the voice connection. + * @param {string} file The absolute path to the file + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play files natively + * voiceChannel.join() + * .then(connection => { + * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); + * }) + * .catch(console.error); + */ + playFile(file, options) { + return this.player.playUnknownStream(`file:${file}`, options); + } + + /** + * Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) + * @param {string} input the arbitrary input + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playArbitraryInput(input, options) { + return this.player.playUnknownStream(input, options); + } + + /** + * Plays and converts an audio stream in the voice connection. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play streams using ytdl-core + * const ytdl = require('ytdl-core'); + * const streamOptions = { seek: 0, volume: 1 }; + * voiceChannel.join() + * .then(connection => { + * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); + * const dispatcher = connection.playStream(stream, streamOptions); + * }) + * .catch(console.error); + */ + playStream(stream, options) { + return this.player.playUnknownStream(stream, options); + } + + /** + * Plays a stream of 16-bit signed stereo PCM. + * @param {ReadableStream} stream The audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playConvertedStream(stream, options) { + return this.player.playPCMStream(stream, options); + } + + /** + * Plays an Opus encoded stream. + * <warn>Note that inline volume is not compatible with this method.</warn> + * @param {ReadableStream} stream The Opus audio stream to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + */ + playOpusStream(stream, options) { + return this.player.playOpusStream(stream, options); + } + + /** + * Plays a voice broadcast. + * @param {VoiceBroadcast} broadcast The broadcast to play + * @param {StreamOptions} [options] Options for playing the stream + * @returns {StreamDispatcher} + * @example + * // Play a broadcast + * const broadcast = client + * .createVoiceBroadcast() + * .playFile('./test.mp3'); + * const dispatcher = voiceConnection.playBroadcast(broadcast); + */ + playBroadcast(broadcast, options) { + return this.player.playBroadcast(broadcast, options); + } + + /** + * Creates a VoiceReceiver so you can start listening to voice data. + * It's recommended to only create one of these. + * @returns {VoiceReceiver} + */ + createReceiver() { + const receiver = new VoiceReceiver(this); + this.receivers.push(receiver); + return receiver; + } +} + +module.exports = VoiceConnection; diff --git a/node_modules/discord.js/src/client/voice/VoiceUDPClient.js b/node_modules/discord.js/src/client/voice/VoiceUDPClient.js new file mode 100644 index 0000000..cf1e388 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceUDPClient.js @@ -0,0 +1,127 @@ +const udp = require('dgram'); +const Constants = require('../../util/Constants'); +const EventEmitter = require('events').EventEmitter; + +/** + * Represents a UDP client for a Voice Connection. + * @extends {EventEmitter} + * @private + */ +class VoiceConnectionUDPClient extends EventEmitter { + constructor(voiceConnection) { + super(); + + /** + * The voice connection that this UDP client serves + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + + /** + * The UDP socket + * @type {?Socket} + */ + this.socket = null; + + /** + * The address of the Discord voice server + * @type {?string} + */ + this.discordAddress = null; + + /** + * The local IP address + * @type {?string} + */ + this.localAddress = null; + + /** + * The local port + * @type {?string} + */ + this.localPort = null; + + this.voiceConnection.on('closing', this.shutdown.bind(this)); + } + + shutdown() { + if (this.socket) { + this.socket.removeAllListeners('message'); + try { + this.socket.close(); + } finally { + this.socket = null; + } + } + } + + /** + * The port of the Discord voice server + * @type {number} + * @readonly + */ + get discordPort() { + return this.voiceConnection.authentication.port; + } + + /** + * Send a packet to the UDP client. + * @param {Object} packet The packet to send + * @returns {Promise<Object>} + */ + send(packet) { + return new Promise((resolve, reject) => { + if (!this.socket) throw new Error('Tried to send a UDP packet, but there is no socket available.'); + if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.'); + this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { + if (error) reject(error); else resolve(packet); + }); + }); + } + + createUDPSocket(address) { + this.discordAddress = address; + const socket = this.socket = udp.createSocket('udp4'); + + socket.once('message', message => { + const packet = parseLocalPacket(message); + if (packet.error) { + this.emit('error', packet.error); + return; + } + + this.localAddress = packet.address; + this.localPort = packet.port; + + this.voiceConnection.sockets.ws.sendPacket({ + op: Constants.VoiceOPCodes.SELECT_PROTOCOL, + d: { + protocol: 'udp', + data: { + address: packet.address, + port: packet.port, + mode: 'xsalsa20_poly1305', + }, + }, + }); + }); + + const blankMessage = Buffer.alloc(70); + blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4); + this.send(blankMessage); + } +} + +function parseLocalPacket(message) { + try { + const packet = Buffer.from(message); + let address = ''; + for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]); + const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10); + return { address, port }; + } catch (error) { + return { error }; + } +} + +module.exports = VoiceConnectionUDPClient; diff --git a/node_modules/discord.js/src/client/voice/VoiceWebSocket.js b/node_modules/discord.js/src/client/voice/VoiceWebSocket.js new file mode 100644 index 0000000..2269007 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/VoiceWebSocket.js @@ -0,0 +1,246 @@ +const Constants = require('../../util/Constants'); +const SecretKey = require('./util/SecretKey'); +const EventEmitter = require('events').EventEmitter; + +let WebSocket; +try { + WebSocket = require('@discordjs/uws'); +} catch (err) { + WebSocket = require('ws'); +} + +/** + * Represents a Voice Connection's WebSocket. + * @extends {EventEmitter} + * @private + */ +class VoiceWebSocket extends EventEmitter { + constructor(voiceConnection) { + super(); + + /** + * The client of this voice WebSocket + * @type {Client} + */ + this.client = voiceConnection.voiceManager.client; + + /** + * The Voice Connection that this WebSocket serves + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + + /** + * How many connection attempts have been made + * @type {number} + */ + this.attempts = 0; + + this.connect(); + this.dead = false; + this.voiceConnection.on('closing', this.shutdown.bind(this)); + } + + shutdown() { + this.dead = true; + this.reset(); + } + + /** + * Resets the current WebSocket. + */ + reset() { + if (this.ws) { + if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close(); + this.ws = null; + } + this.clearHeartbeat(); + } + + /** + * Starts connecting to the Voice WebSocket Server. + */ + connect() { + if (this.dead) return; + if (this.ws) this.reset(); + if (this.attempts >= 5) { + this.emit('debug', new Error(`Too many connection attempts (${this.attempts}).`)); + return; + } + + this.attempts++; + + /** + * The actual WebSocket used to connect to the Voice WebSocket Server. + * @type {WebSocket} + */ + this.ws = new WebSocket(`wss://${this.voiceConnection.authentication.endpoint}`); + this.ws.onopen = this.onOpen.bind(this); + this.ws.onmessage = this.onMessage.bind(this); + this.ws.onclose = this.onClose.bind(this); + this.ws.onerror = this.onError.bind(this); + } + + /** + * Sends data to the WebSocket if it is open. + * @param {string} data The data to send to the WebSocket + * @returns {Promise<string>} + */ + send(data) { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error(`Voice websocket not open to send ${data}.`); + } + this.ws.send(data, null, error => { + if (error) reject(error); else resolve(data); + }); + }); + } + + /** + * JSON.stringify's a packet and then sends it to the WebSocket Server. + * @param {Object} packet The packet to send + * @returns {Promise<string>} + */ + sendPacket(packet) { + try { + packet = JSON.stringify(packet); + } catch (error) { + return Promise.reject(error); + } + return this.send(packet); + } + + /** + * Called whenever the WebSocket opens. + */ + onOpen() { + this.sendPacket({ + op: Constants.OPCodes.DISPATCH, + d: { + server_id: this.voiceConnection.channel.guild.id, + user_id: this.client.user.id, + token: this.voiceConnection.authentication.token, + session_id: this.voiceConnection.authentication.sessionID, + }, + }).catch(() => { + this.emit('error', new Error('Tried to send join packet, but the WebSocket is not open.')); + }); + } + + /** + * Called whenever a message is received from the WebSocket. + * @param {MessageEvent} event The message event that was received + * @returns {void} + */ + onMessage(event) { + try { + return this.onPacket(JSON.parse(event.data)); + } catch (error) { + return this.onError(error); + } + } + + /** + * Called whenever the connection to the WebSocket server is lost. + */ + onClose() { + if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000); + } + + /** + * Called whenever an error occurs with the WebSocket. + * @param {Error} error The error that occurred + */ + onError(error) { + this.emit('error', error); + } + + /** + * Called whenever a valid packet is received from the WebSocket. + * @param {Object} packet The received packet + */ + onPacket(packet) { + switch (packet.op) { + case Constants.VoiceOPCodes.READY: + this.setHeartbeat(packet.d.heartbeat_interval); + /** + * Emitted once the voice WebSocket receives the ready packet. + * @param {Object} packet The received packet + * @event VoiceWebSocket#ready + */ + this.emit('ready', packet.d); + break; + case Constants.VoiceOPCodes.SESSION_DESCRIPTION: + /** + * Emitted once the Voice Websocket receives a description of this voice session. + * @param {string} encryptionMode The type of encryption being used + * @param {SecretKey} secretKey The secret key used for encryption + * @event VoiceWebSocket#sessionDescription + */ + this.emit('sessionDescription', packet.d.mode, new SecretKey(packet.d.secret_key)); + break; + case Constants.VoiceOPCodes.SPEAKING: + /** + * Emitted whenever a speaking packet is received. + * @param {Object} data + * @event VoiceWebSocket#startSpeaking + */ + this.emit('startSpeaking', packet.d); + break; + default: + /** + * Emitted when an unhandled packet is received. + * @param {Object} packet + * @event VoiceWebSocket#unknownPacket + */ + this.emit('unknownPacket', packet); + break; + } + } + + /** + * Sets an interval at which to send a heartbeat packet to the WebSocket. + * @param {number} interval The interval at which to send a heartbeat packet + */ + setHeartbeat(interval) { + if (!interval || isNaN(interval)) { + this.onError(new Error('Tried to set voice heartbeat but no valid interval was specified.')); + return; + } + if (this.heartbeatInterval) { + /** + * Emitted whenver the voice WebSocket encounters a non-fatal error. + * @param {string} warn The warning + * @event VoiceWebSocket#warn + */ + this.emit('warn', 'A voice heartbeat interval is being overwritten'); + clearInterval(this.heartbeatInterval); + } + this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval); + } + + /** + * Clears a heartbeat interval, if one exists. + */ + clearHeartbeat() { + if (!this.heartbeatInterval) { + this.emit('warn', 'Tried to clear a heartbeat interval that does not exist'); + return; + } + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + + /** + * Sends a heartbeat packet. + */ + sendHeartbeat() { + this.sendPacket({ op: Constants.VoiceOPCodes.HEARTBEAT, d: null }).catch(() => { + this.emit('warn', 'Tried to send heartbeat, but connection is not open'); + this.clearHeartbeat(); + }); + } +} + +module.exports = VoiceWebSocket; diff --git a/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js b/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js new file mode 100644 index 0000000..77734ca --- /dev/null +++ b/node_modules/discord.js/src/client/voice/dispatcher/StreamDispatcher.js @@ -0,0 +1,331 @@ +const VolumeInterface = require('../util/VolumeInterface'); +const VoiceBroadcast = require('../VoiceBroadcast'); +const Constants = require('../../../util/Constants'); + +const secretbox = require('../util/Secretbox'); + +const nonce = Buffer.alloc(24); +nonce.fill(0); + +/** + * The class that sends voice packet data to the voice connection. + * ```js + * // Obtained using: + * voiceChannel.join().then(connection => { + * // You can play a file or a stream here: + * const dispatcher = connection.playFile('./file.mp3'); + * }); + * ``` + * @implements {VolumeInterface} + */ +class StreamDispatcher extends VolumeInterface { + constructor(player, stream, streamOptions) { + super(streamOptions); + /** + * The Audio Player that controls this dispatcher + * @type {AudioPlayer} + */ + this.player = player; + /** + * The stream that the dispatcher plays + * @type {ReadableStream|VoiceBroadcast} + */ + this.stream = stream; + if (!(this.stream instanceof VoiceBroadcast)) this.startStreaming(); + this.streamOptions = streamOptions; + + const data = this.streamingData; + data.length = 20; + data.missed = 0; + + /** + * Whether playing is paused + * @type {boolean} + */ + this.paused = false; + /** + * Whether this dispatcher has been destroyed + * @type {boolean} + */ + this.destroyed = false; + + this._opus = streamOptions.opus; + } + + /** + * How many passes the dispatcher should take when sending packets to reduce packet loss. Values over 5 + * aren't recommended, as it means you are using 5x more bandwidth. You _can_ edit this at runtime + * @type {number} + * @readonly + */ + get passes() { + return this.streamOptions.passes || 1; + } + + set passes(n) { + this.streamOptions.passes = n; + } + + get streamingData() { + return this.player.streamingData; + } + + /** + * How long the stream dispatcher has been "speaking" for + * @type {number} + * @readonly + */ + get time() { + return this.streamingData.count * (this.streamingData.length || 0); + } + + /** + * The total time, taking into account pauses and skips, that the dispatcher has been streaming for + * @type {number} + * @readonly + */ + get totalStreamTime() { + return this.time + this.streamingData.pausedTime; + } + + /** + * Stops sending voice packets to the voice connection (stream may still progress however). + */ + pause() { this.setPaused(true); } + + /** + * Resumes sending voice packets to the voice connection (may be further on in the stream than when paused). + */ + resume() { this.setPaused(false); } + + + /** + * Stops the current stream permanently and emits an `end` event. + * @param {string} [reason='user'] An optional reason for stopping the dispatcher + */ + end(reason = 'user') { + this.destroy('end', reason); + } + + setSpeaking(value) { + if (this.speaking === value) return; + if (this.player.voiceConnection.status !== Constants.VoiceStatus.CONNECTED) return; + this.speaking = value; + /** + * Emitted when the dispatcher starts/stops speaking. + * @event StreamDispatcher#speaking + * @param {boolean} value Whether or not the dispatcher is speaking + */ + this.emit('speaking', value); + } + + + /** + * Set the bitrate of the current Opus encoder. + * @param {number} bitrate New bitrate, in kbps + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(bitrate) { + this.player.setBitrate(bitrate); + } + + sendBuffer(buffer, sequence, timestamp, opusPacket) { + opusPacket = opusPacket || this.player.opusEncoder.encode(buffer); + const packet = this.createPacket(sequence, timestamp, opusPacket); + this.sendPacket(packet); + } + + sendPacket(packet) { + let repeats = this.passes; + /** + * Emitted whenever the dispatcher has debug information. + * @event StreamDispatcher#debug + * @param {string} info The debug info + */ + this.setSpeaking(true); + while (repeats-- && this.player.voiceConnection.sockets.udp) { + this.player.voiceConnection.sockets.udp.send(packet) + .catch(e => { + this.setSpeaking(false); + this.emit('debug', `Failed to send a packet ${e}`); + }); + } + } + + createPacket(sequence, timestamp, buffer) { + const packetBuffer = Buffer.alloc(buffer.length + 28); + packetBuffer.fill(0); + packetBuffer[0] = 0x80; + packetBuffer[1] = 0x78; + + packetBuffer.writeUIntBE(sequence, 2, 2); + packetBuffer.writeUIntBE(timestamp, 4, 4); + packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4); + + packetBuffer.copy(nonce, 0, 0, 12); + buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key); + for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i]; + + return packetBuffer; + } + + processPacket(packet) { + try { + if (this.destroyed || !this.player.voiceConnection.authentication.secretKey) { + this.setSpeaking(false); + return; + } + + const data = this.streamingData; + + if (this.paused) { + this.setSpeaking(false); + data.pausedTime = data.length * 10; + return; + } + + if (!packet) { + data.missed++; + data.pausedTime += data.length * 10; + return; + } + + this.started(); + this.missed = 0; + + this.stepStreamingData(); + this.sendBuffer(null, data.sequence, data.timestamp, packet); + } catch (e) { + this.destroy('error', e); + } + } + + process() { + try { + if (this.destroyed) { + this.setSpeaking(false); + return; + } + + const data = this.streamingData; + + if (data.missed >= 5) { + this.destroy('end', 'Stream is not generating quickly enough.'); + return; + } + + if (this.paused) { + this.setSpeaking(false); + // Old code? + // data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0; + data.pausedTime += data.length * 10; + this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10); + return; + } + + this.started(); + + const buffer = this.readStreamBuffer(); + if (!buffer) { + data.missed++; + data.pausedTime += data.length * 10; + this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10); + return; + } + + data.missed = 0; + + this.stepStreamingData(); + + if (this._opus) { + this.sendBuffer(null, data.sequence, data.timestamp, buffer); + } else { + this.sendBuffer(buffer, data.sequence, data.timestamp); + } + + const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now()); + this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), nextTime); + } catch (e) { + this.destroy('error', e); + } + } + + readStreamBuffer() { + const data = this.streamingData; + const bufferLength = (this._opus ? 80 : 1920) * data.channels; + let buffer = this.stream.read(bufferLength); + if (this._opus) return buffer; + if (!buffer) return null; + + if (buffer.length !== bufferLength) { + const newBuffer = Buffer.alloc(bufferLength).fill(0); + buffer.copy(newBuffer); + buffer = newBuffer; + } + + buffer = this.applyVolume(buffer); + return buffer; + } + + started() { + const data = this.streamingData; + + if (!data.startTime) { + /** + * Emitted once the dispatcher starts streaming. + * @event StreamDispatcher#start + */ + this.emit('start'); + data.startTime = Date.now(); + } + } + + stepStreamingData() { + const data = this.streamingData; + data.count++; + data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0; + data.timestamp = (data.timestamp + 960) < 4294967295 ? data.timestamp + 960 : 0; + } + + destroy(type, reason) { + if (this.destroyed) return; + this.destroyed = true; + this.setSpeaking(false); + this.emit(type, reason); + /** + * Emitted once the dispatcher ends. + * @param {string} [reason] The reason the dispatcher ended + * @event StreamDispatcher#end + */ + if (type !== 'end') this.emit('end', `destroyed due to ${type} - ${reason}`); + } + + startStreaming() { + if (!this.stream) { + /** + * Emitted if the dispatcher encounters an error. + * @event StreamDispatcher#error + * @param {string} error The error message + */ + this.emit('error', 'No stream'); + return; + } + + this.stream.on('end', err => this.destroy('end', err || 'stream')); + this.stream.on('error', err => this.destroy('error', err)); + + const data = this.streamingData; + data.length = 20; + data.missed = 0; + + this.stream.once('readable', () => { + data.startTime = null; + data.count = 0; + this.process(); + }); + } + + setPaused(paused) { this.setSpeaking(!(this.paused = paused)); } +} + +module.exports = StreamDispatcher; diff --git a/node_modules/discord.js/src/client/voice/opus/BaseOpusEngine.js b/node_modules/discord.js/src/client/voice/opus/BaseOpusEngine.js new file mode 100644 index 0000000..a510449 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/BaseOpusEngine.js @@ -0,0 +1,60 @@ +/** + * The base opus encoding engine. + * @private + */ +class BaseOpus { + /** + * @param {Object} [options] The options to apply to the Opus engine + * @param {number} [options.bitrate=48] The desired bitrate (kbps) + * @param {boolean} [options.fec=false] Whether to enable forward error correction + * @param {number} [options.plp=0] The expected packet loss percentage + */ + constructor({ bitrate = 48, fec = false, plp = 0 } = {}) { + this.ctl = { + BITRATE: 4002, + FEC: 4012, + PLP: 4014, + }; + + this.samplingRate = 48000; + this.channels = 2; + + /** + * The desired bitrate (kbps) + * @type {number} + */ + this.bitrate = bitrate; + + /** + * Miscellaneous Opus options + * @type {Object} + */ + this.options = { fec, plp }; + } + + init() { + try { + this.setBitrate(this.bitrate); + + // Set FEC (forward error correction) + if (this.options.fec) this.setFEC(this.options.fec); + + // Set PLP (expected packet loss percentage) + if (this.options.plp) this.setPLP(this.options.plp); + } catch (err) { + // Opus engine likely has no support for libopus CTL + } + } + + encode(buffer) { + return buffer; + } + + decode(buffer) { + return buffer; + } + + destroy() {} // eslint-disable-line no-empty-function +} + +module.exports = BaseOpus; diff --git a/node_modules/discord.js/src/client/voice/opus/DiscordJsOpusEngine.js b/node_modules/discord.js/src/client/voice/opus/DiscordJsOpusEngine.js new file mode 100644 index 0000000..a3759c2 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/DiscordJsOpusEngine.js @@ -0,0 +1,34 @@ +const OpusEngine = require('./BaseOpusEngine'); + +class DiscordJsOpusEngine extends OpusEngine { + constructor(player) { + super(player); + const opus = require('@discordjs/opus'); + this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels); + super.init(); + } + + setBitrate(bitrate) { + this.encoder.setBitrate(Math.min(128, Math.max(16, bitrate)) * 1000); + } + + setFEC(enabled) { + this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0); + } + + setPLP(percent) { + this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100))); + } + + encode(buffer) { + super.encode(buffer); + return this.encoder.encode(buffer, 1920); + } + + decode(buffer) { + super.decode(buffer); + return this.encoder.decode(buffer, 1920); + } +} + +module.exports = DiscordJsOpusEngine; diff --git a/node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js b/node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js new file mode 100644 index 0000000..a9ffc55 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js @@ -0,0 +1,34 @@ +const OpusEngine = require('./BaseOpusEngine'); + +class NodeOpusEngine extends OpusEngine { + constructor(player) { + super(player); + const opus = require('node-opus'); + this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels); + super.init(); + } + + setBitrate(bitrate) { + this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + + setFEC(enabled) { + this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0); + } + + setPLP(percent) { + this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100))); + } + + encode(buffer) { + super.encode(buffer); + return this.encoder.encode(buffer, 1920); + } + + decode(buffer) { + super.decode(buffer); + return this.encoder.decode(buffer, 1920); + } +} + +module.exports = NodeOpusEngine; diff --git a/node_modules/discord.js/src/client/voice/opus/OpusEngineList.js b/node_modules/discord.js/src/client/voice/opus/OpusEngineList.js new file mode 100644 index 0000000..cf63fbc --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/OpusEngineList.js @@ -0,0 +1,29 @@ +const list = [ + require('./DiscordJsOpusEngine'), + require('./NodeOpusEngine'), + require('./OpusScriptEngine'), +]; + +function fetch(Encoder, engineOptions) { + try { + return new Encoder(engineOptions); + } catch (err) { + if (err.message.includes('Cannot find module')) return null; + + // The Opus engine exists, but another error occurred. + throw err; + } +} + +exports.add = encoder => { + list.push(encoder); +}; + +exports.fetch = engineOptions => { + for (const encoder of list) { + const fetched = fetch(encoder, engineOptions); + if (fetched) return fetched; + } + + throw new Error('Couldn\'t find an Opus engine.'); +}; diff --git a/node_modules/discord.js/src/client/voice/opus/OpusScriptEngine.js b/node_modules/discord.js/src/client/voice/opus/OpusScriptEngine.js new file mode 100644 index 0000000..271a068 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/opus/OpusScriptEngine.js @@ -0,0 +1,39 @@ +const OpusEngine = require('./BaseOpusEngine'); + +class OpusScriptEngine extends OpusEngine { + constructor(player) { + super(player); + const OpusScript = require('opusscript'); + this.encoder = new OpusScript(this.samplingRate, this.channels); + super.init(); + } + + setBitrate(bitrate) { + this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + + setFEC(enabled) { + this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0); + } + + setPLP(percent) { + this.encoder.encoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100))); + } + + encode(buffer) { + super.encode(buffer); + return this.encoder.encode(buffer, 960); + } + + decode(buffer) { + super.decode(buffer); + return this.encoder.decode(buffer); + } + + destroy() { + super.destroy(); + this.encoder.delete(); + } +} + +module.exports = OpusScriptEngine; diff --git a/node_modules/discord.js/src/client/voice/player/AudioPlayer.js b/node_modules/discord.js/src/client/voice/player/AudioPlayer.js new file mode 100644 index 0000000..9b30c85 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/player/AudioPlayer.js @@ -0,0 +1,170 @@ +const EventEmitter = require('events').EventEmitter; +const Prism = require('prism-media'); +const StreamDispatcher = require('../dispatcher/StreamDispatcher'); +const Collection = require('../../../util/Collection'); +const OpusEncoders = require('../opus/OpusEngineList'); + +const ffmpegArguments = [ + '-analyzeduration', '0', + '-loglevel', '0', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', +]; + +/** + * An Audio Player for a Voice Connection. + * @private + * @extends {EventEmitter} + */ +class AudioPlayer extends EventEmitter { + constructor(voiceConnection) { + super(); + /** + * The voice connection that the player serves + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + /** + * The prism transcoder that the player uses + * @type {Prism} + */ + this.prism = new Prism(); + this.streams = new Collection(); + this.currentStream = {}; + this.streamingData = { + channels: 2, + count: 0, + sequence: 0, + timestamp: 0, + pausedTime: 0, + }; + this.voiceConnection.once('closing', () => this.destroyCurrentStream()); + } + + /** + * The current transcoder + * @type {?Object} + * @readonly + */ + get transcoder() { + return this.currentStream.transcoder; + } + + /** + * The current dispatcher + * @type {?StreamDispatcher} + * @readonly + */ + get dispatcher() { + return this.currentStream.dispatcher; + } + + destroy() { + if (this.opusEncoder) this.opusEncoder.destroy(); + this.opusEncoder = null; + } + + destroyCurrentStream() { + const transcoder = this.transcoder; + const dispatcher = this.dispatcher; + if (transcoder) transcoder.kill(); + if (dispatcher) { + const end = dispatcher.listeners('end')[0]; + const error = dispatcher.listeners('error')[0]; + if (end) dispatcher.removeListener('end', end); + if (error) dispatcher.removeListener('error', error); + dispatcher.destroy('end'); + } + this.currentStream = {}; + } + + /** + * Set the bitrate of the current Opus encoder. + * @param {number} value New bitrate, in kbps + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(value) { + if (!value) return; + if (!this.opusEncoder) return; + const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value; + this.opusEncoder.setBitrate(bitrate); + } + + playUnknownStream(stream, options = {}) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); + const transcoder = this.prism.transcode({ + type: 'ffmpeg', + media: stream, + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), + }); + this.destroyCurrentStream(); + this.currentStream = { + transcoder: transcoder, + output: transcoder.output, + input: stream, + }; + transcoder.on('error', e => { + this.destroyCurrentStream(); + if (this.listenerCount('error') > 0) this.emit('error', e); + this.emit('warn', `prism transcoder error - ${e}`); + }); + return this.playPCMStream(transcoder.output, options, true); + } + + playPCMStream(stream, options = {}, fromUnknown = false) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); + this.setBitrate(options.bitrate); + const dispatcher = this.createDispatcher(stream, options); + if (fromUnknown) { + this.currentStream.dispatcher = dispatcher; + } else { + this.destroyCurrentStream(); + this.currentStream = { + dispatcher, + input: stream, + output: stream, + }; + } + return dispatcher; + } + + playOpusStream(stream, options = {}) { + options.opus = true; + this.destroyCurrentStream(); + const dispatcher = this.createDispatcher(stream, options); + this.currentStream = { + dispatcher, + input: stream, + output: stream, + }; + return dispatcher; + } + + playBroadcast(broadcast, options) { + this.destroyCurrentStream(); + const dispatcher = this.createDispatcher(broadcast, options); + this.currentStream = { + dispatcher, + broadcast, + input: broadcast, + output: broadcast, + }; + broadcast.registerDispatcher(dispatcher); + return dispatcher; + } + + createDispatcher(stream, { seek = 0, volume = 1, passes = 1, opus } = {}) { + const options = { seek, volume, passes, opus }; + + const dispatcher = new StreamDispatcher(this, stream, options); + dispatcher.on('end', () => this.destroyCurrentStream()); + dispatcher.on('error', () => this.destroyCurrentStream()); + dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); + return dispatcher; + } +} + +module.exports = AudioPlayer; diff --git a/node_modules/discord.js/src/client/voice/receiver/VoiceReadable.js b/node_modules/discord.js/src/client/voice/receiver/VoiceReadable.js new file mode 100644 index 0000000..d349428 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/VoiceReadable.js @@ -0,0 +1,17 @@ +const Readable = require('stream').Readable; + +class VoiceReadable extends Readable { + constructor() { + super(); + this._packets = []; + this.open = true; + } + + _read() {} // eslint-disable-line no-empty-function + + _push(d) { + if (this.open) this.push(d); + } +} + +module.exports = VoiceReadable; diff --git a/node_modules/discord.js/src/client/voice/receiver/VoiceReceiver.js b/node_modules/discord.js/src/client/voice/receiver/VoiceReceiver.js new file mode 100644 index 0000000..e37b273 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/receiver/VoiceReceiver.js @@ -0,0 +1,220 @@ +const EventEmitter = require('events').EventEmitter; +const secretbox = require('../util/Secretbox'); +const Readable = require('./VoiceReadable'); +const OpusEncoders = require('../opus/OpusEngineList'); + +const nonce = Buffer.alloc(24); +nonce.fill(0); + +/** + * Receives voice data from a voice connection. + * ```js + * // Obtained using: + * voiceChannel.join() + * .then(connection => { + * const receiver = connection.createReceiver(); + * }); + * ``` + * @extends {EventEmitter} + */ +class VoiceReceiver extends EventEmitter { + constructor(connection) { + super(); + /* + Need a queue because we don't get the ssrc of the user speaking until after the first few packets, + so we queue up unknown SSRCs until they become known, then empty the queue + */ + this.queues = new Map(); + this.pcmStreams = new Map(); + this.opusStreams = new Map(); + this.opusEncoders = new Map(); + + /** + * Whether or not this receiver has been destroyed + * @type {boolean} + */ + this.destroyed = false; + + /** + * The VoiceConnection that instantiated this + * @type {VoiceConnection} + */ + this.voiceConnection = connection; + + this._listener = msg => { + const ssrc = +msg.readUInt32BE(8).toString(10); + const user = connection.client.users.get(connection.ssrcMap.get(ssrc)); + if (!user) { + if (!this.queues.has(ssrc)) this.queues.set(ssrc, []); + this.queues.get(ssrc).push(msg); + } else { + if (this.queues.get(ssrc)) { + this.queues.get(ssrc).push(msg); + this.queues.get(ssrc).map(m => this.handlePacket(m, user)); + this.queues.delete(ssrc); + return; + } + this.handlePacket(msg, user); + } + }; + this.voiceConnection.sockets.udp.socket.on('message', this._listener); + } + + /** + * If this VoiceReceiver has been destroyed, running `recreate()` will recreate the listener. + * This avoids you having to create a new receiver. + * <info>Any streams that you had prior to destroying the receiver will not be recreated.</info> + */ + recreate() { + if (!this.destroyed) return; + this.voiceConnection.sockets.udp.socket.on('message', this._listener); + this.destroyed = false; + } + + /** + * Destroy this VoiceReceiver, also ending any streams that it may be controlling. + */ + destroy() { + this.voiceConnection.sockets.udp.socket.removeListener('message', this._listener); + for (const [id, stream] of this.pcmStreams) { + stream._push(null); + this.pcmStreams.delete(id); + } + for (const [id, stream] of this.opusStreams) { + stream._push(null); + this.opusStreams.delete(id); + } + for (const [id, encoder] of this.opusEncoders) { + encoder.destroy(); + this.opusEncoders.delete(id); + } + this.destroyed = true; + } + + /** + * Invoked when a user stops speaking. + * @param {User} user The user that stopped speaking + * @private + */ + stoppedSpeaking(user) { + const opusStream = this.opusStreams.get(user.id); + const pcmStream = this.pcmStreams.get(user.id); + const opusEncoder = this.opusEncoders.get(user.id); + if (opusStream) { + opusStream.push(null); + opusStream.open = false; + this.opusStreams.delete(user.id); + } + if (pcmStream) { + pcmStream.push(null); + pcmStream.open = false; + this.pcmStreams.delete(user.id); + } + if (opusEncoder) { + opusEncoder.destroy(); + this.opusEncoders.delete(user.id); + } + } + + /** + * Creates a readable stream for a user that provides opus data while the user is speaking. When the user + * stops speaking, the stream is destroyed. + * @param {UserResolvable} user The user to create the stream for + * @returns {ReadableStream} + */ + createOpusStream(user) { + user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); + if (!user) throw new Error('Couldn\'t resolve the user to create Opus stream.'); + if (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.'); + const stream = new Readable(); + this.opusStreams.set(user.id, stream); + return stream; + } + + /** + * Creates a readable stream for a user that provides PCM data while the user is speaking. When the user + * stops speaking, the stream is destroyed. The stream is 16-bit signed stereo PCM at 48KHz. + * @param {UserResolvable} user The user to create the stream for + * @returns {ReadableStream} + */ + createPCMStream(user) { + user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); + if (!user) throw new Error('Couldn\'t resolve the user to create PCM stream.'); + if (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.'); + const stream = new Readable(); + this.pcmStreams.set(user.id, stream); + return stream; + } + + handlePacket(msg, user) { + msg.copy(nonce, 0, 0, 12); + let data = secretbox.methods.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key); + if (!data) { + /** + * Emitted whenever a voice packet experiences a problem. + * @event VoiceReceiver#warn + * @param {string} reason The reason for the warning. If it happened because the voice packet could not be + * decrypted, this would be `decrypt`. If it happened because the voice packet could not be decoded into + * PCM, this would be `decode` + * @param {string} message The warning message + */ + this.emit('warn', 'decrypt', 'Failed to decrypt voice packet'); + return; + } + data = Buffer.from(data); + + // Strip RTP Header Extensions (one-byte only) + if (data[0] === 0xBE && data[1] === 0xDE && data.length > 4) { + const headerExtensionLength = data.readUInt16BE(2); + let offset = 4; + for (let i = 0; i < headerExtensionLength; i++) { + const byte = data[offset]; + offset++; + if (byte === 0) { + continue; + } + offset += 1 + (0b1111 & (byte >> 4)); + } + // Skip over undocumented Discord byte + offset++; + + data = data.slice(offset); + } + + if (this.opusStreams.get(user.id)) this.opusStreams.get(user.id)._push(data); + /** + * Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM). + * @event VoiceReceiver#opus + * @param {User} user The user that is sending the buffer (is speaking) + * @param {Buffer} buffer The opus buffer + */ + this.emit('opus', user, data); + if (this.listenerCount('pcm') > 0 || this.pcmStreams.size > 0) { + if (!this.opusEncoders.get(user.id)) this.opusEncoders.set(user.id, OpusEncoders.fetch()); + const { pcm, error } = VoiceReceiver._tryDecode(this.opusEncoders.get(user.id), data); + if (error) { + this.emit('warn', 'decode', `Failed to decode packet voice to PCM because: ${error.message}`); + return; + } + if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm); + /** + * Emits decoded voice data when it's received. For performance reasons, the decoding will only + * happen if there is at least one `pcm` listener on this receiver. + * @event VoiceReceiver#pcm + * @param {User} user The user that is sending the buffer (is speaking) + * @param {Buffer} buffer The decoded buffer + */ + this.emit('pcm', user, pcm); + } + } + + static _tryDecode(encoder, data) { + try { + return { pcm: encoder.decode(data) }; + } catch (error) { + return { error }; + } + } +} + +module.exports = VoiceReceiver; diff --git a/node_modules/discord.js/src/client/voice/util/SecretKey.js b/node_modules/discord.js/src/client/voice/util/SecretKey.js new file mode 100644 index 0000000..670e9e5 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/SecretKey.js @@ -0,0 +1,16 @@ +/** + * Represents a Secret Key used in encryption over voice. + * @private + */ +class SecretKey { + constructor(key) { + /** + * The key used for encryption + * @type {Uint8Array} + */ + this.key = new Uint8Array(new ArrayBuffer(key.length)); + for (const index of Object.keys(key)) this.key[index] = key[index]; + } +} + +module.exports = SecretKey; diff --git a/node_modules/discord.js/src/client/voice/util/Secretbox.js b/node_modules/discord.js/src/client/voice/util/Secretbox.js new file mode 100644 index 0000000..a833d51 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/Secretbox.js @@ -0,0 +1,33 @@ +const libs = { + sodium: sodium => ({ + open: sodium.api.crypto_secretbox_open_easy, + close: sodium.api.crypto_secretbox_easy, + }), + 'libsodium-wrappers': sodium => ({ + open: sodium.crypto_secretbox_open_easy, + close: sodium.crypto_secretbox_easy, + }), + tweetnacl: tweetnacl => ({ + open: tweetnacl.secretbox.open, + close: tweetnacl.secretbox, + }), +}; + +exports.methods = {}; + +for (const libName of Object.keys(libs)) { + try { + const lib = require(libName); + if (libName === 'libsodium-wrappers' && lib.ready) { + lib.ready.then(() => { + exports.methods = libs[libName](lib); + }).catch(() => { + const tweetnacl = require('tweetnacl'); + exports.methods = libs.tweetnacl(tweetnacl); + }).catch(() => undefined); + } else { + exports.methods = libs[libName](lib); + } + break; + } catch (err) {} // eslint-disable-line no-empty +} diff --git a/node_modules/discord.js/src/client/voice/util/Silence.js b/node_modules/discord.js/src/client/voice/util/Silence.js new file mode 100644 index 0000000..239ceb4 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/Silence.js @@ -0,0 +1,16 @@ +const { Readable } = require('stream'); + +const SILENCE_FRAME = Buffer.from([0xF8, 0xFF, 0xFE]); + +/** + * A readable emitting silent opus frames. + * @extends {Readable} + * @private + */ +class Silence extends Readable { + _read() { + this.push(SILENCE_FRAME); + } +} + +module.exports = Silence; diff --git a/node_modules/discord.js/src/client/voice/util/SingleSilence.js b/node_modules/discord.js/src/client/voice/util/SingleSilence.js new file mode 100644 index 0000000..b59341e --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/SingleSilence.js @@ -0,0 +1,17 @@ +const Silence = require('./Silence'); + +/** + * Only emits a single silent opus frame. + * This is used as a workaround for Discord now requiring + * silence to be sent before being able to receive audio. + * @extends {Silence} + * @private + */ +class SingleSilence extends Silence { + _read() { + super._read(); + this.push(null); + } +} + +module.exports = SingleSilence; diff --git a/node_modules/discord.js/src/client/voice/util/VolumeInterface.js b/node_modules/discord.js/src/client/voice/util/VolumeInterface.js new file mode 100644 index 0000000..62e6da5 --- /dev/null +++ b/node_modules/discord.js/src/client/voice/util/VolumeInterface.js @@ -0,0 +1,86 @@ +const EventEmitter = require('events'); + +/** + * An interface class for volume transformation. + * @extends {EventEmitter} + */ +class VolumeInterface extends EventEmitter { + constructor({ volume = 1 } = {}) { + super(); + this.setVolume(volume); + } + + /** + * The current volume of the broadcast + * @readonly + * @type {number} + */ + get volume() { + return this._volume; + } + + /** + * The current volume of the broadcast in decibels + * @readonly + * @type {number} + */ + get volumeDecibels() { + return Math.log10(this._volume) * 20; + } + + /** + * The current volume of the broadcast from a logarithmic scale + * @readonly + * @type {number} + */ + get volumeLogarithmic() { + return Math.pow(this._volume, 1 / 1.660964); + } + + applyVolume(buffer, volume) { + volume = volume || this._volume; + if (volume === 1) return buffer; + + const out = Buffer.alloc(buffer.length); + for (let i = 0; i < buffer.length; i += 2) { + if (i >= buffer.length - 1) break; + const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i)))); + out.writeInt16LE(uint, i); + } + + return out; + } + + /** + * Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double. + * @param {number} volume The volume that you want to set + */ + setVolume(volume) { + /** + * Emitted when the volume of this interface changes. + * @event VolumeInterface#volumeChange + * @param {number} oldVolume The old volume of this interface + * @param {number} newVolume The new volume of this interface + */ + this.emit('volumeChange', this._volume, volume); + this._volume = volume; + } + + /** + * Set the volume in decibels. + * @param {number} db The decibels + */ + setVolumeDecibels(db) { + this.setVolume(Math.pow(10, db / 20)); + } + + /** + * Set the volume so that a perceived value of 0.5 is half the perceived volume etc. + * @param {number} value The value for the volume + */ + setVolumeLogarithmic(value) { + this.setVolume(Math.pow(value, 1.660964)); + } +} + +module.exports = VolumeInterface; diff --git a/node_modules/discord.js/src/client/websocket/WebSocketConnection.js b/node_modules/discord.js/src/client/websocket/WebSocketConnection.js new file mode 100644 index 0000000..4ea078b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/WebSocketConnection.js @@ -0,0 +1,509 @@ +const browser = typeof window !== 'undefined'; +const EventEmitter = require('events'); +const Constants = require('../../util/Constants'); +const zlib = require('zlib'); +const PacketManager = require('./packets/WebSocketPacketManager'); +const erlpack = (function findErlpack() { + try { + const e = require('erlpack'); + if (!e.pack) return null; + return e; + } catch (e) { + return null; + } +}()); + +const WebSocket = (function findWebSocket() { + if (browser) return window.WebSocket; // eslint-disable-line no-undef + try { + const uws = require('@discordjs/uws'); + process.emitWarning('uws support is being removed in the next version of discord.js', + 'DeprecationWarning', findWebSocket); + return uws; + } catch (e) { + return require('ws'); + } +}()); + +/** + * Abstracts a WebSocket connection with decoding/encoding for the Discord gateway. + * @private + */ +class WebSocketConnection extends EventEmitter { + /** + * @param {WebSocketManager} manager The WebSocket manager + * @param {string} gateway The WebSocket gateway to connect to + */ + constructor(manager, gateway) { + super(); + /** + * The WebSocket Manager of this connection + * @type {WebSocketManager} + */ + this.manager = manager; + + /** + * The client this belongs to + * @type {Client} + */ + this.client = manager.client; + + /** + * The WebSocket connection itself + * @type {WebSocket} + */ + this.ws = null; + + /** + * The current sequence of the WebSocket + * @type {number} + */ + this.sequence = -1; + + /** + * The current status of the client + * @type {Status} + */ + this.status = Constants.Status.IDLE; + + /** + * The Packet Manager of the connection + * @type {WebSocketPacketManager} + */ + this.packetManager = new PacketManager(this); + + /** + * The last time a ping was sent (a timestamp) + * @type {number} + */ + this.lastPingTimestamp = 0; + + /** + * Contains the rate limit queue and metadata + * @type {Object} + */ + this.ratelimit = { + queue: [], + remaining: 120, + total: 120, + time: 60e3, + resetTimer: null, + }; + this.connect(gateway); + + /** + * Events that are disabled (will not be processed) + * @type {Object} + */ + this.disabledEvents = {}; + + /** + * The sequence on WebSocket close + * @type {number} + */ + this.closeSequence = 0; + + /** + * Whether or not the WebSocket is expecting to be closed + * @type {boolean} + */ + this.expectingClose = false; + for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true; + } + + /** + * Causes the client to be marked as ready and emits the ready event. + * @returns {void} + */ + triggerReady() { + if (this.status === Constants.Status.READY) { + this.debug('Tried to mark self as ready, but already ready'); + return; + } + /** + * Emitted when the client becomes ready to start working. + * @event Client#ready + */ + this.status = Constants.Status.READY; + this.client.emit(Constants.Events.READY); + this.packetManager.handleQueue(); + } + + /** + * Checks whether the client is ready to be marked as ready. + * @returns {void} + */ + checkIfReady() { + if (this.status === Constants.Status.READY || this.status === Constants.Status.NEARLY) return false; + let unavailableGuilds = 0; + for (const guild of this.client.guilds.values()) { + if (!guild.available) unavailableGuilds++; + } + if (unavailableGuilds === 0) { + this.status = Constants.Status.NEARLY; + if (!this.client.options.fetchAllMembers) return this.triggerReady(); + // Fetch all members before marking self as ready + const promises = this.client.guilds.map(g => g.fetchMembers()); + Promise.all(promises) + .then(() => this.triggerReady()) + .catch(e => { + this.debug(`Failed to fetch all members before ready! ${e}`); + this.triggerReady(); + }); + } + return true; + } + + // Util + /** + * Emits a debug message. + * @param {string} message Debug message + * @returns {void} + */ + debug(message) { + if (message instanceof Error) message = message.stack; + return this.manager.debug(`[connection] ${message}`); + } + + /** + * Attempts to serialise data from the WebSocket. + * @param {string|Object} data Data to unpack + * @returns {Object} + */ + unpack(data) { + if (data instanceof ArrayBuffer) data = Buffer.from(new Uint8Array(data)); + + if (erlpack && typeof data !== 'string') return erlpack.unpack(data); + else if (data instanceof Buffer) data = zlib.inflateSync(data).toString(); + + return JSON.parse(data); + } + + /** + * Packs an object ready to be sent. + * @param {Object} data Data to pack + * @returns {string|Buffer} + */ + pack(data) { + return erlpack ? erlpack.pack(data) : JSON.stringify(data); + } + + /** + * Processes the current WebSocket queue. + */ + processQueue() { + if (this.ratelimit.remaining === 0) return; + if (this.ratelimit.queue.length === 0) return; + if (this.ratelimit.remaining === this.ratelimit.total) { + this.ratelimit.resetTimer = this.client.setTimeout(() => { + this.ratelimit.remaining = this.ratelimit.total; + this.processQueue(); + }, this.ratelimit.time); + } + while (this.ratelimit.remaining > 0) { + const item = this.ratelimit.queue.shift(); + if (!item) return; + this._send(item); + this.ratelimit.remaining--; + } + } + + /** + * Sends data, bypassing the queue. + * @param {Object} data Packet to send + * @returns {void} + */ + _send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); + return; + } + this.ws.send(this.pack(data)); + } + + /** + * Adds data to the queue to be sent. + * @param {Object} data Packet to send + * @returns {void} + */ + send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); + return; + } + this.ratelimit.queue.push(data); + this.processQueue(); + } + + /** + * Creates a connection to a gateway. + * @param {string} gateway The gateway to connect to + * @param {number} [after=0] How long to wait before connecting + * @param {boolean} [force=false] Whether or not to force a new connection even if one already exists + * @returns {boolean} + */ + connect(gateway = this.gateway, after = 0, force = false) { + if (after) return this.client.setTimeout(() => this.connect(gateway, 0, force), after); // eslint-disable-line + if (this.ws && !force) { + this.debug('WebSocket connection already exists'); + return false; + } else if (typeof gateway !== 'string') { + this.debug(`Tried to connect to an invalid gateway: ${gateway}`); + return false; + } + this.expectingClose = false; + this.gateway = gateway; + this.debug(`Connecting to ${gateway}`); + const ws = this.ws = new WebSocket(gateway); + if (browser) ws.binaryType = 'arraybuffer'; + ws.onmessage = this.onMessage.bind(this); + ws.onopen = this.onOpen.bind(this); + ws.onerror = this.onError.bind(this); + ws.onclose = this.onClose.bind(this); + this.status = Constants.Status.CONNECTING; + return true; + } + + /** + * Destroys the connection. + * @returns {boolean} + */ + destroy() { + const ws = this.ws; + if (!ws) { + this.debug('Attempted to destroy WebSocket but no connection exists!'); + return false; + } + this.heartbeat(-1); + this.expectingClose = true; + ws.close(1000); + this.packetManager.handleQueue(); + this.ws = null; + this.status = Constants.Status.DISCONNECTED; + this.ratelimit.remaining = this.ratelimit.total; + return true; + } + + /** + * Called whenever a message is received. + * @param {Event} event Event received + * @returns {boolean} + */ + onMessage(event) { + let data; + try { + data = this.unpack(event.data); + } catch (err) { + this.emit('debug', err); + } + return this.onPacket(data); + } + + /** + * Sets the current sequence of the connection. + * @param {number} s New sequence + */ + setSequence(s) { + this.sequence = s > this.sequence ? s : this.sequence; + } + + /** + * Called whenever a packet is received. + * @param {Object} packet Received packet + * @returns {boolean} + */ + onPacket(packet) { + if (!packet) { + this.debug('Received null packet'); + return false; + } + this.client.emit('raw', packet); + switch (packet.op) { + case Constants.OPCodes.HELLO: + return this.heartbeat(packet.d.heartbeat_interval); + case Constants.OPCodes.RECONNECT: + return this.reconnect(); + case Constants.OPCodes.INVALID_SESSION: + if (!packet.d) this.sessionID = null; + this.sequence = -1; + this.debug('Session invalidated -- will identify with a new session'); + return this.identify(packet.d ? 2500 : 0); + case Constants.OPCodes.HEARTBEAT_ACK: + return this.ackHeartbeat(); + case Constants.OPCodes.HEARTBEAT: + return this.heartbeat(); + default: + return this.packetManager.handle(packet); + } + } + + /** + * Called whenever a connection is opened to the gateway. + * @param {Event} event Received open event + */ + onOpen(event) { + if (event && event.target && event.target.url) this.gateway = event.target.url; + this.debug(`Connected to gateway ${this.gateway}`); + this.identify(); + } + + /** + * Causes a reconnection to the gateway. + */ + reconnect() { + this.debug('Attemping to reconnect in 5500ms...'); + /** + * Emitted whenever the client tries to reconnect to the WebSocket. + * @event Client#reconnecting + */ + this.client.emit(Constants.Events.RECONNECTING); + this.connect(this.gateway, 5500, true); + } + + /** + * Called whenever an error occurs with the WebSocket. + * @param {Error} error The error that occurred + */ + onError(error) { + if (error && error.message === 'uWs client connection error') { + this.reconnect(); + return; + } + /** + * Emitted whenever the client's WebSocket encounters a connection error. + * @event Client#error + * @param {Error} error The encountered error + */ + this.client.emit(Constants.Events.ERROR, error); + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * Called whenever a connection to the gateway is closed. + * @param {CloseEvent} event Close event that was received + */ + onClose(event) { + this.debug(`${this.expectingClose ? 'Client' : 'Server'} closed the WebSocket connection: ${event.code}`); + this.closeSequence = this.sequence; + // Reset the state before trying to fix anything + this.emit('close', event); + this.heartbeat(-1); + // Should we reconnect? + if (event.code === 1000 ? this.expectingClose : Constants.WSCodes[event.code]) { + this.expectingClose = false; + /** + * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. + * @event Client#disconnect + * @param {CloseEvent} event The WebSocket close event + */ + this.client.emit(Constants.Events.DISCONNECT, event); + this.debug(Constants.WSCodes[event.code]); + this.destroy(); + return; + } + this.expectingClose = false; + this.reconnect(); + } + + // Heartbeat + /** + * Acknowledges a heartbeat. + */ + ackHeartbeat() { + this.debug(`Heartbeat acknowledged, latency of ${Date.now() - this.lastPingTimestamp}ms`); + this.client._pong(this.lastPingTimestamp); + } + + /** + * Sends a heartbeat or sets an interval for sending heartbeats. + * @param {number} [time] If -1, clears the interval, any other number sets an interval + * If no value is given, a heartbeat will be sent instantly + */ + heartbeat(time) { + if (!isNaN(time)) { + if (time === -1) { + this.debug('Clearing heartbeat interval'); + this.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } else { + this.debug(`Setting a heartbeat interval for ${time}ms`); + this.heartbeatInterval = this.client.setInterval(() => this.heartbeat(), time); + } + return; + } + this.debug('Sending a heartbeat'); + this.lastPingTimestamp = Date.now(); + this.send({ + op: Constants.OPCodes.HEARTBEAT, + d: this.sequence, + }); + } + + // Identification + /** + * Identifies the client on a connection. + * @param {number} [after] How long to wait before identifying + * @returns {void} + */ + identify(after) { + if (after) return this.client.setTimeout(this.identify.bind(this), after); + return this.sessionID ? this.identifyResume() : this.identifyNew(); + } + + /** + * Identifies as a new connection on the gateway. + * @returns {void} + */ + identifyNew() { + if (!this.client.token) { + this.debug('No token available to identify a new session with'); + return; + } + // Clone the generic payload and assign the token + const d = Object.assign({ token: this.client.token }, this.client.options.ws); + + // Sharding stuff + const { shardId, shardCount } = this.client.options; + if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)]; + + // Send the payload + this.debug('Identifying as a new session'); + this.send({ op: Constants.OPCodes.IDENTIFY, d }); + } + + /** + * Resumes a session on the gateway. + * @returns {void} + */ + identifyResume() { + if (!this.sessionID) { + this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead'); + return this.identifyNew(); + } + this.debug(`Attempting to resume session ${this.sessionID}`); + + const d = { + token: this.client.token, + session_id: this.sessionID, + seq: this.sequence, + }; + + return this.send({ + op: Constants.OPCodes.RESUME, + d, + }); + } +} + +/** + * Encoding the WebSocket connections will use. + * @type {string} + */ +WebSocketConnection.ENCODING = erlpack ? 'etf' : 'json'; +WebSocketConnection.WebSocket = WebSocket; + +module.exports = WebSocketConnection; diff --git a/node_modules/discord.js/src/client/websocket/WebSocketManager.js b/node_modules/discord.js/src/client/websocket/WebSocketManager.js new file mode 100644 index 0000000..9ef073f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/WebSocketManager.js @@ -0,0 +1,90 @@ +const EventEmitter = require('events').EventEmitter; +const Constants = require('../../util/Constants'); +const WebSocketConnection = require('./WebSocketConnection'); + +/** + * WebSocket Manager of the client. + * @private + */ +class WebSocketManager extends EventEmitter { + constructor(client) { + super(); + /** + * The client that instantiated this WebSocketManager + * @type {Client} + */ + this.client = client; + + /** + * The WebSocket connection of this manager + * @type {?WebSocketConnection} + */ + this.connection = null; + } + + /** + * Sends a heartbeat on the available connection. + * @returns {void} + */ + heartbeat() { + if (!this.connection) return this.debug('No connection to heartbeat'); + return this.connection.heartbeat(); + } + + /** + * Emits a debug event. + * @param {string} message Debug message + * @returns {void} + */ + debug(message) { + return this.client.emit('debug', `[ws] ${message}`); + } + + /** + * Destroy the client. + * @returns {void} Whether or not destruction was successful + */ + destroy() { + if (!this.connection) { + this.debug('Attempted to destroy WebSocket but no connection exists!'); + return false; + } + return this.connection.destroy(); + } + + /** + * Send a packet on the available WebSocket. + * @param {Object} packet Packet to send + * @returns {void} + */ + send(packet) { + if (!this.connection) { + this.debug('No connection to websocket'); + return; + } + this.connection.send(packet); + } + + /** + * Connects the client to a gateway. + * @param {string} gateway The gateway to connect to + * @returns {boolean} + */ + connect(gateway) { + if (!this.connection) { + this.connection = new WebSocketConnection(this, gateway); + return true; + } + switch (this.connection.status) { + case Constants.Status.IDLE: + case Constants.Status.DISCONNECTED: + this.connection.connect(gateway, 5500); + return true; + default: + this.debug(`Couldn't connect to ${gateway} as the websocket is at state ${this.connection.status}`); + return false; + } + } +} + +module.exports = WebSocketManager; diff --git a/node_modules/discord.js/src/client/websocket/packets/WebSocketPacketManager.js b/node_modules/discord.js/src/client/websocket/packets/WebSocketPacketManager.js new file mode 100644 index 0000000..c84b16b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/WebSocketPacketManager.js @@ -0,0 +1,113 @@ +const Constants = require('../../../util/Constants'); + +const BeforeReadyWhitelist = [ + Constants.WSEvents.READY, + Constants.WSEvents.RESUMED, + Constants.WSEvents.GUILD_CREATE, + Constants.WSEvents.GUILD_DELETE, + Constants.WSEvents.GUILD_MEMBERS_CHUNK, + Constants.WSEvents.GUILD_MEMBER_ADD, + Constants.WSEvents.GUILD_MEMBER_REMOVE, +]; + +class WebSocketPacketManager { + constructor(connection) { + this.ws = connection; + this.handlers = {}; + this.queue = []; + + this.register(Constants.WSEvents.READY, require('./handlers/Ready')); + this.register(Constants.WSEvents.RESUMED, require('./handlers/Resumed')); + this.register(Constants.WSEvents.GUILD_CREATE, require('./handlers/GuildCreate')); + this.register(Constants.WSEvents.GUILD_DELETE, require('./handlers/GuildDelete')); + this.register(Constants.WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate')); + this.register(Constants.WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd')); + this.register(Constants.WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove')); + this.register(Constants.WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd')); + this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove')); + this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate')); + this.register(Constants.WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate')); + this.register(Constants.WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete')); + this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate')); + this.register(Constants.WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate')); + this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk')); + this.register(Constants.WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate')); + this.register(Constants.WSEvents.INVITE_CREATE, require('./handlers/InviteCreate')); + this.register(Constants.WSEvents.INVITE_DELETE, require('./handlers/InviteDelete')); + this.register(Constants.WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate')); + this.register(Constants.WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete')); + this.register(Constants.WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate')); + this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate')); + this.register(Constants.WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate')); + this.register(Constants.WSEvents.USER_UPDATE, require('./handlers/UserUpdate')); + this.register(Constants.WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate')); + this.register(Constants.WSEvents.USER_SETTINGS_UPDATE, require('./handlers/UserSettingsUpdate')); + this.register(Constants.WSEvents.USER_GUILD_SETTINGS_UPDATE, require('./handlers/UserGuildSettingsUpdate')); + this.register(Constants.WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate')); + this.register(Constants.WSEvents.TYPING_START, require('./handlers/TypingStart')); + this.register(Constants.WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate')); + this.register(Constants.WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete')); + this.register(Constants.WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate')); + this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk')); + this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate')); + this.register(Constants.WSEvents.GUILD_SYNC, require('./handlers/GuildSync')); + this.register(Constants.WSEvents.RELATIONSHIP_ADD, require('./handlers/RelationshipAdd')); + this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, require('./handlers/RelationshipRemove')); + this.register(Constants.WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd')); + this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove')); + this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE_EMOJI, require('./handlers/MessageReactionRemoveEmoji')); + this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll')); + this.register(Constants.WSEvents.WEBHOOKS_UPDATE, require('./handlers/WebhooksUpdate')); + } + + get client() { + return this.ws.client; + } + + register(event, Handler) { + this.handlers[event] = new Handler(this); + } + + handleQueue() { + this.queue.forEach((element, index) => { + this.handle(this.queue[index], true); + this.queue.splice(index, 1); + }); + } + + handle(packet, queue = false) { + if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) { + this.ws.client._pong(this.ws.client._pingTimestamp); + this.ws.lastHeartbeatAck = true; + this.ws.client.emit('debug', 'Heartbeat acknowledged'); + } else if (packet.op === Constants.OPCodes.HEARTBEAT) { + this.client.ws.send({ + op: Constants.OPCodes.HEARTBEAT, + d: this.client.ws.sequence, + }); + this.ws.client.emit('debug', 'Received gateway heartbeat'); + } + + if (this.ws.status === Constants.Status.RECONNECTING) { + this.ws.reconnecting = false; + this.ws.checkIfReady(); + } + + this.ws.setSequence(packet.s); + + if (this.ws.disabledEvents[packet.t] !== undefined) return false; + + if (this.ws.status !== Constants.Status.READY) { + if (BeforeReadyWhitelist.indexOf(packet.t) === -1) { + this.queue.push(packet); + return false; + } + } + + if (!queue && this.queue.length > 0) this.handleQueue(); + if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet); + return false; + } +} + +module.exports = WebSocketPacketManager; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/AbstractHandler.js b/node_modules/discord.js/src/client/websocket/packets/handlers/AbstractHandler.js new file mode 100644 index 0000000..c1c2a5a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/AbstractHandler.js @@ -0,0 +1,11 @@ +class AbstractHandler { + constructor(packetManager) { + this.packetManager = packetManager; + } + + handle(packet) { + return packet; + } +} + +module.exports = AbstractHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelCreate.js new file mode 100644 index 0000000..04cb298 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelCreate.js @@ -0,0 +1,17 @@ +const AbstractHandler = require('./AbstractHandler'); + +class ChannelCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.ChannelCreate.handle(data); + } +} + +/** + * Emitted whenever a channel is created. + * @event Client#channelCreate + * @param {Channel} channel The channel that was created + */ + +module.exports = ChannelCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelDelete.js new file mode 100644 index 0000000..b25f585 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelDelete.js @@ -0,0 +1,20 @@ +const AbstractHandler = require('./AbstractHandler'); + +const Constants = require('../../../../util/Constants'); + +class ChannelDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.ChannelDelete.handle(data); + if (response.channel) client.emit(Constants.Events.CHANNEL_DELETE, response.channel); + } +} + +/** + * Emitted whenever a channel is deleted. + * @event Client#channelDelete + * @param {Channel} channel The channel that was deleted + */ + +module.exports = ChannelDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelPinsUpdate.js new file mode 100644 index 0000000..16ffe1c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelPinsUpdate.js @@ -0,0 +1,37 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +/* +{ t: 'CHANNEL_PINS_UPDATE', + s: 666, + op: 0, + d: + { last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00', + channel_id: '314866471639044027' } } +*/ + +class ChannelPinsUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const channel = client.channels.get(data.channel_id); + const time = new Date(data.last_pin_timestamp); + if (channel && time) { + // Discord sends null for last_pin_timestamp if the last pinned message was removed + channel.lastPinTimestamp = time.getTime() || null; + + client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time); + } + } +} + +/** + * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, not much information + * can be provided easily here - you need to manually check the pins yourself. + * <warn>The `time` parameter will be a Unix Epoch Date object when there are no pins left.</warn> + * @event Client#channelPinsUpdate + * @param {Channel} channel The channel that the pins update occured in + * @param {Date} time The time when the last pinned message was pinned + */ + +module.exports = ChannelPinsUpdate; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelUpdate.js new file mode 100644 index 0000000..fa535b1 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/ChannelUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class ChannelUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.ChannelUpdate.handle(data); + } +} + +module.exports = ChannelUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanAdd.js new file mode 100644 index 0000000..60ce72d --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanAdd.js @@ -0,0 +1,23 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class GuildBanAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + const user = client.users.get(data.user.id); + if (guild && user) client.emit(Constants.Events.GUILD_BAN_ADD, guild, user); + } +} + +/** + * Emitted whenever a member is banned from a guild. + * @event Client#guildBanAdd + * @param {Guild} guild The guild that the ban occurred in + * @param {User} user The user that was banned + */ + +module.exports = GuildBanAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanRemove.js new file mode 100644 index 0000000..c4edbde --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildBanRemove.js @@ -0,0 +1,20 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildBanRemoveHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildBanRemove.handle(data); + } +} + +/** + * Emitted whenever a member is unbanned from a guild. + * @event Client#guildBanRemove + * @param {Guild} guild The guild that the unban occurred in + * @param {User} user The user that was unbanned + */ + +module.exports = GuildBanRemoveHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildCreate.js new file mode 100644 index 0000000..d7c1803 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildCreate.js @@ -0,0 +1,22 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + const guild = client.guilds.get(data.id); + if (guild) { + if (!guild.available && !data.unavailable) { + // A newly available guild + guild.setup(data); + this.packetManager.ws.checkIfReady(); + } + } else { + // A new guild + client.dataManager.newGuild(data); + } + } +} + +module.exports = GuildCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildDelete.js new file mode 100644 index 0000000..35e3c53 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildDelete.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class GuildDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.GuildDelete.handle(data); + if (response.guild) client.emit(Constants.Events.GUILD_DELETE, response.guild); + } +} + +/** + * Emitted whenever a guild is deleted/left. + * @event Client#guildDelete + * @param {Guild} guild The guild that was deleted + */ + +module.exports = GuildDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildEmojisUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildEmojisUpdate.js new file mode 100644 index 0000000..2906e74 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildEmojisUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildEmojisUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildEmojisUpdate.handle(data); + } +} + +module.exports = GuildEmojisUpdate; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js new file mode 100644 index 0000000..5adfb5b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const { Events } = require('../../../../util/Constants'); + +class GuildIntegrationsHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsHandler; + +/** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberAdd.js new file mode 100644 index 0000000..d4d122f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberAdd.js @@ -0,0 +1,17 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildMemberAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (guild) { + guild.memberCount++; + guild._addMember(data); + } + } +} + +module.exports = GuildMemberAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberRemove.js new file mode 100644 index 0000000..6ec1bfe --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberRemove.js @@ -0,0 +1,13 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildMemberRemoveHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildMemberRemove.handle(data); + } +} + +module.exports = GuildMemberRemoveHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberUpdate.js new file mode 100644 index 0000000..94ac71f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMemberUpdate.js @@ -0,0 +1,18 @@ +// ##untested handler## + +const AbstractHandler = require('./AbstractHandler'); + +class GuildMemberUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + const member = guild.members.get(data.user.id); + if (member) guild._updateMember(member, data); + } + } +} + +module.exports = GuildMemberUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMembersChunk.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMembersChunk.js new file mode 100644 index 0000000..4458644 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildMembersChunk.js @@ -0,0 +1,33 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); +// Uncomment in v12 +// const Collection = require('../../../../util/Collection'); + +class GuildMembersChunkHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (!guild) return; + + // Uncomment in v12 + // const members = new Collection(); + // + // for (const member of data.members) members.set(member.id, guild._addMember(member, false)); + + const members = data.members.map(member => guild._addMember(member, false)); + + client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members, guild); + + client.ws.lastHeartbeatAck = true; + } +} + +/** + * Emitted whenever a chunk of guild members is received (all members come from the same guild). + * @event Client#guildMembersChunk + * @param {GuildMember[]} members The members in the chunk + * @param {Guild} guild The guild related to the member chunk + */ + +module.exports = GuildMembersChunkHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleCreate.js new file mode 100644 index 0000000..8581d53 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleCreate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildRoleCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildRoleCreate.handle(data); + } +} + +module.exports = GuildRoleCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleDelete.js new file mode 100644 index 0000000..63439b0 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleDelete.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildRoleDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildRoleDelete.handle(data); + } +} + +module.exports = GuildRoleDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleUpdate.js new file mode 100644 index 0000000..6fbdc10 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildRoleUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildRoleUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildRoleUpdate.handle(data); + } +} + +module.exports = GuildRoleUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildSync.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildSync.js new file mode 100644 index 0000000..0b9f5aa --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildSync.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildSyncHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildSync.handle(data); + } +} + +module.exports = GuildSyncHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/GuildUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildUpdate.js new file mode 100644 index 0000000..70eff52 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/GuildUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class GuildUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.GuildUpdate.handle(data); + } +} + +module.exports = GuildUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/InviteCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteCreate.js new file mode 100644 index 0000000..00efb67 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteCreate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class InviteCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.InviteCreate.handle(data); + } +} + +module.exports = InviteCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/InviteDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteDelete.js new file mode 100644 index 0000000..0b8da42 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/InviteDelete.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class InviteDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.InviteDelete.handle(data); + } +} + +module.exports = InviteDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageCreate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageCreate.js new file mode 100644 index 0000000..beed34d --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageCreate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class MessageCreateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.MessageCreate.handle(data); + if (response.message) client.emit(Constants.Events.MESSAGE_CREATE, response.message); + } +} + +/** + * Emitted whenever a message is created. + * @event Client#message + * @param {Message} message The created message + */ + +module.exports = MessageCreateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDelete.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDelete.js new file mode 100644 index 0000000..043c70a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDelete.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class MessageDeleteHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const response = client.actions.MessageDelete.handle(data); + if (response.message) client.emit(Constants.Events.MESSAGE_DELETE, response.message); + } +} + +/** + * Emitted whenever a message is deleted. + * @event Client#messageDelete + * @param {Message} message The deleted message + */ + +module.exports = MessageDeleteHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDeleteBulk.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDeleteBulk.js new file mode 100644 index 0000000..db02df0 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageDeleteBulk.js @@ -0,0 +1,17 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageDeleteBulkHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageDeleteBulk.handle(data); + } +} + +/** + * Emitted whenever messages are deleted in bulk. + * @event Client#messageDeleteBulk + * @param {Collection<Snowflake, Message>} messages The deleted messages, mapped by their ID + */ + +module.exports = MessageDeleteBulkHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionAdd.js new file mode 100644 index 0000000..a58db70 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionAdd.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionAdd.handle(data); + } +} + +module.exports = MessageReactionAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemove.js new file mode 100644 index 0000000..cddde70 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemove.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionRemove extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionRemove.handle(data); + } +} + +module.exports = MessageReactionRemove; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js new file mode 100644 index 0000000..303da9c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionRemoveAll extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionRemoveAll.handle(data); + } +} + +module.exports = MessageReactionRemoveAll; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveEmoji.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveEmoji.js new file mode 100644 index 0000000..c16af74 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageReactionRemoveEmoji.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageReactionRemoveEmoji extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageReactionRemoveEmoji.handle(data); + } +} + +module.exports = MessageReactionRemoveEmoji; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/MessageUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageUpdate.js new file mode 100644 index 0000000..527632d --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/MessageUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class MessageUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.MessageUpdate.handle(data); + } +} + +module.exports = MessageUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/PresenceUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/PresenceUpdate.js new file mode 100644 index 0000000..8bcf659 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/PresenceUpdate.js @@ -0,0 +1,76 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); +const Util = require('../../../../util/Util'); + +class PresenceUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + let user = client.users.get(data.user.id); + const guild = client.guilds.get(data.guild_id); + + // Step 1 + if (!user) { + if (data.user.username) { + user = client.dataManager.newUser(data.user); + } else { + return; + } + } + + const oldUser = Util.cloneObject(user); + user.patch(data.user); + if (!user.equals(oldUser)) { + client.emit(Constants.Events.USER_UPDATE, oldUser, user); + } + + if (guild) { + let member = guild.members.get(user.id); + if (!member && data.status !== 'offline') { + member = guild._addMember({ + user, + roles: data.roles, + deaf: false, + mute: false, + }, false); + client.emit(Constants.Events.GUILD_MEMBER_AVAILABLE, member); + } + if (member) { + if (client.listenerCount(Constants.Events.PRESENCE_UPDATE) === 0) { + guild._setPresence(user.id, data); + return; + } + const oldMember = Util.cloneObject(member); + if (member.presence) { + oldMember.frozenPresence = Util.cloneObject(member.presence); + } + guild._setPresence(user.id, data); + client.emit(Constants.Events.PRESENCE_UPDATE, oldMember, member); + } else { + guild._setPresence(user.id, data); + } + } + } +} + +/** + * Emitted whenever a guild member's presence changes, or they change one of their details. + * @event Client#presenceUpdate + * @param {GuildMember} oldMember The member before the presence update + * @param {GuildMember} newMember The member after the presence update + */ + +/** + * Emitted whenever a user's details (e.g. username) are changed. + * @event Client#userUpdate + * @param {User} oldUser The user before the update + * @param {User} newUser The user after the update + */ + +/** + * Emitted whenever a member becomes available in a large guild. + * @event Client#guildMemberAvailable + * @param {GuildMember} member The member that became available + */ + +module.exports = PresenceUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/Ready.js b/node_modules/discord.js/src/client/websocket/packets/handlers/Ready.js new file mode 100644 index 0000000..3eb80a2 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/Ready.js @@ -0,0 +1,84 @@ +const AbstractHandler = require('./AbstractHandler'); + +const ClientUser = require('../../../../structures/ClientUser'); + +class ReadyHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + client.ws.heartbeat(); + + data.user.user_settings = data.user_settings; + data.user.user_guild_settings = data.user_guild_settings; + + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.readyAt = new Date(); + client.users.set(clientUser.id, clientUser); + + for (const guild of data.guilds) if (!client.guilds.has(guild.id)) client.dataManager.newGuild(guild); + for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM); + + for (const relation of data.relationships) { + const user = client.dataManager.newUser(relation.user); + if (relation.type === 1) { + client.user.friends.set(user.id, user); + } else if (relation.type === 2) { + client.user.blocked.set(user.id, user); + } + } + + data.presences = data.presences || []; + for (const presence of data.presences) { + client.dataManager.newUser(presence.user); + client._setPresence(presence.user.id, presence); + } + + if (data.notes) { + for (const user of Object.keys(data.notes)) { + let note = data.notes[user]; + if (!note.length) note = null; + + client.user.notes.set(user, note); + } + } + + if (!client.user.bot && client.options.sync) client.setInterval(client.syncGuilds.bind(client), 30000); + + if (!client.users.has('1')) { + client.dataManager.newUser({ + id: '1', + username: 'Clyde', + discriminator: '0000', + avatar: 'https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png', + bot: true, + status: 'online', + game: null, + verified: true, + }); + } + + const t = client.setTimeout(() => { + client.ws.connection.triggerReady(); + }, 1200 * data.guilds.length); + + const guildCount = data.guilds.length; + + if (client.getMaxListeners() !== 0) client.setMaxListeners(client.getMaxListeners() + guildCount); + + client.once('ready', () => { + client.syncGuilds(); + if (client.getMaxListeners() !== 0) client.setMaxListeners(client.getMaxListeners() - guildCount); + client.clearTimeout(t); + }); + + const ws = this.packetManager.ws; + + ws.sessionID = data.session_id; + client.emit('debug', `READY ${ws.sessionID}`); + ws.checkIfReady(); + } +} + +module.exports = ReadyHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipAdd.js b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipAdd.js new file mode 100644 index 0000000..122b4c5 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipAdd.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); + +class RelationshipAddHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + if (data.type === 1) { + client.fetchUser(data.id).then(user => { + client.user.friends.set(user.id, user); + }); + } else if (data.type === 2) { + client.fetchUser(data.id).then(user => { + client.user.blocked.set(user.id, user); + }); + } + } +} + +module.exports = RelationshipAddHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipRemove.js b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipRemove.js new file mode 100644 index 0000000..b57326a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/RelationshipRemove.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); + +class RelationshipRemoveHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + if (data.type === 2) { + if (client.user.blocked.has(data.id)) { + client.user.blocked.delete(data.id); + } + } else if (data.type === 1) { + if (client.user.friends.has(data.id)) { + client.user.friends.delete(data.id); + } + } + } +} + +module.exports = RelationshipRemoveHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/Resumed.js b/node_modules/discord.js/src/client/websocket/packets/handlers/Resumed.js new file mode 100644 index 0000000..a0a51c0 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/Resumed.js @@ -0,0 +1,26 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class ResumedHandler extends AbstractHandler { + handle() { + const client = this.packetManager.client; + const ws = client.ws.connection; + + ws.status = Constants.Status.READY; + this.packetManager.handleQueue(); + + const replayed = ws.sequence - ws.closeSequence; + + ws.debug(`RESUMED | replayed ${replayed} events.`); + client.emit(Constants.Events.RESUME, replayed); + ws.heartbeat(); + } +} + +/** + * Emitted whenever a WebSocket resumes. + * @event Client#resume + * @param {number} replayed The number of events that were replayed + */ + +module.exports = ResumedHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/TypingStart.js b/node_modules/discord.js/src/client/websocket/packets/handlers/TypingStart.js new file mode 100644 index 0000000..a7f5a36 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/TypingStart.js @@ -0,0 +1,68 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class TypingStartHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const channel = client.channels.get(data.channel_id); + const user = client.users.get(data.user_id); + const timestamp = new Date(data.timestamp * 1000); + + if (channel && user) { + if (channel.type === 'voice') { + client.emit(Constants.Events.WARN, `Discord sent a typing packet to voice channel ${channel.id}`); + return; + } + if (channel._typing.has(user.id)) { + const typing = channel._typing.get(user.id); + typing.lastTimestamp = timestamp; + typing.resetTimeout(tooLate(channel, user)); + } else { + channel._typing.set(user.id, new TypingData(client, timestamp, timestamp, tooLate(channel, user))); + client.emit(Constants.Events.TYPING_START, channel, user); + } + } + } +} + +class TypingData { + constructor(client, since, lastTimestamp, _timeout) { + this.client = client; + this.since = since; + this.lastTimestamp = lastTimestamp; + this._timeout = _timeout; + } + + resetTimeout(_timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = _timeout; + } + + get elapsedTime() { + return Date.now() - this.since; + } +} + +function tooLate(channel, user) { + return channel.client.setTimeout(() => { + channel.client.emit(Constants.Events.TYPING_STOP, channel, user, channel._typing.get(user.id)); + channel._typing.delete(user.id); + }, 6000); +} + +/** + * Emitted whenever a user starts typing in a channel. + * @event Client#typingStart + * @param {Channel} channel The channel the user started typing in + * @param {User} user The user that started typing + */ + +/** + * Emitted whenever a user stops typing in a channel. + * @event Client#typingStop + * @param {Channel} channel The channel the user stopped typing in + * @param {User} user The user that stopped typing + */ + +module.exports = TypingStartHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js new file mode 100644 index 0000000..90bca4c --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js @@ -0,0 +1,21 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); +const ClientUserGuildSettings = require('../../../../structures/ClientUserGuildSettings'); + +class UserGuildSettingsUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const settings = client.user.guildSettings.get(packet.d.guild_id); + if (settings) settings.patch(packet.d); + else client.user.guildSettings.set(packet.d.guild_id, new ClientUserGuildSettings(packet.d, client)); + client.emit(Constants.Events.USER_GUILD_SETTINGS_UPDATE, client.user.guildSettings.get(packet.d.guild_id)); + } +} + +/** + * Emitted whenever the client user's settings update. + * @event Client#clientUserGuildSettingsUpdate + * @param {ClientUserGuildSettings} clientUserGuildSettings The new client user guild settings + */ + +module.exports = UserGuildSettingsUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserNoteUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserNoteUpdate.js new file mode 100644 index 0000000..1e4777a --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserNoteUpdate.js @@ -0,0 +1,12 @@ +const AbstractHandler = require('./AbstractHandler'); + +class UserNoteUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + client.actions.UserNoteUpdate.handle(data); + } +} + +module.exports = UserNoteUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserSettingsUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserSettingsUpdate.js new file mode 100644 index 0000000..903b64b --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserSettingsUpdate.js @@ -0,0 +1,18 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class UserSettingsUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + client.user.settings.patch(packet.d); + client.emit(Constants.Events.USER_SETTINGS_UPDATE, client.user.settings); + } +} + +/** + * Emitted when the client user's settings update. + * @event Client#clientUserSettingsUpdate + * @param {ClientUserSettings} clientUserSettings The new client user settings + */ + +module.exports = UserSettingsUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/UserUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/UserUpdate.js new file mode 100644 index 0000000..bc34f34 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/UserUpdate.js @@ -0,0 +1,11 @@ +const AbstractHandler = require('./AbstractHandler'); + +class UserUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.actions.UserUpdate.handle(data); + } +} + +module.exports = UserUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceServerUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceServerUpdate.js new file mode 100644 index 0000000..97885d6 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceServerUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); + +/* +{ + "token": "my_token", + "guild_id": "41771983423143937", + "endpoint": "smart.loyal.discord.gg" +} +*/ + +class VoiceServerUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + client.emit('self.voiceServer', data); + } +} + +module.exports = VoiceServerUpdate; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceStateUpdate.js new file mode 100644 index 0000000..2b0b40f --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -0,0 +1,53 @@ +const AbstractHandler = require('./AbstractHandler'); + +const Constants = require('../../../../util/Constants'); +const Util = require('../../../../util/Util'); + +class VoiceStateUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + + const guild = client.guilds.get(data.guild_id); + if (guild) { + const member = guild.members.get(data.user_id); + if (member) { + const oldVoiceChannelMember = Util.cloneObject(member); + if (member.voiceChannel && member.voiceChannel.id !== data.channel_id) { + member.voiceChannel.members.delete(oldVoiceChannelMember.id); + } + + // If the member left the voice channel, unset their speaking property + if (!data.channel_id) member.speaking = null; + + if (member.user.id === client.user.id) { + client.emit('self.voiceStateUpdate', data); + } + + const newChannel = client.channels.get(data.channel_id); + if (newChannel) { + newChannel.members.set(member.id, member); + member.guild.channels.set(data.channel_id, newChannel); + } + + member.serverMute = data.mute; + member.serverDeaf = data.deaf; + member.selfMute = data.self_mute; + member.selfDeaf = data.self_deaf; + member.selfStream = data.self_stream || false; + member.voiceSessionID = data.session_id; + member.voiceChannelID = data.channel_id; + client.emit(Constants.Events.VOICE_STATE_UPDATE, oldVoiceChannelMember, member); + } + } + } +} + +/** + * Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * @event Client#voiceStateUpdate + * @param {GuildMember} oldMember The member before the voice state update + * @param {GuildMember} newMember The member after the voice state update + */ + +module.exports = VoiceStateUpdateHandler; diff --git a/node_modules/discord.js/src/client/websocket/packets/handlers/WebhooksUpdate.js b/node_modules/discord.js/src/client/websocket/packets/handlers/WebhooksUpdate.js new file mode 100644 index 0000000..7ed2721 --- /dev/null +++ b/node_modules/discord.js/src/client/websocket/packets/handlers/WebhooksUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const { Events } = require('../../../../util/Constants'); + +class WebhooksUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const channel = client.channels.get(data.channel_id); + if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); + } +} + +/** + * Emitted whenever a guild text channel has its webhooks changed. + * @event Client#webhookUpdate + * @param {TextChannel} channel The channel that had a webhook update + */ + +module.exports = WebhooksUpdate; diff --git a/node_modules/discord.js/src/index.js b/node_modules/discord.js/src/index.js new file mode 100644 index 0000000..f1fe01f --- /dev/null +++ b/node_modules/discord.js/src/index.js @@ -0,0 +1,71 @@ +const Util = require('./util/Util'); + +module.exports = { + // "Root" classes (starting points) + Client: require('./client/Client'), + Shard: require('./sharding/Shard'), + ShardClientUtil: require('./sharding/ShardClientUtil'), + ShardingManager: require('./sharding/ShardingManager'), + WebhookClient: require('./client/WebhookClient'), + + // Utilities + BitField: require('./util/BitField'), + Collection: require('./util/Collection'), + Constants: require('./util/Constants'), + DiscordAPIError: require('./client/rest/DiscordAPIError'), + EvaluatedPermissions: require('./util/Permissions'), + MessageFlags: require('./util/MessageFlags'), + Permissions: require('./util/Permissions'), + Snowflake: require('./util/Snowflake'), + SnowflakeUtil: require('./util/Snowflake'), + SystemChannelFlags: require('./util/SystemChannelFlags'), + Util: Util, + util: Util, + version: require('../package').version, + + // Shortcuts to Util methods + escapeMarkdown: Util.escapeMarkdown, + fetchRecommendedShards: Util.fetchRecommendedShards, + resolveString: Util.resolveString, + splitMessage: Util.splitMessage, + + // Structures + Attachment: require('./structures/Attachment'), + CategoryChannel: require('./structures/CategoryChannel'), + Channel: require('./structures/Channel'), + ClientUser: require('./structures/ClientUser'), + ClientUserSettings: require('./structures/ClientUserSettings'), + Collector: require('./structures/interfaces/Collector'), + DMChannel: require('./structures/DMChannel'), + Emoji: require('./structures/Emoji'), + Game: require('./structures/Presence').Game, + GroupDMChannel: require('./structures/GroupDMChannel'), + Guild: require('./structures/Guild'), + GuildAuditLogs: require('./structures/GuildAuditLogs'), + GuildChannel: require('./structures/GuildChannel'), + GuildMember: require('./structures/GuildMember'), + Integration: require('./structures/Integration'), + Invite: require('./structures/Invite'), + Message: require('./structures/Message'), + MessageAttachment: require('./structures/MessageAttachment'), + MessageCollector: require('./structures/MessageCollector'), + MessageEmbed: require('./structures/MessageEmbed'), + MessageMentions: require('./structures/MessageMentions'), + MessageReaction: require('./structures/MessageReaction'), + NewsChannel: require('./structures/NewsChannel'), + OAuth2Application: require('./structures/OAuth2Application'), + ClientOAuth2Application: require('./structures/OAuth2Application'), + PartialGuild: require('./structures/PartialGuild'), + PartialGuildChannel: require('./structures/PartialGuildChannel'), + PermissionOverwrites: require('./structures/PermissionOverwrites'), + Presence: require('./structures/Presence').Presence, + ReactionEmoji: require('./structures/ReactionEmoji'), + ReactionCollector: require('./structures/ReactionCollector'), + RichEmbed: require('./structures/RichEmbed'), + Role: require('./structures/Role'), + StoreChannel: require('./structures/StoreChannel'), + TextChannel: require('./structures/TextChannel'), + User: require('./structures/User'), + VoiceChannel: require('./structures/VoiceChannel'), + Webhook: require('./structures/Webhook'), +}; 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..b6fddb4 --- /dev/null +++ b/node_modules/discord.js/src/sharding/Shard.js @@ -0,0 +1,282 @@ +const childProcess = require('child_process'); +const EventEmitter = require('events'); +const path = require('path'); +const Util = require('../util/Util'); + +/** + * Represents a Shard spawned by the ShardingManager. + */ +class Shard extends EventEmitter { + /** + * @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 = []) { + super(); + /** + * 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, + }); + + /** + * Whether the shard's {@link Client} is ready + * @type {boolean} + */ + this.ready = false; + + this._evals = new Map(); + this._fetches = new Map(); + + /** + * Listener function for the {@link ChildProcess}' `exit` event + * @type {Function} + * @private + */ + this._exitListener = this._handleExit.bind(this, undefined); + + /** + * Process of the shard + * @type {ChildProcess} + */ + this.process = null; + + this.spawn(args); + } + + /** + * Forks a child process for the shard. + * <warn>You should not need to call this manually.</warn> + * @param {Array} [args=this.manager.args] Command line arguments to pass to the script + * @param {Array} [execArgv=this.manager.execArgv] Command line arguments to pass to the process executable + * @returns {ChildProcess} + */ + spawn(args = this.manager.args, execArgv = this.manager.execArgv) { + this.process = childProcess.fork(path.resolve(this.manager.file), args, { + env: this.env, execArgv, + }) + .on('exit', this._exitListener) + .on('message', this._handleMessage.bind(this)); + + /** + * Emitted upon the creation of the shard's child process. + * @event Shard#spawn + * @param {ChildProcess} process Child process that was created + */ + this.emit('spawn', this.process); + + return new Promise((resolve, reject) => { + this.once('ready', resolve); + this.once('disconnect', () => reject(new Error(`Shard ${this.id}'s Client disconnected before becoming ready.`))); + this.once('death', () => reject(new Error(`Shard ${this.id}'s process exited before its Client became ready.`))); + setTimeout(() => reject(new Error(`Shard ${this.id}'s Client took too long to become ready.`)), 30000); + }).then(() => this.process); + } + + /** + * Immediately kills the shard's process and does not restart it. + */ + kill() { + this.process.removeListener('exit', this._exitListener); + this.process.kill(); + this._handleExit(false); + } + + /** + * Kills and restarts the shard's process. + * @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds) + * @returns {Promise<ChildProcess>} + */ + respawn(delay = 500) { + this.kill(); + if (delay > 0) return Util.delayFor(delay).then(() => this.spawn()); + return this.spawn(); + } + + /** + * 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) => { + this.process.send(message, err => { + if (err) reject(err); else resolve(this); + }); + }); + } + + /** + * 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(Util.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 ready + if (message._ready) { + this.ready = true; + /** + * Emitted upon the shard's {@link Client#ready} event. + * @event Shard#ready + */ + this.emit('ready'); + return; + } + + // Shard has disconnected + if (message._disconnect) { + this.ready = false; + /** + * Emitted upon the shard's {@link Client#disconnect} event. + * @event Shard#disconnect + */ + this.emit('disconnect'); + return; + } + + // Shard is attempting to reconnect + if (message._reconnecting) { + this.ready = false; + /** + * Emitted upon the shard's {@link Client#reconnecting} event. + * @event Shard#reconnecting + */ + this.emit('reconnecting'); + return; + } + + // 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: Util.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: Util.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); + + /** + * Emitted upon recieving a message from the child process. + * @event Shard#message + * @param {*} message Message that was received + */ + this.emit('message', message); + } + + /** + * Handles the shard's process exiting. + * @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again + * @private + */ + _handleExit(respawn = this.manager.respawn) { + /** + * Emitted upon the shard's child process exiting. + * @event Shard#death + * @param {ChildProcess} process Child process that exited + */ + this.emit('death', this.process); + + this.process = null; + this._evals.clear(); + this._fetches.clear(); + + if (respawn) this.manager.createShard(this.id); + } +} + +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..28571f1 --- /dev/null +++ b/node_modules/discord.js/src/sharding/ShardClientUtil.js @@ -0,0 +1,146 @@ +const Util = require('../util/Util'); + +/** + * Helper class for sharded clients spawned as a child process, such as from a ShardingManager. + */ +class ShardClientUtil { + /** + * @param {Client} client The client of the current shard + */ + constructor(client) { + this.client = client; + process.on('message', this._handleMessage.bind(this)); + client.on('ready', () => { process.send({ _ready: true }); }); + client.on('disconnect', () => { process.send({ _disconnect: true }); }); + client.on('reconnecting', () => { process.send({ _reconnecting: true }); }); + } + + /** + * 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) => { + process.send(message, err => { + if (err) reject(err); else resolve(); + }); + }); + } + + /** + * 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(Util.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(Util.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: Util.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 The 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..5b07299 --- /dev/null +++ b/node_modules/discord.js/src/sharding/ShardingManager.js @@ -0,0 +1,220 @@ +const path = require('path'); +const fs = require('fs'); +const EventEmitter = require('events').EventEmitter; +const Shard = require('./Shard'); +const Collection = require('../util/Collection'); +const Util = require('../util/Util'); + +/** + * 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 = Util.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; + + /** + * Arguments for the shard's process executable + * @type {?string[]} + */ + this.execArgv = options.execArgv; + + /** + * 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=7500] How long to wait in between spawning each shard (in milliseconds) + * @returns {Promise<Collection<number, Shard>>} + */ + spawn(amount = this.totalShards, delay = 7500) { + if (amount === 'auto') { + return Util.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); + } + + /** + * Kills all running shards and respawns them. + * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds) + * @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it + * (in milliseconds) + * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another + * @param {number} [currentShardIndex=0] The shard index to start respawning at + * @returns {Promise<Collection<number, Shard>>} + */ + respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true, currentShardIndex = 0) { + let s = 0; + const shard = this.shards.get(currentShardIndex); + const promises = [shard.respawn(respawnDelay, waitForReady)]; + if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay)); + return Promise.all(promises).then(() => { + if (++currentShardIndex === this.shards.size) return this.shards; + return this.respawnAll(shardDelay, respawnDelay, waitForReady, currentShardIndex); + }); + } +} + +module.exports = ShardingManager; diff --git a/node_modules/discord.js/src/structures/Attachment.js b/node_modules/discord.js/src/structures/Attachment.js new file mode 100644 index 0000000..216b61c --- /dev/null +++ b/node_modules/discord.js/src/structures/Attachment.js @@ -0,0 +1,75 @@ +/** + * Represents an attachment in a message. + * @param {BufferResolvable|Stream} file The file + * @param {string} [name] The name of the file, if any + */ +class Attachment { + constructor(file, name) { + this.file = null; + if (name) this.setAttachment(file, name); + else this._attach(file); + } + + /** + * The name of the file + * @type {?string} + * @readonly + */ + get name() { + return this.file.name; + } + + /** + * The file + * @type {?BufferResolvable|Stream} + * @readonly + */ + get attachment() { + return this.file.attachment; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} file The file + * @param {string} name The name of the file + * @returns {Attachment} This attachment + */ + setAttachment(file, name) { + this.file = { attachment: file, name }; + return this; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} attachment The file + * @returns {Attachment} This attachment + */ + setFile(attachment) { + this.file = { attachment }; + return this; + } + + /** + * Set the name of this attachment. + * @param {string} name The name of the image + * @returns {Attachment} This attachment + */ + setName(name) { + this.file.name = name; + return this; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} file The file + * @param {string} name The name of the file + * @returns {void} + * @private + */ + _attach(file, name) { + if (typeof file === 'string') this.file = file; + else this.setAttachment(file, name); + } +} + +module.exports = Attachment; diff --git a/node_modules/discord.js/src/structures/CategoryChannel.js b/node_modules/discord.js/src/structures/CategoryChannel.js new file mode 100644 index 0000000..9ba263a --- /dev/null +++ b/node_modules/discord.js/src/structures/CategoryChannel.js @@ -0,0 +1,22 @@ +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild category channel on Discord. + * @extends {GuildChannel} + */ +class CategoryChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'category'; + } + /** + * The channels that are part of this category + * @type {?Collection<Snowflake, GuildChannel>} + * @readonly + */ + get children() { + return this.guild.channels.filter(c => c.parentID === this.id); + } +} + +module.exports = CategoryChannel; 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..732945d --- /dev/null +++ b/node_modules/discord.js/src/structures/Channel.js @@ -0,0 +1,78 @@ +const Snowflake = require('../util/Snowflake'); + +/** + * 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 + * * `category` - a guild category channel + * * `news` - a guild news channel + * * `store` - a guild store channel + * @type {string} + */ + this.type = null; + + /** + * Whether the channel has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The unique ID of the channel + * @type {Snowflake} + */ + this.id = data.id; + } + + /** + * The timestamp the channel was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * 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(console.log) + * .catch(console.error); + */ + delete() { + return this.client.rest.methods.deleteChannel(this); + } +} + +module.exports = Channel; 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..4ae8c6e --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUser.js @@ -0,0 +1,447 @@ +const User = require('./User'); +const Collection = require('../util/Collection'); +const ClientUserSettings = require('./ClientUserSettings'); +const ClientUserGuildSettings = require('./ClientUserGuildSettings'); +const Constants = require('../util/Constants'); +const util = require('util'); + +/** + * 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 + * <warn>This is only filled when using a user account.</warn> + * @type {?string} + * @deprecated + */ + 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<Snowflake, User>} + * @deprecated + */ + 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<Snowflake, User>} + * @deprecated + */ + 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<Snowflake, string>} + * @deprecated + */ + this.notes = new Collection(); + + /** + * If the user has Discord premium (nitro) + * <warn>This is only filled when using a user account.</warn> + * @type {?boolean} + * @deprecated + */ + this.premium = typeof data.premium === 'boolean' ? data.premium : null; + + /** + * If the user has MFA enabled on their account + * @type {boolean} + */ + this.mfaEnabled = data.mfa_enabled; + + /** + * If the user has ever used a mobile device on Discord + * <warn>This is only filled when using a user account.</warn> + * @type {?boolean} + * @deprecated + */ + this.mobile = typeof data.mobile === 'boolean' ? data.mobile : null; + + /** + * Various settings for this user + * <warn>This is only filled when using a user account.</warn> + * @type {?ClientUserSettings} + * @deprecated + */ + this.settings = data.user_settings ? new ClientUserSettings(this, data.user_settings) : null; + + /** + * All of the user's guild settings + * <warn>This is only filled when using a user account</warn> + * @type {Collection<Snowflake, ClientUserGuildSettings>} + * @deprecated + */ + this.guildSettings = new Collection(); + if (data.user_guild_settings) { + for (const settings of data.user_guild_settings) { + this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(settings, this.client)); + } + } + } + + 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>} + * @deprecated + * @example + * // Set email + * client.user.setEmail('[email protected]', '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>} + * @deprecated + * @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) { + return this.client.resolver.resolveImage(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 + * @property {?ActivityType|number} [game.type] Type of the activity + */ + + /** + * Sets the full presence of the client user. + * @param {PresenceData} data Data for the presence + * @returns {Promise<ClientUser>} + * @example + * // Set the client user's presence + * client.user.setPresence({ game: { name: 'with discord.js' }, status: 'idle' }) + * .then(console.log) + * .catch(console.error); + */ + 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'); + if (this.bot) { + status = data.status; + } else { + this.settings.update(Constants.UserSettingsMap.status, data.status); + status = 'invisible'; + } + } + + if (data.game) { + game = data.game; + game.type = game.url && typeof game.type === 'undefined' ? 1 : game.type || 0; + if (typeof game.type === 'string') { + game.type = Constants.ActivityTypes.indexOf(game.type.toUpperCase()); + } + } else if (typeof data.game !== 'undefined') { + game = null; + } + + 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>} + * @example + * // Set the client user's status + * client.user.setStatus('idle') + * .then(console.log) + * .catch(console.error); + */ + 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>} + * @deprecated + */ + setGame(game, streamingURL) { + if (!game) return this.setPresence({ game: null }); + return this.setPresence({ + game: { + name: game, + url: streamingURL, + }, + }); + } + + /** + * Sets the activity the client user is playing. + * @param {?string} name Activity being played + * @param {Object} [options] Options for setting the activity + * @param {string} [options.url] Twitch stream URL + * @param {ActivityType|number} [options.type] Type of the activity + * @returns {Promise<Presence>} + * @example + * client.user.setActivity('YouTube', { type: 'WATCHING' }) + * .then(presence => console.log(`Activity set to ${presence.game ? presence.game.name : 'none'}`)) + * .catch(console.error); + */ + setActivity(name, { url, type } = {}) { + if (!name) return this.setPresence({ game: null }); + return this.setPresence({ + game: { name, type, url }, + }).then(clientUser => clientUser.presence); + } + + /** + * 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. + * <warn>This is only available when using a user account.</warn> + * @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 {GuildResolvable} [options.guild] Limit the search to a specific guild + * @returns {Promise<Message[]>} + * @deprecated + * @example + * // Fetch mentions + * client.user.fetchMentions() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch mentions from a guild + * client.user.fetchMentions({ guild: '222078108977594368' }) + * .then(console.log) + * .catch(console.error); + */ + fetchMentions(options = {}) { + 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 + * @deprecated + */ + 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 + * @deprecated + */ + removeFriend(user) { + user = this.client.resolver.resolveUser(user); + return this.client.rest.methods.removeFriend(user); + } + + /** + * Creates a guild. + * <warn>This is only available to bots in less than 10 guilds and user accounts.</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 (typeof icon === 'string' && icon.startsWith('data:')) { + return this.client.rest.methods.createGuild({ name, icon, region }); + } else { + return this.client.resolver.resolveImage(icon).then(data => + this.client.rest.methods.createGuild({ name, icon: data, region }) + ); + } + } + + /** + * An object containing either a user or access token, and an optional nickname. + * @typedef {Object} GroupDMRecipientOptions + * @property {UserResolvable|Snowflake} [user] User to add to the Group DM + * (only available if a user is creating the DM) + * @property {string} [accessToken] Access token to use to add a user to the Group DM + * (only available if a bot is creating the DM) + * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) + */ + + /** + * Creates a Group DM. + * @param {GroupDMRecipientOptions[]} recipients The recipients + * @returns {Promise<GroupDMChannel>} + * @example + * // Create a Group DM with a token provided from OAuth + * client.user.createGroupDM([{ + * user: '66564597481480192', + * accessToken: token + * }]) + * .then(console.log) + * .catch(console.error); + */ + createGroupDM(recipients) { + return this.client.rest.methods.createGroupDM({ + recipients: recipients.map(u => this.client.resolver.resolveUserID(u.user)), + accessTokens: recipients.map(u => u.accessToken), + nicks: recipients.reduce((o, r) => { + if (r.nick) o[r.user ? r.user.id : r.id] = r.nick; + return o; + }, {}), + }); + } + + /** + * Accepts an invite to join a guild. + * <warn>This is only available when using a user account.</warn> + * @param {Invite|string} invite Invite or code to accept + * @returns {Promise<Guild>} Joined guild + * @deprecated + */ + acceptInvite(invite) { + return this.client.rest.methods.acceptInvite(invite); + } +} + +ClientUser.prototype.acceptInvite = + util.deprecate(ClientUser.prototype.acceptInvite, 'ClientUser#acceptInvite: userbot methods will be removed'); + +ClientUser.prototype.setGame = + util.deprecate(ClientUser.prototype.setGame, 'ClientUser#setGame: use ClientUser#setActivity instead'); + +ClientUser.prototype.addFriend = + util.deprecate(ClientUser.prototype.addFriend, 'ClientUser#addFriend: userbot methods will be removed'); + +ClientUser.prototype.removeFriend = + util.deprecate(ClientUser.prototype.removeFriend, 'ClientUser#removeFriend: userbot methods will be removed'); + +ClientUser.prototype.setPassword = + util.deprecate(ClientUser.prototype.setPassword, 'ClientUser#setPassword: userbot methods will be removed'); + +ClientUser.prototype.setEmail = + util.deprecate(ClientUser.prototype.setEmail, 'ClientUser#setEmail: userbot methods will be removed'); + +ClientUser.prototype.fetchMentions = + util.deprecate(ClientUser.prototype.fetchMentions, 'ClientUser#fetchMentions: userbot methods will be removed'); + +module.exports = ClientUser; diff --git a/node_modules/discord.js/src/structures/ClientUserChannelOverride.js b/node_modules/discord.js/src/structures/ClientUserChannelOverride.js new file mode 100644 index 0000000..93efa45 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUserChannelOverride.js @@ -0,0 +1,30 @@ +const Constants = require('../util/Constants'); + +/** + * A wrapper around the ClientUser's channel overrides. + */ +class ClientUserChannelOverride { + constructor(data) { + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + * @returns {void} + * @private + */ + patch(data) { + for (const key of Object.keys(Constants.UserChannelOverrideMap)) { + const value = Constants.UserChannelOverrideMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } +} + +module.exports = ClientUserChannelOverride; diff --git a/node_modules/discord.js/src/structures/ClientUserGuildSettings.js b/node_modules/discord.js/src/structures/ClientUserGuildSettings.js new file mode 100644 index 0000000..5a28747 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUserGuildSettings.js @@ -0,0 +1,60 @@ +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const ClientUserChannelOverride = require('./ClientUserChannelOverride'); + +/** + * A wrapper around the ClientUser's guild settings. + */ +class ClientUserGuildSettings { + constructor(data, client) { + /** + * The client that created the instance of the ClientUserGuildSettings + * @name ClientUserGuildSettings#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + /** + * The ID of the guild this settings are for + * @type {Snowflake} + */ + this.guildID = data.guild_id; + this.channelOverrides = new Collection(); + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + * @returns {void} + * @private + */ + patch(data) { + for (const key of Object.keys(Constants.UserGuildSettingsMap)) { + const value = Constants.UserGuildSettingsMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (key === 'channel_overrides') { + for (const channel of data[key]) { + this.channelOverrides.set(channel.channel_id, + new ClientUserChannelOverride(channel)); + } + } else if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } + + /** + * Update a specific property of the guild settings. + * @param {string} name Name of property + * @param {value} value Value to patch + * @returns {Promise<Object>} + */ + update(name, value) { + return this.client.rest.methods.patchClientUserGuildSettings(this.guildID, { [name]: value }); + } +} + +module.exports = ClientUserGuildSettings; diff --git a/node_modules/discord.js/src/structures/ClientUserSettings.js b/node_modules/discord.js/src/structures/ClientUserSettings.js new file mode 100644 index 0000000..6b18c03 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUserSettings.js @@ -0,0 +1,80 @@ +const Constants = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * A wrapper around the ClientUser's settings. + */ +class ClientUserSettings { + constructor(user, data) { + this.user = user; + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + * @returns {void} + * @private + */ + patch(data) { + for (const key of Object.keys(Constants.UserSettingsMap)) { + const value = Constants.UserSettingsMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } + + /** + * Update a specific property of of user settings. + * @param {string} name Name of property + * @param {*} value Value to patch + * @returns {Promise<Object>} + */ + update(name, value) { + return this.user.client.rest.methods.patchUserSettings({ [name]: value }); + } + + /** + * Sets the position at which this guild will appear in the Discord client. + * @param {Guild} guild The guild to move + * @param {number} position Absolute or relative position + * @param {boolean} [relative=false] Whether to position relatively or absolutely + * @returns {Promise<Guild>} + */ + setGuildPosition(guild, position, relative) { + const temp = Object.assign([], this.guildPositions); + Util.moveElementInArray(temp, guild.id, position, relative); + return this.update('guild_positions', temp).then(() => guild); + } + + /** + * Add a guild to the list of restricted guilds. + * @param {Guild} guild The guild to add + * @returns {Promise<Guild>} + */ + addRestrictedGuild(guild) { + const temp = Object.assign([], this.restrictedGuilds); + if (temp.includes(guild.id)) return Promise.reject(new Error('Guild is already restricted')); + temp.push(guild.id); + return this.update('restricted_guilds', temp).then(() => guild); + } + + /** + * Remove a guild from the list of restricted guilds. + * @param {Guild} guild The guild to remove + * @returns {Promise<Guild>} + */ + removeRestrictedGuild(guild) { + const temp = Object.assign([], this.restrictedGuilds); + const index = temp.indexOf(guild.id); + if (index < 0) return Promise.reject(new Error('Guild is not restricted')); + temp.splice(index, 1); + return this.update('restricted_guilds', temp).then(() => guild); + } +} + +module.exports = ClientUserSettings; 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..0a6a3d9 --- /dev/null +++ b/node_modules/discord.js/src/structures/DMChannel.js @@ -0,0 +1,76 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interfaces/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]); + + /** + * The ID of the last message in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; + } + + /** + * 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 + /* eslint-disable no-empty-function */ + get lastPinAt() {} + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendFiles() {} + sendCode() {} + fetchMessage() {} + fetchMessages() {} + fetchPinnedMessages() {} + search() {} + startTyping() {} + stopTyping() {} + get typing() {} + get typingCount() {} + createCollector() {} + createMessageCollector() {} + awaitMessages() {} + // Doesn't work on DM channels; bulkDelete() {} + acknowledge() {} + _cacheMessage() {} +} + +TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); + +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..f514b81 --- /dev/null +++ b/node_modules/discord.js/src/structures/Emoji.js @@ -0,0 +1,273 @@ +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const Permissions = require('../util/Permissions'); +const Snowflake = require('../util/Snowflake'); + +/** + * 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; + + /** + * Whether this emoji has been deleted + * @type {boolean} + */ + this.deleted = false; + + this.setup(data); + } + + setup(data) { + /** + * The ID of the emoji + * @type {Snowflake} + */ + 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; + + /** + * Whether this emoji is animated + * @type {boolean} + */ + this.animated = data.animated; + + /** + * Whether this emoji is available + * @type {boolean} + * @name Emoji#available + */ + if (typeof data.available !== 'undefined') this.available = data.available; + + this._roles = data.roles; + } + + /** + * The timestamp the emoji was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the emoji was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Whether the emoji is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + return !this.managed && this.guild.me.hasPermission(Permissions.FLAGS.MANAGE_EMOJIS); + } + + /** + * A collection of roles this emoji is active for (empty if all), mapped by role ID + * @type {Collection<Snowflake, 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.CDN(this.client.options.http.cdn).Emoji(this.id, this.animated ? 'gif' : 'png'); + } + + /** + * The identifier of this emoji, used for message reactions + * @type {string} + * @readonly + */ + get identifier() { + if (this.id) return `${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * Data for editing an emoji. + * @typedef {Object} EmojiEditData + * @property {string} [name] The name of the emoji + * @property {Collection<Snowflake, Role>|Array<Snowflake|Role>} [roles] Roles to restrict emoji to + */ + + /** + * Edits the emoji. + * @param {EmojiEditData} data The new data for the emoji + * @param {string} [reason] Reason for editing this emoji + * @returns {Promise<Emoji>} + * @example + * // Edit an emoji + * emoji.edit({name: 'newemoji'}) + * .then(e => console.log(`Edited emoji ${e}`)) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateEmoji(this, data, reason); + } + + /** + * Set the name of the emoji. + * @param {string} name The new name for the emoji + * @param {string} [reason] The reason for changing the emoji's name + * @returns {Promise<Emoji>} + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Fetches the author for this emoji + * @returns {Promise<User>} + */ + fetchAuthor() { + if (this.managed) return Promise.reject(new Error('Emoji is managed and has no Author.')); + if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { + return Promise.reject( + new Error(`Client must have Manage Emoji permission in guild ${this.guild} to see emoji authors.`) + ); + } + return this.client.rest.makeRequest('get', Constants.Endpoints.Guild(this.guild).Emoji(this.id), true) + .then(emoji => this.client.dataManager.newUser(emoji.user)); + } + + /** + * Add a role to the list of roles that can use this emoji. + * @param {Role} role The role to add + * @returns {Promise<Emoji>} + */ + addRestrictedRole(role) { + return this.addRestrictedRoles([role]); + } + + /** + * Add multiple roles to the list of roles that can use this emoji. + * @param {Role[]} roles Roles to add + * @returns {Promise<Emoji>} + */ + addRestrictedRoles(roles) { + const newRoles = new Collection(this.roles); + for (const role of roles) { + if (this.guild.roles.has(role.id)) newRoles.set(role.id, role); + } + return this.edit({ roles: newRoles }); + } + + /** + * Remove a role from the list of roles that can use this emoji. + * @param {Role} role The role to remove + * @returns {Promise<Emoji>} + */ + removeRestrictedRole(role) { + return this.removeRestrictedRoles([role]); + } + + /** + * Remove multiple roles from the list of roles that can use this emoji. + * @param {Role[]} roles Roles to remove + * @returns {Promise<Emoji>} + */ + removeRestrictedRoles(roles) { + const newRoles = new Collection(this.roles); + for (const role of roles) { + if (newRoles.has(role.id)) newRoles.delete(role.id); + } + return this.edit({ roles: newRoles }); + } + + + /** + * Deletes the emoji. + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise<Emoji>} + */ + delete(reason) { + return this.client.rest.methods.deleteEmoji(this, reason); + } + + /** + * 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() { + if (!this.id || !this.requiresColons) { + return this.name; + } + + return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; + } + + /** + * 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 + ); + } + } +} + +module.exports = Emoji; 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..0febf6c --- /dev/null +++ b/node_modules/discord.js/src/structures/GroupDMChannel.js @@ -0,0 +1,246 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); + +/* +{ 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 this 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 the DM is managed by an application + * @type {boolean} + */ + this.managed = data.managed; + + /** + * Application ID of the application that made this Group DM, if applicable + * @type {?string} + */ + this.applicationID = data.application_id; + + if (data.nicks) { + /** + * Nicknames for group members + * @type {?Collection<Snowflake, string>} + */ + this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick])); + } + + if (!this.recipients) { + /** + * A collection of the recipients of this DM, mapped by their ID + * @type {Collection<Snowflake, 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); + } + } + + /** + * The ID of the last message in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; + } + + /** + * The owner of this Group DM + * @type {User} + * @readonly + */ + get owner() { + return this.client.users.get(this.ownerID); + } + + /** + * The URL to this guild's icon + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.Channel(this).Icon(this.client.options.http.cdn, this.icon); + } + + edit(data) { + const _data = {}; + if (data.name) _data.name = data.name; + if (typeof data.icon !== 'undefined') _data.icon = data.icon; + return this.client.rest.methods.updateGroupDMChannel(this, _data); + } + + /** + * 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; + } + + /** + * Add a user to the DM + * @param {UserResolvable|string} accessTokenOrID Access token or user resolvable + * @param {string} [nick] Permanent nickname to give the user (only available if a bot is creating the DM) + * @returns {Promise<GroupDMChannel>} + */ + + addUser(accessTokenOrID, nick) { + return this.client.rest.methods.addUserToGroupDM(this, { + nick, + id: this.client.resolver.resolveUserID(accessTokenOrID), + accessToken: accessTokenOrID, + }); + } + + /** + * Set a new GroupDMChannel icon. + * @param {Base64Resolvable|BufferResolvable} icon The new icon of the group dm + * @returns {Promise<GroupDMChannel>} + * @example + * // Edit the group dm icon + * channel.setIcon('./icon.png') + * .then(updated => console.log('Updated the channel icon')) + * .catch(console.error); + */ + setIcon(icon) { + return this.client.resolver.resolveImage(icon).then(data => this.edit({ icon: data })); + } + + /** + * Sets a new name for this Group DM. + * @param {string} name New name for this Group DM + * @returns {Promise<GroupDMChannel>} + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Removes a user from this Group DM. + * @param {UserResolvable} user User to remove + * @returns {Promise<GroupDMChannel>} + */ + removeUser(user) { + const id = this.client.resolver.resolveUserID(user); + return this.client.rest.methods.removeUserFromGroupDM(this, id); + } + + /** + * 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 + /* eslint-disable no-empty-function */ + get lastPinAt() {} + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendFiles() {} + sendCode() {} + fetchMessage() {} + fetchMessages() {} + fetchPinnedMessages() {} + search() {} + startTyping() {} + stopTyping() {} + get typing() {} + get typingCount() {} + createCollector() {} + createMessageCollector() {} + awaitMessages() {} + // Doesn't work on Group DMs; bulkDelete() {} + acknowledge() {} + _cacheMessage() {} +} + +TextBasedChannel.applyToClass(GroupDMChannel, true, ['bulkDelete']); + +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..69acfe1 --- /dev/null +++ b/node_modules/discord.js/src/structures/Guild.js @@ -0,0 +1,1711 @@ +const util = require('util'); +const Long = require('long'); +const User = require('./User'); +const Role = require('./Role'); +const Emoji = require('./Emoji'); +const Presence = require('./Presence').Presence; +const GuildMember = require('./GuildMember'); +const Integration = require('./Integration'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const Util = require('../util/Util'); +const Snowflake = require('../util/Snowflake'); +const SystemChannelFlags = require('../util/SystemChannelFlags'); + +/** + * 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 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<Snowflake, 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<Snowflake, 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<Snowflake, Role>} + */ + this.roles = new Collection(); + + /** + * A collection of presences in this guild + * @type {Collection<Snowflake, Presence>} + */ + this.presences = new Collection(); + + /** + * Whether the bot has been removed from the guild + * @type {boolean} + */ + this.deleted = false; + + 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 {Snowflake} + */ + this.id = data.id; + } else { + this.setup(data); + if (!data.channels) this.available = false; + } + } + + /* eslint-disable complexity */ + /** + * 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 + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image (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 + * @type {number} + */ + this.memberCount = data.member_count || this.memberCount; + + /** + * Whether the guild is "large" (has more than 250 members) + * @type {boolean} + */ + this.large = Boolean('large' in data ? 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 {?Snowflake} + */ + this.applicationID = data.application_id; + + /** + * 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; + + /** + * The ID of the system channel + * @type {?Snowflake} + */ + this.systemChannelID = data.system_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 explicit content filter level of the guild + * @type {number} + */ + this.explicitContentFilter = data.explicit_content_filter; + + /** + * The required MFA level for the guild + * @type {number} + */ + this.mfaLevel = data.mfa_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; + + /** + * The value set for a guild's default message notifications + * @type {DefaultMessageNotifications|number} + */ + this.defaultMessageNotifications = Constants.DefaultMessageNotifications[data.default_message_notifications] || + data.default_message_notifications; + + /** + * The value for the guild's system channel flags + * @type {Readonly<SystemChannelFlags>} + */ + this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze(); + + /** + * The type of premium tier: + * * 0: NONE + * * 1: TIER_1 + * * 2: TIER_2 + * * 3: TIER_3 + * @typedef {number} PremiumTier + */ + + /** + * The premium tier on this guild + * @type {PremiumTier} + */ + this.premiumTier = data.premium_tier; + + /** + * The total number of users currently boosting this server + * @type {?number} + * @name Guild#premiumSubscriptionCount + */ + if (typeof data.premium_subscription_count !== 'undefined') { + this.premiumSubscriptionCount = data.premium_subscription_count; + } + + /** + * The hash of the guild banner + * @type {?string} + */ + this.banner = data.banner; + + /** + * The description of the guild, if any + * @type {?string} + */ + this.description = data.description; + + /** + * The embed channel ID, if enabled + * @type {?string} + * @name Guild#embedChannelID + */ + if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id; + + /** + * The maximum amount of members the guild can have + * <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info> + * @type {?number} + * @name Guild#maximumMembers + */ + if (typeof data.max_members !== 'undefined') this.maximumMembers = data.max_members || 250000; + + /** + * The maximum amount of presences the guild can have + * <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info> + * @type {?number} + * @name Guild#maximumPresences + */ + if (typeof data.max_presences !== 'undefined') this.maximumPresences = data.max_presences || 5000; + + /** + * Whether widget images are enabled on this guild + * @type {?boolean} + * @name Guild#widgetEnabled + */ + if (typeof data.widget_enabled !== 'undefined') this.widgetEnabled = data.widget_enabled; + + /** + * The widget channel ID, if enabled + * @type {?string} + * @name Guild#widgetChannelID + */ + if (typeof data.widget_channel_id !== 'undefined') this.widgetChannelID = data.widget_channel_id; + + /** + * The vanity URL code of the guild, if any + * @type {?string} + */ + this.vanityURLCode = data.vanity_url_code; + + this.id = data.id; + this.available = !data.unavailable; + this.features = data.features || this.features || []; + + /** + * The ID of the rules channel for the guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?Snowflake} + */ + this.rulesChannelID = data.rules_channel_id; + + /** + * The ID of the public updates channel for the guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?Snowflake} + */ + this.publicUpdatesChannelID = data.public_updates_channel_id; + + 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 {Snowflake} + */ + 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); + const voiceChannel = this.channels.get(voiceState.channel_id); + if (member && voiceChannel) { + member.serverMute = voiceState.mute; + member.serverDeaf = voiceState.deaf; + member.selfMute = voiceState.self_mute; + member.selfDeaf = voiceState.self_deaf; + member.selfStream = voiceState.self_stream || false; + member.voiceSessionID = voiceState.session_id; + member.voiceChannelID = voiceState.channel_id; + voiceChannel.members.set(member.user.id, member); + } + } + } + + if (!this.emojis) { + /** + * A collection of emojis that are in this guild + * The key is the emoji's ID, the value is the emoji + * @type {Collection<Snowflake, Emoji>} + */ + this.emojis = new Collection(); + for (const emoji of data.emojis) this.emojis.set(emoji.id, new Emoji(this, emoji)); + } else { + this.client.actions.GuildEmojisUpdate.handle({ + guild_id: this.id, + emojis: data.emojis, + }); + } + } + + /** + * The timestamp the guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the guild was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Embed channel for this guild + * @type {?TextChannel} + * @readonly + */ + get embedChannel() { + return this.channels.get(this.embedChannelID) || null; + } + + /** + * Widget channel for this guild + * @type {?TextChannel} + * @readonly + */ + get widgetChannel() { + return this.channels.get(this.widgetChannelID) || null; + } + + /** + * The time the client user joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * If this guild is verified + * @type {boolean} + * @readonly + */ + get verified() { + return this.features.includes('VERIFIED'); + } + + /** + * The URL to this guild's icon + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.Guild(this).Icon(this.client.options.http.cdn, this.icon); + } + + /** + * The URL to this guild's banner. + * @type {?string} + * @readonly + */ + get bannerURL() { + if (!this.banner) return null; + return Constants.Endpoints.Guild(this).Banner(this.client.options.http.cdn, this.banner); + } + + /** + * The acronym that shows up in place of a guild icon. + * @type {string} + * @readonly + */ + get nameAcronym() { + return this.name.replace(/\w+/g, name => name[0]).replace(/\s/g, ''); + } + + /** + * The URL to this guild's splash + * @type {?string} + * @readonly + */ + get splashURL() { + if (!this.splash) return null; + return Constants.Endpoints.Guild(this).Splash(this.client.options.http.cdn, this.splash); + } + + /** + * The owner of the guild + * @type {?GuildMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID); + } + + /** + * AFK voice channel for this guild + * @type {?VoiceChannel} + * @readonly + */ + get afkChannel() { + return this.client.channels.get(this.afkChannelID) || null; + } + + /** + * System channel for this guild + * @type {?GuildChannel} + * @readonly + */ + get systemChannel() { + return this.client.channels.get(this.systemChannelID) || null; + } + + /** + * 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 position of this guild + * <warn>This is only available when using a user account.</warn> + * @type {?number} + * @readonly + * @deprecated + */ + get position() { + if (this.client.user.bot) return null; + if (!this.client.user.settings.guildPositions) return null; + return this.client.user.settings.guildPositions.indexOf(this.id); + } + + /** + * Whether the guild is muted + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get muted() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).muted; + } catch (err) { + return false; + } + } + + /** + * The type of message that should notify you + * <warn>This is only available when using a user account.</warn> + * @type {?MessageNotificationType} + * @readonly + * @deprecated + */ + get messageNotifications() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).messageNotifications; + } catch (err) { + return null; + } + } + + /** + * Whether to receive mobile push notifications + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get mobilePush() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).mobilePush; + } catch (err) { + return false; + } + } + + /** + * Whether to suppress everyone messages + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get suppressEveryone() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).suppressEveryone; + } catch (err) { + return null; + } + } + + /** + * The `@everyone` role of the guild + * @type {Role} + * @readonly + */ + get defaultRole() { + return this.roles.get(this.id); + } + + /** + * Rules channel for this guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?TextChannel} + * @readonly + */ + get rulesChannel() { + return this.client.channels.get(this.rulesChannelID) || null; + } + + /** + * Public updates channel for this guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?TextChannel} + * @readonly + */ + get publicUpdatesChannel() { + return this.client.channels.get(this.publicUpdatesChannelID) || null; + } + + /** + * The client user as a GuildMember of this guild + * @type {?GuildMember} + * @readonly + */ + get me() { + return this.members.get(this.client.user.id); + } + + /** + * Fetches a collection of roles in the current guild sorted by position + * @type {Collection<Snowflake, Role>} + * @readonly + * @private + */ + get _sortedRoles() { + return this._sortPositionWithID(this.roles); + } + + /** + * 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); + } + + /** + * Fetches this guild. + * @returns {Promise<Guild>} + */ + fetch() { + return this.client.rest.methods.getGuild(this).then(data => { + this.setup(data); + + return this; + }); + } + + /** + * An object containing information about a guild member's ban. + * @typedef {Object} BanInfo + * @property {User} user User that was banned + * @property {?string} reason Reason the user was banned + */ + + /** + * Fetch a ban for a user. + * @returns {Promise<BanInfo>} + * @param {UserResolvable} user The user to fetch the ban for + * @example + * // Get ban + * guild.fetchBan(message.author) + * .then(({ user, reason }) => console.log(`${user.tag} was banned for the reason: ${reason}.`)) + * .catch(console.error); + */ + fetchBan(user) { + return this.client.rest.methods.getGuildBan(this, user); + } + + /** + * Fetch a collection of banned users in this guild. + * @returns {Promise<Collection<Snowflake, User|BanInfo>>} + * @param {boolean} [withReasons=false] Whether or not to include the ban reason(s) + * @example + * // Fetch bans in guild + * guild.fetchBans() + * .then(bans => console.log(`This guild has ${bans.size} bans`)) + * .catch(console.error); + */ + fetchBans(withReasons = false) { + if (withReasons) return this.client.rest.methods.getGuildBans(this); + return this.client.rest.methods.getGuildBans(this) + .then(bans => { + const users = new Collection(); + for (const ban of bans.values()) users.set(ban.user.id, ban.user); + return users; + }); + } + + /** + * Fetches a collection of integrations to this guild. + * Resolves with a collection mapping integrations by their ids. + * @returns {Promise<Collection<string, Integration>>} + * @example + * // Fetch integrations + * guild.fetchIntegrations() + * .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) + * .catch(console.error); + */ + fetchIntegrations() { + return this.client.rest.methods.getIntegrations(this).then(data => + data.reduce((collection, integration) => + collection.set(integration.id, new Integration(this.client, integration, this)), + new Collection()) + ); + } + + /** + * The data for creating an integration. + * @typedef {Object} IntegrationData + * @property {string} id The integration id + * @property {string} type The integration type + */ + + /** + * Creates an integration by attaching an integration object + * @param {IntegrationData} data The data for thes integration + * @param {string} reason Reason for creating the integration + * @returns {Promise<Guild>} + */ + createIntegration(data, reason) { + return this.client.rest.methods.createIntegration(this, data, reason) + .then(() => this); + } + + /** + * Fetch a collection of invites to this guild. + * Resolves with a collection mapping invites by their codes. + * @returns {Promise<Collection<string, Invite>>} + * @example + * // Fetch invites + * guild.fetchInvites() + * .then(invites => console.log(`Fetched ${invites.size} invites`)) + * .catch(console.error); + * @example + * // Fetch invite creator by their id + * guild.fetchInvites() + * .then(invites => console.log(invites.find(invite => invite.inviter.id === '84484653687267328'))) + * .catch(console.error); + */ + fetchInvites() { + return this.client.rest.methods.getGuildInvites(this); + } + + /** + * Fetches the vanity url invite code to this guild. + * Resolves with a string matching the vanity url invite code, not the full url. + * @returns {Promise<string>} + * @example + * // Fetch invites + * guild.fetchVanityCode() + * .then(code => { + * console.log(`Vanity URL: https://discord.gg/${code}`); + * }) + * .catch(console.error); + */ + fetchVanityCode() { + if (!this.features.includes('VANITY_URL')) { + return Promise.reject(new Error('This guild does not have the VANITY_URL feature enabled.')); + } + return this.client.rest.methods.getGuildVanityCode(this); + } + + + /** + * Fetch all webhooks for the guild. + * @returns {Promise<Collection<Snowflake, Webhook>>} + * @example + * // Fetch webhooks + * guild.fetchWebhooks() + * .then(webhooks => console.log(`Fetched ${webhooks.size} webhooks`)) + * .catch(console.error); + */ + fetchWebhooks() { + return this.client.rest.methods.getGuildWebhooks(this); + } + + /** + * Fetch available voice regions. + * @returns {Promise<Collection<string, VoiceRegion>>} + * @example + * // Fetch voice regions + * guild.fetchVoiceRegions() + * .then(console.log) + * .catch(console.error); + */ + fetchVoiceRegions() { + return this.client.rest.methods.fetchVoiceRegions(this.id); + } + + /** + * The Guild Embed object + * @typedef {Object} GuildEmbedData + * @property {boolean} enabled Whether the embed is enabled + * @property {?ChannelResolvable} channel The embed channel + */ + + /** + * Fetches the guild embed. + * @returns {Promise<GuildEmbedData>} + * @example + * // Fetches the guild embed + * guild.fetchEmbed() + * .then(embed => console.log(`The embed is ${embed.enabled ? 'enabled' : 'disabled'}`)) + * .catch(console.error); + */ + fetchEmbed() { + return this.client.rest.methods.fetchEmbed(this.id); + } + + /** + * Fetch audit logs for this guild. + * @param {Object} [options={}] Options for fetching audit logs + * @param {Snowflake|GuildAuditLogsEntry} [options.before] Limit to entries from before specified entry + * @param {Snowflake|GuildAuditLogsEntry} [options.after] Limit to entries from after specified entry + * @param {number} [options.limit] Limit number of entries + * @param {UserResolvable} [options.user] Only show entries involving this user + * @param {AuditLogAction} [options.type] Only show entries involving this action type + * @returns {Promise<GuildAuditLogs>} + * @example + * // Output audit log entries + * guild.fetchAuditLogs() + * .then(audit => console.log(audit.entries.first())) + * .catch(console.error); + */ + fetchAuditLogs(options) { + return this.client.rest.methods.getGuildAuditLogs(this, options); + } + + /** + * Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission. + * @param {UserResolvable} user User to add to the guild + * @param {Object} options Options for the addition + * @param {string} options.accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the + * bot's application + * @param {string} [options.nick] Nickname to give the member (requires `MANAGE_NICKNAMES`) + * @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} [options.roles] Roles to add to the member + * (requires `MANAGE_ROLES`) + * @param {boolean} [options.mute] Whether the member should be muted (requires `MUTE_MEMBERS`) + * @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`) + * @returns {Promise<GuildMember>} + */ + addMember(user, options) { + user = this.client.resolver.resolveUserID(user); + if (this.members.has(user)) return Promise.resolve(this.members.get(user)); + return this.client.rest.methods.putGuildMember(this, user, options); + } + + /** + * Fetch a single guild member from a user. + * @param {UserResolvable} user The user to fetch the member for + * @param {boolean} [cache=true] Insert the member into the members cache + * @returns {Promise<GuildMember>} + * @example + * // Fetch a guild member + * guild.fetchMember(message.author) + * .then(console.log) + * .catch(console.error); + */ + fetchMember(user, cache = true) { + const userID = this.client.resolver.resolveUserID(user); + if (!userID) return Promise.reject(new Error('Invalid id provided.')); + const member = this.members.get(userID); + if (member && member.joinedTimestamp) return Promise.resolve(member); + return this.client.rest.methods.getGuildMember(this, userID, cache); + } + + /** + * 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=''] Limit fetch to members with similar usernames + * @param {number} [limit=0] Maximum number of members to request + * @returns {Promise<Guild>} + * @example + * // Fetch guild members + * guild.fetchMembers() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetches a maximum of 1 member with the given query + * guild.fetchMembers('hydrabolt', 1) + * .then(console.log) + * .catch(console.error); + */ + fetchMembers(query = '', limit = 0) { + return new Promise((resolve, reject) => { + if (this.memberCount === this.members.size) { + resolve(this); + return; + } + this.client.ws.send({ + op: Constants.OPCodes.REQUEST_GUILD_MEMBERS, + d: { + guild_id: this.id, + query, + limit, + }, + }); + const handler = (members, guild) => { + if (guild.id !== this.id) return; + if (this.memberCount === this.members.size || members.length < 1000) { + this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); + resolve(this); + } + }; + this.client.on(Constants.Events.GUILD_MEMBERS_CHUNK, handler); + this.client.setTimeout(() => reject(new Error('Members didn\'t arrive in time.')), 120 * 1000); + }); + } + + /** + * Performs a search within the entire guild. + * <warn>This is only available when using a user account.</warn> + * @param {MessageSearchOptions} [options={}] Options to pass to the search + * @returns {Promise<MessageSearchResult>} + * @deprecated + * @example + * guild.search({ + * content: 'discord.js', + * before: '2016-11-17' + * }) + * .then(res => { + * const hit = res.messages[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); + * }) + * .catch(console.error); + */ + search(options = {}) { + return this.client.rest.methods.search(this, options); + } + + /** + * 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 {number} [explicitContentFilter] The level of the explicit content filter + * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild + * @property {ChannelResolvable} [systemChannel] The system channel of the guild + * @property {number} [afkTimeout] The AFK timeout of the guild + * @property {Base64Resolvable} [icon] The icon of the guild + * @property {Base64Resolvable} [banner] The banner of the guild + * @property {GuildMemberResolvable} [owner] The owner of the guild + * @property {Base64Resolvable} [splash] The splash screen of the guild + * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild + */ + + /** + * Updates the guild with new information - e.g. a new name. + * @param {GuildEditData} data The data to update the guild with + * @param {string} [reason] Reason for editing the guild + * @returns {Promise<Guild>} + * @example + * // Set the guild name and region + * guild.edit({ + * name: 'Discord Guild', + * region: 'london', + * }) + * .then(g => console.log(`Changed guild name to ${g} and region to ${g.region}`)) + * .catch(console.error); + */ + edit(data, reason) { + const _data = {}; + if (data.name) _data.name = data.name; + if (data.region) _data.region = data.region; + if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel); + if (typeof data.afkChannel !== 'undefined') { + _data.afk_channel_id = this.client.resolver.resolveChannelID(data.afkChannel); + } + if (typeof data.systemChannel !== 'undefined') { + _data.system_channel_id = this.client.resolver.resolveChannelID(data.systemChannel); + } + if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); + if (typeof data.icon !== 'undefined') _data.icon = data.icon; + if (data.owner) _data.owner_id = this.client.resolver.resolveUser(data.owner).id; + if (typeof data.splash !== 'undefined') _data.splash = data.splash; + if (typeof data.banner !== 'undefined') _data.banner = data.banner; + if (typeof data.explicitContentFilter !== 'undefined') { + _data.explicit_content_filter = Number(data.explicitContentFilter); + } + if (typeof data.defaultMessageNotifications !== 'undefined') { + _data.default_message_notifications = typeof data.defaultMessageNotifications === 'string' ? + Constants.DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) : + Number(data.defaultMessageNotifications); + } + if (typeof data.systemChannelFlags !== 'undefined') { + _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); + } + return this.client.rest.methods.updateGuild(this, _data, reason); + } + + /** + * Sets a new guild banner. + * @param {BufferResolvable|Base64Resolvable} banner The new banner of the guild + * @param {string} [reason] Reason for changing the guild's banner + * @returns {Guild} + */ + setBanner(banner, reason) { + return this.client.resolver.resolveImage(banner).then(data => this.edit({ banner: data }, reason)); + } + + /** + * Edit the level of the explicit content filter. + * @param {number} explicitContentFilter The new level of the explicit content filter + * @param {string} [reason] Reason for changing the level of the guild's explicit content filter + * @returns {Promise<Guild>} + */ + setExplicitContentFilter(explicitContentFilter, reason) { + return this.edit({ explicitContentFilter }, reason); + } + + /** + * Edits the setting of the default message notifications of the guild. + * @param {DefaultMessageNotifications|number} defaultMessageNotifications + * The new setting for the default message notifications + * @param {string} [reason] Reason for changing the setting of the default message notifications + * @returns {Promise<Guild>} + */ + setDefaultMessageNotifications(defaultMessageNotifications, reason) { + return this.edit({ defaultMessageNotifications }, reason); + } + + /** + * Edits the flags of the default message notifications of the guild. + * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications + * @param {string} [reason] Reason for changing the flags of the default message notifications + * @returns {Promise<Guild>} + */ + setSystemChannelFlags(systemChannelFlags, reason) { + return this.edit({ systemChannelFlags }, reason); + } + + /** + * Edit the name of the guild. + * @param {string} name The new name of the guild + * @param {string} [reason] Reason for changing the guild's name + * @returns {Promise<Guild>} + * @example + * // Edit the guild name + * guild.setName('Discord Guild') + * .then(g => console.log(`Updated guild name to ${g}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Edit the region of the guild. + * @param {string} region The new region of the guild + * @param {string} [reason] Reason for changing the guild's region + * @returns {Promise<Guild>} + * @example + * // Edit the guild region + * guild.setRegion('london') + * .then(g => console.log(`Updated guild region to ${g.region}`)) + * .catch(console.error); + */ + setRegion(region, reason) { + return this.edit({ region }, reason); + } + + /** + * Edit the verification level of the guild. + * @param {number} verificationLevel The new verification level of the guild + * @param {string} [reason] Reason for changing the guild's verification level + * @returns {Promise<Guild>} + * @example + * // Edit the guild verification level + * guild.setVerificationLevel(1) + * .then(g => console.log(`Updated guild verification level to ${g.verificationLevel}`)) + * .catch(console.error); + */ + setVerificationLevel(verificationLevel, reason) { + return this.edit({ verificationLevel }, reason); + } + + /** + * Edit the AFK channel of the guild. + * @param {ChannelResolvable} afkChannel The new AFK channel + * @param {string} [reason] Reason for changing the guild's AFK channel + * @returns {Promise<Guild>} + * @example + * // Edit the guild AFK channel + * guild.setAFKChannel(channel) + * .then(g => console.log(`Updated guild AFK channel to ${g.afkChannel.name}`)) + * .catch(console.error); + */ + setAFKChannel(afkChannel, reason) { + return this.edit({ afkChannel }, reason); + } + + /** + * Edit the system channel of the guild. + * @param {ChannelResolvable} systemChannel The new system channel + * @param {string} [reason] Reason for changing the guild's system channel + * @returns {Promise<Guild>} + */ + setSystemChannel(systemChannel, reason) { + return this.edit({ systemChannel }, reason); + } + + /** + * Edit the AFK timeout of the guild. + * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK + * @param {string} [reason] Reason for changing the guild's AFK timeout + * @returns {Promise<Guild>} + * @example + * // Edit the guild AFK channel + * guild.setAFKTimeout(60) + * .then(g => console.log(`Updated guild AFK timeout to ${g.afkTimeout}`)) + * .catch(console.error); + */ + setAFKTimeout(afkTimeout, reason) { + return this.edit({ afkTimeout }, reason); + } + + /** + * Set a new guild icon. + * @param {Base64Resolvable|BufferResolvable} icon The new icon of the guild + * @param {string} [reason] Reason for changing the guild's icon + * @returns {Promise<Guild>} + * @example + * // Edit the guild icon + * guild.setIcon('./icon.png') + * .then(console.log) + * .catch(console.error); + */ + setIcon(icon, reason) { + return this.client.resolver.resolveImage(icon).then(data => this.edit({ icon: data, reason })); + } + + /** + * Sets a new owner of the guild. + * @param {GuildMemberResolvable} owner The new owner of the guild + * @param {string} [reason] Reason for setting the new owner + * @returns {Promise<Guild>} + * @example + * // Edit the guild owner + * guild.setOwner(guild.members.first()) + * .then(g => console.log(`Updated the guild owner to ${g.owner.displayName}`)) + * .catch(console.error); + */ + setOwner(owner, reason) { + return this.edit({ owner }, reason); + } + + /** + * Set a new guild splash screen. + * @param {BufferResolvable|Base64Resolvable} splash The new splash screen of the guild + * @param {string} [reason] Reason for changing the guild's splash screen + * @returns {Promise<Guild>} + * @example + * // Edit the guild splash + * guild.setSplash('./splash.png') + * .then(console.log) + * .catch(console.error); + */ + setSplash(splash) { + return this.client.resolver.resolveImage(splash).then(data => this.edit({ splash: data })); + } + + /** + * Sets the position of the guild in the guild listing. + * <warn>This is only available when using a user account.</warn> + * @param {number} position Absolute or relative position + * @param {boolean} [relative=false] Whether to position relatively or absolutely + * @returns {Promise<Guild>} + * @deprecated + */ + setPosition(position, relative) { + if (this.client.user.bot) { + return Promise.reject(new Error('Setting guild position is only available for user accounts')); + } + return this.client.user.settings.setGuildPosition(this, position, relative); + } + + /** + * Marks all messages in this guild as read. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<Guild>} + * @deprecated + */ + acknowledge() { + return this.client.rest.methods.ackGuild(this); + } + + /** + * Allow direct messages from guild members. + * <warn>This is only available when using a user account.</warn> + * @param {boolean} allow Whether to allow direct messages + * @returns {Promise<Guild>} + * @deprecated + */ + allowDMs(allow) { + const settings = this.client.user.settings; + if (allow) return settings.removeRestrictedGuild(this); + else return settings.addRestrictedGuild(this); + } + + /** + * Bans a user from the guild. + * @param {UserResolvable} user The user to ban + * @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a + * string, the ban reason. Supplying an object allows you to do both. + * @param {number} [options.days=0] Number of days of messages to delete + * @param {string} [options.reason] Reason for banning + * @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 by ID + * guild.ban('some user ID') + * .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild}`)) + * .catch(console.error); + * @example + * // Ban a user by object with reason and days + * guild.ban(user, { days: 7, reason: 'He needed to go' }) + * .then(console.log) + * .catch(console.error); + */ + ban(user, options = {}) { + if (typeof options === 'number') { + options = { reason: null, 'delete-message-days': options }; + } else if (typeof options === 'string') { + options = { reason: options, 'delete-message-days': 0 }; + } + if (options.days) options['delete-message-days'] = options.days; + return this.client.rest.methods.banGuildMember(this, user, options); + } + + /** + * Unbans a user from the guild. + * @param {UserResolvable} user The user to unban + * @param {string} [reason] Reason for unbanning the user + * @returns {Promise<User>} + * @example + * // Unban a user by ID (or with a user/guild member object) + * guild.unban('some user ID') + * .then(user => console.log(`Unbanned ${user.username} from ${guild}`)) + * .catch(console.error); + */ + unban(user, reason) { + return this.client.rest.methods.unbanGuildMember(this, user, reason); + } + + /** + * 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 + * @param {string} [reason] Reason for this prune + * @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, reason) { + if (typeof days !== 'number') throw new TypeError('Days must be a number.'); + return this.client.rest.methods.pruneGuildMembers(this, days, dry, reason); + } + + /** + * Syncs this guild (already done automatically every 30 seconds). + * <warn>This is only available when using a user account.</warn> + * @deprecated + */ + sync() { + if (!this.client.user.bot) this.client.syncGuilds([this]); + } + + /** + * Overwrites to use when creating a channel or replacing overwrites + * @typedef {Object} ChannelCreationOverwrites + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [allowed] The permissions to allow + * **(deprecated)** + * @property {PermissionResolvable} [deny] The permissions to deny + * @property {PermissionResolvable} [denied] The permissions to deny + * **(deprecated)** + * @property {GuildMemberResolvable|RoleResolvable} id Member or role this overwrite is for + */ + + /** + * Creates a new channel in the guild. + * @param {string} name The name of the new channel + * @param {string|ChannelData} [typeOrOptions='text'] + * The type of the new channel, one of `text`, `voice`, `category`, `news`, or `store`. **(deprecated, use options)** + * Alternatively options for the new channel, overriding the following parameters. + * @param {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites] + * Permission overwrites **(deprecated, use options)** + * @param {string} [reason] Reason for creating this channel **(deprecated, use options)** + * @returns {Promise<CategoryChannel|TextChannel|VoiceChannel>} + * @example + * // Create a new text channel + * guild.createChannel('new-general', { type: 'text' }) + * .then(console.log) + * .catch(console.error); + * @example + * // Create a new category channel with permission overwrites + * guild.createChannel('new-category', { + * type: 'category', + * permissionOverwrites: [{ + * id: guild.id, + * deny: ['MANAGE_MESSAGES'], + * allow: ['SEND_MESSAGES'] + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + createChannel(name, typeOrOptions, permissionOverwrites, reason) { + if (!typeOrOptions || (typeof typeOrOptions === 'string')) { + if (typeOrOptions) { + process.emitWarning( + 'Guild#createChannel: Create channels with an options object instead of separate parameters', + 'DeprecationWarning' + ); + } + typeOrOptions = { + type: typeOrOptions, + permissionOverwrites, + reason, + }; + } + return this.client.rest.methods.createChannel(this, name, typeOrOptions); + } + + /** + * The data needed for updating a channel's position. + * @typedef {Object} ChannelPosition + * @property {ChannelResolvable} channel Channel to update + * @property {number} position New position for the channel + */ + + /** + * Batch-updates the guild's channels' positions. + * @param {ChannelPosition[]} channelPositions Channel positions to update + * @returns {Promise<Guild>} + * @example + * guild.updateChannels([{ channel: channelID, position: newChannelIndex }]) + * .then(g => console.log(`Updated channel positions for ${g}`)) + * .catch(console.error); + */ + setChannelPositions(channelPositions) { + channelPositions = channelPositions.map(({ channel, position }) => ({ id: channel.id || channel, position })); + return this.client.rest.methods.setChannelPositions(this.id, channelPositions); + } + + /** + * The data needed for updating a role's position. + * @typedef {Object} RolePosition + * @property {RoleResolvable} role Role to update + * @property {number} position New position for the role + */ + + /** + * Batch-updates the guild's role's positions. + * @param {RolePosition[]} rolePositions Role positions to update + * @returns {Promise<Guild>} + */ + setRolePositions(rolePositions) { + rolePositions = rolePositions.map(({ role, position }) => ({ id: role.id || role, position })); + return this.client.rest.methods.setRolePositions(this.id, rolePositions); + } + + /** + * Edits the guild's embed. + * @param {GuildEmbedData} embed The embed for the guild + * @param {string} [reason] Reason for changing the guild's embed + * @returns {Promise<Guild>} + */ + setEmbed(embed, reason) { + return this.client.rest.methods.updateEmbed(this.id, embed, reason) + .then(() => this); + } + + /** + * Creates a new role in the guild with given information. + * @param {RoleData} [data] The data to update the role with + * @param {string} [reason] Reason for creating this role + * @returns {Promise<Role>} + * @example + * // Create a new role + * guild.createRole() + * .then(role => console.log(`Created new role with name ${role.name}`)) + * .catch(console.error); + * @example + * // Create a new role with data + * guild.createRole({ + * name: 'Super Cool People', + * color: 'BLUE', + * }) + * .then(role => console.log(`Created new role with name ${role.name} and color ${role.color}`)) + * .catch(console.error) + */ + createRole(data = {}, reason) { + return this.client.rest.methods.createGuildRole(this, data, reason); + } + + /** + * Creates a new custom emoji in the guild. + * @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji + * @param {string} name The name for the emoji + * @param {Collection<Snowflake, Role>|Role[]} [roles] Roles to limit the emoji to + * @param {string} [reason] Reason for creating 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, roles, reason) { + if (typeof attachment === 'string' && attachment.startsWith('data:')) { + return this.client.rest.methods.createEmoji(this, attachment, name, roles, reason); + } else { + return this.client.resolver.resolveImage(attachment).then(data => + this.client.rest.methods.createEmoji(this, data, name, roles, reason) + ); + } + } + + /** + * Delete an emoji. + * @param {Emoji|string} emoji The emoji to delete + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise} + * @deprecated + */ + deleteEmoji(emoji, reason) { + if (typeof emoji === 'string') emoji = this.emojis.get(emoji); + if (!(emoji instanceof Emoji)) throw new TypeError('Emoji must be either an instance of Emoji or an ID'); + return emoji.delete(reason); + } + + /** + * 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); + } + + /** + * 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 The 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 && + Util.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.selfStream = voiceState.self_stream || false; + 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.connection.status === Constants.Status.READY && emitEvent && !existing) { + this.client.emit(Constants.Events.GUILD_MEMBER_ADD, member); + } + + return member; + } + + _updateMember(member, data) { + const oldMember = Util.cloneObject(member); + + if (data.premium_since) member.premiumSinceTimestamp = new Date(data.premium_since).getTime(); + if (data.roles) member._roles = data.roles; + if (typeof data.nick !== 'undefined') member.nickname = data.nick; + + const notSame = member.nickname !== oldMember.nickname || + member.premiumSinceTimestamp !== oldMember.premiumSinceTimestamp || + !Util.arraysEqual(member._roles, oldMember._roles); + + if (this.client.ws.connection.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) { + if (guildMember.voiceChannel) guildMember.voiceChannel.members.delete(guildMember.id); + this.members.delete(guildMember.id); + } + + _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, this.client)); + } + + /** + * 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 + * @param {boolean} [relative=false] Position Moves the role relative to its current position + * @returns {Promise<Guild>} + */ + setRolePosition(role, position, relative = false) { + if (typeof role === 'string') { + role = this.roles.get(role); + if (!role) return Promise.reject(new Error('Supplied role is not a role or snowflake.')); + } + + position = Number(position); + if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + + let updatedRoles = this._sortedRoles.array(); + + Util.moveElementInArray(updatedRoles, role, position, relative); + + updatedRoles = updatedRoles.map((r, i) => ({ id: r.id, position: i })); + return this.client.rest.methods.setRolePositions(this.id, updatedRoles); + } + + /** + * Set the position of a channel in this guild. + * @param {string|GuildChannel} channel The channel to edit, can be a channel object or a channel ID + * @param {number} position The new position of the channel + * @param {boolean} [relative=false] Position Moves the channel relative to its current position + * @returns {Promise<Guild>} + */ + setChannelPosition(channel, position, relative = false) { + if (typeof channel === 'string') { + channel = this.channels.get(channel); + if (!channel) return Promise.reject(new Error('Supplied channel is not a channel or snowflake.')); + } + + position = Number(position); + if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + + let updatedChannels = this._sortedChannels(channel.type).array(); + + Util.moveElementInArray(updatedChannels, channel, position, relative); + + updatedChannels = updatedChannels.map((c, i) => ({ id: c.id, position: i })); + return this.client.rest.methods.setChannelPositions(this.id, updatedChannels); + } + + /** + * Fetches a collection of channels in the current guild sorted by position. + * @param {string} type The channel type + * @returns {Collection<Snowflake, GuildChannel>} + * @private + */ + _sortedChannels(type) { + return this._sortPositionWithID(this.channels.filter(c => { + if (type === 'voice' && c.type === 'voice') return true; + else if (type !== 'voice' && c.type !== 'voice') return true; + else return type === c.type; + })); + } + + /** + * Sorts a collection by object position or ID if the positions are equivalent. + * Intended to be identical to Discord's sorting method. + * @param {Collection} collection The collection to sort + * @returns {Collection} + * @private + */ + _sortPositionWithID(collection) { + return collection.sort((a, b) => + a.position !== b.position ? + a.position - b.position : + Long.fromString(b.id).sub(Long.fromString(a.id)).toNumber() + ); + } +} + +/** + * The `#general` TextChannel of the guild + * @name Guild#defaultChannel + * @type {TextChannel} + * @readonly + * @deprecated + */ +Object.defineProperty(Guild.prototype, 'defaultChannel', { + get: util.deprecate(function defaultChannel() { + return this.channels.get(this.id); + }, 'Guild#defaultChannel: This property is obsolete, will be removed in v12.0.0, and may not function as expected.'), +}); + +Guild.prototype.allowDMs = + util.deprecate(Guild.prototype.allowDMs, 'Guild#allowDMs: userbot methods will be removed'); + +Guild.prototype.acknowledge = + util.deprecate(Guild.prototype.acknowledge, 'Guild#acknowledge: userbot methods will be removed'); + +Guild.prototype.setPosition = + util.deprecate(Guild.prototype.setPosition, 'Guild#setPosition: userbot methods will be removed'); + +Guild.prototype.search = + util.deprecate(Guild.prototype.search, 'Guild#search: userbot methods will be removed'); + +Guild.prototype.sync = + util.deprecate(Guild.prototype.sync, 'Guild#sync:, userbot methods will be removed'); + +Guild.prototype.deleteEmoji = + util.deprecate(Guild.prototype.deleteEmoji, 'Guild#deleteEmoji: use Emoji#delete instead'); + +module.exports = Guild; diff --git a/node_modules/discord.js/src/structures/GuildAuditLogs.js b/node_modules/discord.js/src/structures/GuildAuditLogs.js new file mode 100644 index 0000000..5c98594 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildAuditLogs.js @@ -0,0 +1,462 @@ +const Collection = require('../util/Collection'); +const Snowflake = require('../util/Snowflake'); +const Webhook = require('./Webhook'); +const Integration = require('./Integration'); +const Invite = require('./Invite'); + +/** + * The target type of an entry, e.g. `GUILD`. Here are the available types: + * * GUILD + * * CHANNEL + * * USER + * * ROLE + * * INVITE + * * WEBHOOK + * * EMOJI + * * MESSAGE + * * INTEGRATION + * @typedef {string} AuditLogTargetType + */ + +/** + * Key mirror of all available audit log targets. + * @name GuildAuditLogs.Targets + * @type {AuditLogTargetType} + */ +const Targets = { + ALL: 'ALL', + GUILD: 'GUILD', + CHANNEL: 'CHANNEL', + USER: 'USER', + ROLE: 'ROLE', + INVITE: 'INVITE', + WEBHOOK: 'WEBHOOK', + EMOJI: 'EMOJI', + MESSAGE: 'MESSAGE', + INTEGRATION: 'INTEGRATION', + UNKNOWN: 'UNKNOWN', +}; + +/** + * The action of an entry. Here are the available actions: + * * ALL: null + * * GUILD_UPDATE: 1 + * * CHANNEL_CREATE: 10 + * * CHANNEL_UPDATE: 11 + * * CHANNEL_DELETE: 12 + * * CHANNEL_OVERWRITE_CREATE: 13 + * * CHANNEL_OVERWRITE_UPDATE: 14 + * * CHANNEL_OVERWRITE_DELETE: 15 + * * MEMBER_KICK: 20 + * * MEMBER_PRUNE: 21 + * * MEMBER_BAN_ADD: 22 + * * MEMBER_BAN_REMOVE: 23 + * * MEMBER_UPDATE: 24 + * * MEMBER_ROLE_UPDATE: 25 + * * MEMBER_MOVE: 26 + * * MEMBER_DISCONNECT: 27 + * * BOT_ADD: 28, + * * ROLE_CREATE: 30 + * * ROLE_UPDATE: 31 + * * ROLE_DELETE: 32 + * * INVITE_CREATE: 40 + * * INVITE_UPDATE: 41 + * * INVITE_DELETE: 42 + * * WEBHOOK_CREATE: 50 + * * WEBHOOK_UPDATE: 51 + * * WEBHOOK_DELETE: 52 + * * EMOJI_CREATE: 60 + * * EMOJI_UPDATE: 61 + * * EMOJI_DELETE: 62 + * * MESSAGE_DELETE: 72 + * * MESSAGE_BULK_DELETE: 73 + * * MESSAGE_PIN: 74 + * * MESSAGE_UNPIN: 75 + * * INTEGRATION_CREATE: 80 + * * INTEGRATION_UPDATE: 81 + * * INTEGRATION_DELETE: 82 + * @typedef {?number|string} AuditLogAction + */ + +/** + * All available actions keyed under their names to their numeric values. + * @name GuildAuditLogs.Actions + * @type {AuditLogAction} + */ +const Actions = { + ALL: null, + GUILD_UPDATE: 1, + CHANNEL_CREATE: 10, + CHANNEL_UPDATE: 11, + CHANNEL_DELETE: 12, + CHANNEL_OVERWRITE_CREATE: 13, + CHANNEL_OVERWRITE_UPDATE: 14, + CHANNEL_OVERWRITE_DELETE: 15, + MEMBER_KICK: 20, + MEMBER_PRUNE: 21, + MEMBER_BAN_ADD: 22, + MEMBER_BAN_REMOVE: 23, + MEMBER_UPDATE: 24, + MEMBER_ROLE_UPDATE: 25, + MEMBER_MOVE: 26, + MEMBER_DISCONNECT: 27, + BOT_ADD: 28, + ROLE_CREATE: 30, + ROLE_UPDATE: 31, + ROLE_DELETE: 32, + INVITE_CREATE: 40, + INVITE_UPDATE: 41, + INVITE_DELETE: 42, + WEBHOOK_CREATE: 50, + WEBHOOK_UPDATE: 51, + WEBHOOK_DELETE: 52, + EMOJI_CREATE: 60, + EMOJI_UPDATE: 61, + EMOJI_DELETE: 62, + MESSAGE_DELETE: 72, + MESSAGE_BULK_DELETE: 73, + MESSAGE_PIN: 74, + MESSAGE_UNPIN: 75, + INTEGRATION_CREATE: 80, + INTEGRATION_UPDATE: 81, + INTEGRATION_DELETE: 82, +}; + + +/** + * Audit logs entries are held in this class. + */ +class GuildAuditLogs { + constructor(guild, data) { + if (data.users) for (const user of data.users) guild.client.dataManager.newUser(user); + + /** + * Cached webhooks + * @type {Collection<Snowflake, Webhook>} + * @private + */ + this.webhooks = new Collection(); + if (data.webhooks) { + for (const hook of data.webhooks) { + this.webhooks.set(hook.id, new Webhook(guild.client, hook)); + } + } + + /** + * Cached integrations + * @type {Collection<Snowflake, Integration>} + * @private + */ + this.integrations = new Collection(); + if (data.integrations) { + for (const integration of data.integrations) { + this.integrations.set(integration.id, new Integration(guild.client, integration, guild)); + } + } + + /** + * The entries for this guild's audit logs + * @type {Collection<Snowflake, GuildAuditLogsEntry>} + */ + this.entries = new Collection(); + for (const item of data.audit_log_entries) { + const entry = new GuildAuditLogsEntry(this, guild, item); + this.entries.set(entry.id, entry); + } + } + + /** + * Handles possible promises for entry targets. + * @returns {Promise<GuildAuditLogs>} + */ + static build(...args) { + const logs = new GuildAuditLogs(...args); + return Promise.all(logs.entries.map(e => e.target)).then(() => logs); + } + + /** + * The target of an entry. It can be one of: + * * A guild + * * A user + * * A role + * * An emoji + * * An invite + * * A webhook + * * An integration + * * An object with an id key if target was deleted + * * An object where the keys represent either the new value or the old value + * @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook|Integration} AuditLogEntryTarget + */ + + /** + * Find target type from entry action. + * @param {number} target The action target + * @returns {?string} + */ + static targetType(target) { + if (target < 10) return Targets.GUILD; + if (target < 20) return Targets.CHANNEL; + if (target < 30) return Targets.USER; + if (target < 40) return Targets.ROLE; + if (target < 50) return Targets.INVITE; + if (target < 60) return Targets.WEBHOOK; + if (target < 70) return Targets.EMOJI; + if (target < 80) return Targets.MESSAGE; + if (target < 90) return Targets.INTEGRATION; + return null; + } + + /** + * The action type of an entry, e.g. `CREATE`. Here are the available types: + * * CREATE + * * DELETE + * * UPDATE + * * ALL + * @typedef {string} AuditLogActionType + */ + + + /** + * Finds the action type from the entry action. + * @param {AuditLogAction} action The action target + * @returns {AuditLogActionType} + */ + static actionType(action) { + if ([ + Actions.CHANNEL_CREATE, + Actions.CHANNEL_OVERWRITE_CREATE, + Actions.MEMBER_BAN_REMOVE, + Actions.BOT_ADD, + Actions.ROLE_CREATE, + Actions.INVITE_CREATE, + Actions.WEBHOOK_CREATE, + Actions.EMOJI_CREATE, + Actions.MESSAGE_PIN, + Actions.INTEGRATION_CREATE, + ].includes(action)) return 'CREATE'; + + if ([ + Actions.CHANNEL_DELETE, + Actions.CHANNEL_OVERWRITE_DELETE, + Actions.MEMBER_KICK, + Actions.MEMBER_PRUNE, + Actions.MEMBER_BAN_ADD, + Actions.MEMBER_DISCONNECT, + Actions.ROLE_DELETE, + Actions.INVITE_DELETE, + Actions.WEBHOOK_DELETE, + Actions.EMOJI_DELETE, + Actions.MESSAGE_DELETE, + Actions.MESSAGE_BULK_DELETE, + Actions.MESSAGE_UNPIN, + Actions.INTEGRATION_DELETE, + ].includes(action)) return 'DELETE'; + + if ([ + Actions.GUILD_UPDATE, + Actions.CHANNEL_UPDATE, + Actions.CHANNEL_OVERWRITE_UPDATE, + Actions.MEMBER_UPDATE, + Actions.MEMBER_ROLE_UPDATE, + Actions.MEMBER_MOVE, + Actions.ROLE_UPDATE, + Actions.INVITE_UPDATE, + Actions.WEBHOOK_UPDATE, + Actions.EMOJI_UPDATE, + Actions.INTEGRATION_UPDATE, + ].includes(action)) return 'UPDATE'; + + return 'ALL'; + } +} + +/** + * Audit logs entry. + */ +class GuildAuditLogsEntry { + // eslint-disable-next-line complexity + constructor(logs, guild, data) { + const targetType = GuildAuditLogs.targetType(data.action_type); + /** + * The target type of this entry + * @type {AuditLogTargetType} + */ + this.targetType = targetType; + + /** + * The action type of this entry + * @type {AuditLogActionType} + */ + this.actionType = GuildAuditLogs.actionType(data.action_type); + + /** + * Specific action type of this entry in its string representation + * @type {AuditLogAction} + */ + this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type); + + /** + * The reason of this entry + * @type {?string} + */ + this.reason = data.reason || null; + + /** + * The user that executed this entry + * @type {User} + */ + this.executor = guild.client.users.get(data.user_id); + + /** + * An entry in the audit log representing a specific change. + * @typedef {object} AuditLogChange + * @property {string} key The property that was changed, e.g. `nick` for nickname changes + * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname + * @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname + */ + + /** + * Specific property changes + * @type {AuditLogChange[]} + */ + this.changes = data.changes ? data.changes.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) : null; + + /** + * The ID of this entry + * @type {Snowflake} + */ + this.id = data.id; + + /** + * Any extra data from the entry + * @type {?Object|Role|GuildMember} + */ + this.extra = null; + switch (data.action_type) { + case Actions.MEMBER_PRUNE: + this.extra = { + removed: Number(data.options.members_removed), + days: Number(data.options.delete_member_days), + }; + break; + + case Actions.MEMBER_MOVE: + case Actions.MESSAGE_DELETE: + case Actions.MESSAGE_BULK_DELETE: + this.extra = { + channel: guild.channels.get(data.options.channel_id) || { id: data.options.channel_id }, + count: Number(data.options.count), + }; + break; + + case Actions.MESSAGE_PIN: + case Actions.MESSAGE_UNPIN: + this.extra = { + channel: guild.client.channels.get(data.options.channel_id) || { id: data.options.channel_id }, + messageID: data.options.message_id, + }; + break; + + case Actions.MEMBER_DISCONNECT: + this.extra = { + count: Number(data.options.count), + }; + break; + + case Actions.CHANNEL_OVERWRITE_CREATE: + case Actions.CHANNEL_OVERWRITE_UPDATE: + case Actions.CHANNEL_OVERWRITE_DELETE: + switch (data.options.type) { + case 'member': + this.extra = guild.members.get(data.options.id) || + { id: data.options.id, type: 'member' }; + break; + case 'role': + this.extra = guild.roles.get(data.options.id) || + { id: data.options.id, name: data.options.role_name, type: 'role' }; + break; + default: + break; + } + break; + + default: + break; + } + + /** + * The target of this entry + * @type {AuditLogEntryTarget} + */ + this.target = null; + if (targetType === Targets.UNKNOWN) { + this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, {}); + this.target.id = data.target_id; + // MEMBER_DISCONNECT and similar types do not provide a target_id. + } else if (targetType === Targets.USER && data.target_id) { + this.target = guild.client.users.get(data.target_id); + } else if (targetType === Targets.GUILD) { + this.target = guild.client.guilds.get(data.target_id); + } else if (targetType === Targets.WEBHOOK) { + this.target = logs.webhooks.get(data.target_id) || + new Webhook(guild.client, + this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { + id: data.target_id, + guild_id: guild.id, + })); + } else if (targetType === Targets.INVITE) { + const changes = this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { + id: data.target_id, + guild, + }); + changes.channel = { id: changes.channel_id }; + this.target = new Invite(guild.client, changes); + } else if (targetType === Targets.MESSAGE) { + // Discord sends a channel id for the MESSAGE_BULK_DELETE action type. + this.target = data.action_type === Actions.MESSAGE_BULK_DELETE ? + guild.channels.get(data.target_id) || { id: data.target_id } : + guild.client.users.get(data.target_id); + } else if (targetType === Targets.INTEGRATION) { + this.target = logs.integrations.get(data.target_id) || + new Integration(guild.client, this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { id: data.target_id }), guild); + } else if (data.target_id) { + this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id) || { id: data.target_id }; + } + } + + /** + * The timestamp this entry was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time this entry was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } +} + +GuildAuditLogs.Actions = Actions; +GuildAuditLogs.Targets = Targets; +GuildAuditLogs.Entry = GuildAuditLogsEntry; + +module.exports = GuildAuditLogs; 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..0dbc0e3 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildChannel.js @@ -0,0 +1,601 @@ +const Channel = require('./Channel'); +const Role = require('./Role'); +const PermissionOverwrites = require('./PermissionOverwrites'); +const Permissions = require('../util/Permissions'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); +const Invite = require('./Invite'); +const Util = require('../util/Util'); + +/** + * 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; + + /** + * The ID of the category parent of this channel + * @type {?Snowflake} + */ + this.parentID = data.parent_id; + + /** + * A map of permission overwrites in this channel for roles and users + * @type {Collection<Snowflake, 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)); + } + } + } + + /** + * The position of the channel + * @type {number} + * @readonly + */ + get calculatedPosition() { + const sorted = this.guild._sortedChannels(this.type); + return sorted.array().indexOf(sorted.get(this.id)); + } + + /** + * The category parent of this channel + * @type {?CategoryChannel} + * @readonly + */ + get parent() { + return this.guild.channels.get(this.parentID) || null; + } + + /** + * If the permissionOverwrites match the parent channel, null if no parent + * @type {?boolean} + * @readonly + */ + get permissionsLocked() { + if (!this.parent) return null; + if (this.permissionOverwrites.size !== this.parent.permissionOverwrites.size) return false; + return this.permissionOverwrites.every((value, key) => { + const testVal = this.parent.permissionOverwrites.get(key); + return testVal !== undefined && + testVal.deny === value.deny && + testVal.allow === value.allow; + }); + } + + /** + * Gets the overall set of permissions for a user in this channel, taking into account channel overwrites. + * @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for + * @returns {?Permissions} + */ + memberPermissions(member) { + member = this.client.resolver.resolveGuildMember(this.guild, member); + if (!member) return null; + + if (member.id === this.guild.ownerID) return new Permissions(member, Permissions.ALL); + + const roles = member.roles; + const permissions = new Permissions(roles.map(role => role.permissions)); + + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); + + const overwrites = this.overwritesFor(member, true, roles); + + return permissions + .remove(overwrites.everyone ? overwrites.everyone.deny : 0) + .add(overwrites.everyone ? overwrites.everyone.allow : 0) + .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : 0) + .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : 0) + .remove(overwrites.member ? overwrites.member.deny : 0) + .add(overwrites.member ? overwrites.member.allow : 0) + .freeze(); + } + + /** + * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. + * @param {RoleResolvable} role The role that you want to obtain the overall permissions for + * @returns {?Permissions} + */ + rolePermissions(role) { + if (role.permissions & Permissions.FLAGS.ADMINISTRATOR) return new Permissions(Permissions.ALL).freeze(); + + const everyoneOverwrites = this.permissionOverwrites.get(this.guild.id); + const roleOverwrites = this.permissionOverwrites.get(role.id); + + return new Permissions(role.permissions) + .remove(everyoneOverwrites ? everyoneOverwrites.deny : 0) + .add(everyoneOverwrites ? everyoneOverwrites.allow : 0) + .remove(roleOverwrites ? roleOverwrites.deny : 0) + .add(roleOverwrites ? roleOverwrites.allow : 0) + .freeze(); + } + + /** + * Get the overall set of permissions for a member or role in this channel, taking into account channel overwrites. + * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for + * @returns {?Permissions} + */ + permissionsFor(memberOrRole) { + const member = this.guild.member(memberOrRole); + if (member) return this.memberPermissions(member); + const role = this.client.resolver.resolveRole(this.guild, memberOrRole); + if (role) return this.rolePermissions(role); + return null; + } + + 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 = []; + let memberOverwrites; + let everyoneOverwrites; + + for (const overwrite of this.permissionOverwrites.values()) { + if (overwrite.id === this.guild.id) { + everyoneOverwrites = overwrite; + } else if (roles.has(overwrite.id)) { + roleOverwrites.push(overwrite); + } else if (overwrite.id === member.id) { + memberOverwrites = overwrite; + } + } + + return { + everyone: everyoneOverwrites, + roles: roleOverwrites, + member: memberOverwrites, + }; + } + + /** + * Replaces the permission overwrites for a channel + * @param {Object} [options] Options + * @param {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [options.overwrites] + * Permission overwrites + * @param {string} [options.reason] Reason for updating the channel overwrites + * @returns {Promise<GuildChannel>} + * @example + * channel.replacePermissionOverwrites({ + * overwrites: [ + * { + * id: message.author.id, + * denied: ['VIEW_CHANNEL'], + * }, + * ], + * reason: 'Needed to change permissions' + * }); + */ + replacePermissionOverwrites({ overwrites, reason } = {}) { + return this.edit({ permissionOverwrites: overwrites, reason }) + .then(() => this); + } + + /** + * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). + * ```js + * { + * 'SEND_MESSAGES': true, + * 'EMBED_LINKS': null, + * 'ATTACH_FILES': false, + * } + * ``` + * @typedef {Object} PermissionOverwriteOptions + */ + + /** + * Overwrites the permissions for a user or role in this channel. + * @param {Role|Snowflake|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The configuration for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise<GuildChannel>} + * @example + * // Overwrite permissions for a message author + * message.channel.overwritePermissions(message.author, { + * SEND_MESSAGES: false + * }) + * .then(updated => console.log(updated.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + * @example + * // Overwite permissions for a message author and reset some + * message.channel.overwritePermissions(message.author, { + * VIEW_CHANNEL: false, + * SEND_MESSAGES: null + * }) + * .then(updated => console.log(updated.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + overwritePermissions(userOrRole, options, reason) { + 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 of Object.keys(options)) { + if (options[perm] === true) { + payload.allow |= Permissions.FLAGS[perm] || 0; + payload.deny &= ~(Permissions.FLAGS[perm] || 0); + } else if (options[perm] === false) { + payload.allow &= ~(Permissions.FLAGS[perm] || 0); + payload.deny |= Permissions.FLAGS[perm] || 0; + } else if (options[perm] === null) { + payload.allow &= ~(Permissions.FLAGS[perm] || 0); + payload.deny &= ~(Permissions.FLAGS[perm] || 0); + } + } + + return this.client.rest.methods.setChannelOverwrite(this, payload, reason).then(() => this); + } + + /** + * Locks in the permission overwrites from the parent channel. + * @returns {Promise<GuildChannel>} + */ + lockPermissions() { + if (!this.parent) return Promise.reject(new TypeError('Could not find a parent to this guild channel.')); + const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => ({ + deny: overwrite.deny, + allow: overwrite.allow, + id: overwrite.id, + type: overwrite.type, + })); + return this.edit({ permissionOverwrites }); + } + + /** + * The data for a guild channel. + * @typedef {Object} ChannelData + * @property {string} [type] The type of the channel (Only when creating) + * @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 {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the channel + * @property {CategoryChannel|Snowflake} [parent] The parent or parent ID of the channel + * @property {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites] + * Overwrites of the channel + * @property {number} [rateLimitPerUser] The rate limit per user of the channel in seconds + * @property {string} [reason] Reason for creating the channel (Only when creating) + */ + + /** + * Edits the channel. + * @param {ChannelData} data The new data for the channel + * @param {string} [reason] Reason for editing this channel + * @returns {Promise<GuildChannel>} + * @example + * // Edit a channel + * channel.edit({ name: 'new-channel' }) + * .then(console.log) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateChannel(this, data, reason).then(() => this); + } + + /** + * Set a new name for the guild channel. + * @param {string} name The new name for the guild channel + * @param {string} [reason] Reason for changing the guild channel's name + * @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, reason) { + return this.edit({ name }, reason); + } + + /** + * Set a new position for the guild channel. + * @param {number} position The new position for the guild channel + * @param {boolean} [relative=false] Move the position relative to its current value + * @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, relative) { + return this.guild.setChannelPosition(this, position, relative).then(() => this); + } + + /** + * Set a new parent for the guild channel. + * @param {CategoryChannel|SnowFlake} parent The new parent for the guild channel + * @param {string} [reason] Reason for changing the guild channel's parent + * @returns {Promise<GuildChannel>} + * @example + * // Sets the parent of a channel + * channel.setParent('174674066072928256') + * .then(updated => console.log(`Set the category of ${updated.name} to ${updated.parent.name}`)) + * .catch(console.error); + */ + setParent(parent, reason) { + parent = this.client.resolver.resolveChannelID(parent); + return this.edit({ parent }, reason); + } + + /** + * Set a new topic for the guild channel. + * @param {string} topic The new topic for the guild channel + * @param {string} [reason] Reason for changing the guild channel's topic + * @returns {Promise<GuildChannel>} + * @example + * // Set a new channel topic + * channel.setTopic('Needs more rate limiting') + * .then(updated => console.log(`Channel's new topic is ${updated.topic}`)) + * .catch(console.error); + */ + setTopic(topic, reason) { + return this.edit({ topic }, reason); + } + + /** + * Create an invite to this guild channel. + * <warn>This is only available when using a bot account.</warn> + * @param {Object} [options={}] Options for the invite + * @param {boolean} [options.temporary=false] Whether members that joined via the invite should be automatically + * kicked after 24 hours if they have not yet received a role + * @param {number} [options.maxAge=86400] How long the invite should last (in seconds, 0 for forever) + * @param {number} [options.maxUses=0] Maximum number of uses + * @param {boolean} [options.unique=false] Create a unique invite, or use an existing one with similar settings + * @param {string} [reason] Reason for creating the invite + * @returns {Promise<Invite>} + * @example + * // Create an invite to a channel + * channel.createInvite() + * .then(invite => console.log(`Created an invite with a code of ${invite.code}`)) + * .catch(console.error); + */ + createInvite(options = {}, reason) { + return this.client.rest.methods.createChannelInvite(this, options, reason); + } + + /* eslint-disable max-len */ + /** + * Options to clone a guild channel. + * @typedef {Object} GuildChannelCloneOptions + * @property {string} [name=this.name] Name of the new channel + * @property {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites=this.permissionOverwrites] + * Permission overwrites of the new channel + * @property {string} [type=this.type] Type of the new channel + * @property {string} [topic=this.topic] Topic of the new channel (only text) + * @property {boolean} [nsfw=this.nsfw] Whether the new channel is nsfw (only text) + * @property {number} [bitrate=this.bitrate] Bitrate of the new channel in bits (only voice) + * @property {number} [userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice) + * @property {number} [rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text) + * @property {ChannelResolvable} [parent=this.parent] Parent of the new channel + * @property {string} [reason] Reason for cloning this channel + */ + /* eslint-enable max-len */ + + /** + * Clone this channel. + * @param {string|GuildChannelCloneOptions} [nameOrOptions={}] Name for the new channel. + * **(deprecated, use options)** + * Alternatively options for cloning the channel + * @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites + * **(deprecated, use options)** + * @param {boolean} [withTopic=true] Whether to clone the channel with this channel's topic + * **(deprecated, use options)** + * @param {string} [reason] Reason for cloning this channel **(deprecated, user options)** + * @returns {Promise<GuildChannel>} + * @example + * // Clone a channel + * channel.clone({ topic: null, reason: 'Needed a clone' }) + * .then(clone => console.log(`Cloned ${channel.name} to make a channel called ${clone.name}`)) + * .catch(console.error); + */ + clone(nameOrOptions = {}, withPermissions = true, withTopic = true, reason) { + // If more than one parameter was specified or the first is a string, + // convert them to a compatible options object and issue a warning + if (arguments.length > 1 || typeof nameOrOptions === 'string') { + process.emitWarning( + 'GuildChannel#clone: Clone channels using an options object instead of separate parameters.', + 'Deprecation Warning' + ); + + nameOrOptions = { + name: nameOrOptions, + permissionOverwrites: withPermissions ? this.permissionOverwrites : null, + topic: withTopic ? this.topic : null, + reason: reason || null, + }; + } + + Util.mergeDefault({ + name: this.name, + permissionOverwrites: this.permissionOverwrites, + topic: this.topic, + type: this.type, + nsfw: this.nsfw, + parent: this.parent, + bitrate: this.bitrate, + userLimit: this.userLimit, + rateLimitPerUser: this.rateLimitPerUser, + reason: null, + }, nameOrOptions); + + return this.guild.createChannel(nameOrOptions.name, nameOrOptions); + } + + /** + * Fetches a collection of invites to this guild channel. + * Resolves with a collection mapping invites by their codes. + * @returns {Promise<Collection<string, Invite>>} + */ + fetchInvites() { + return this.client.rest.makeRequest('get', Constants.Endpoints.Channel(this.id).invites, true) + .then(data => { + const invites = new Collection(); + for (let invite of data) { + invite = new Invite(this.client, invite); + invites.set(invite.code, invite); + } + + return invites; + }); + } + + /** + * Deletes this channel. + * @param {string} [reason] Reason for deleting this channel + * @returns {Promise<GuildChannel>} + * @example + * // Delete the channel + * channel.delete('Making room for new channels') + * .then(deleted => console.log(`Deleted ${deleted.name} to make room for new channels`)) + * .catch(console.error); + */ + delete(reason) { + return this.client.rest.methods.deleteChannel(this, reason); + } + + /** + * 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; + } + + /** + * Whether the channel is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + return this.id !== this.guild.id && + this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS); + } + + /** + * Whether the channel is manageable by the client user + * @type {boolean} + * @readonly + */ + get manageable() { + if (this.client.user.id === this.guild.ownerID) return true; + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + return permissions.has([Permissions.FLAGS.MANAGE_CHANNELS, Permissions.FLAGS.VIEW_CHANNEL]); + } + + /** + * Whether the channel is muted + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get muted() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).muted; + } catch (err) { + return false; + } + } + + /** + * The type of message that should notify you + * <warn>This is only available when using a user account.</warn> + * @type {?MessageNotificationType} + * @readonly + * @deprecated + */ + get messageNotifications() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).messageNotifications; + } catch (err) { + return Constants.MessageNotificationTypes[3]; + } + } + + /** + * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. + * @returns {string} + * @example + * // Logs: Hello from <#123456789012345678> + * console.log(`Hello from ${channel}`); + * @example + * // Logs: Hello from <#123456789012345678> + * 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..45b75b3 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildMember.js @@ -0,0 +1,636 @@ +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const Role = require('./Role'); +const Permissions = require('../util/Permissions'); +const Collection = require('../util/Collection'); +const { Presence } = require('./Presence'); +const util = require('util'); + +/** + * 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 member instance Represents + * @type {User} + */ + this.user = {}; + + /** + * The timestamp this member joined the guild at + * @type {number} + */ + this.joinedTimestamp = null; + + /** + * The timestamp of when the member used their Nitro boost on the guild, if it was used + * @type {?number} + */ + this.premiumSinceTimestamp = null; + + this._roles = []; + if (data) this.setup(data); + + /** + * The ID of the last message sent by this member in their guild, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The Message object of the last message sent by this member in their guild, if one was sent + * @type {?Message} + */ + this.lastMessage = null; + + /** + * Whether the member has been removed from the guild + * @type {boolean} + */ + this.deleted = false; + } + + 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; + + /** + * Whether this member is streaming using "Go Live" + * @type {boolean} + */ + this.selfStream = data.self_stream || false; + + /** + * The voice session ID of this member, if any + * @type {?Snowflake} + */ + this.voiceSessionID = data.session_id; + + /** + * The voice channel ID of this member, if any + * @type {?Snowflake} + */ + this.voiceChannelID = data.channel_id; + + /** + * Whether this member is speaking and the client is in the same channel + * @type {boolean} + */ + this.speaking = false; + + /** + * The nickname of this member, if they have one + * @type {?string} + */ + this.nickname = data.nick || null; + + if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); + if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime(); + + this.user = data.user; + this._roles = data.roles; + } + + /** + * The time this member joined the guild + * @type {?Date} + * @readonly + */ + get joinedAt() { + return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null; + } + + /** + * The time of when the member used their Nitro boost on the guild, if it was used + * @type {?Date} + * @readonly + */ + get premiumSince() { + return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null; + } + + /** + * The presence of this member + * @type {Presence} + * @readonly + */ + get presence() { + return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(undefined, this.client); + } + + /** + * A list of roles that are applied to this member, mapped by the role ID + * @type {Collection<Snowflake, 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 this member with the highest position + * @type {Role} + * @readonly + */ + get highestRole() { + return this.roles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); + } + + /** + * The role of this member used to set their color + * @type {?Role} + * @readonly + */ + get colorRole() { + const coloredRoles = this.roles.filter(role => role.color); + if (!coloredRoles.size) return null; + return coloredRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); + } + + /** + * The displayed color of this member in base 10 + * @type {number} + * @readonly + */ + get displayColor() { + const role = this.colorRole; + return (role && role.color) || 0; + } + + /** + * The displayed color of this member in hexadecimal + * @type {string} + * @readonly + */ + get displayHexColor() { + const role = this.colorRole; + return (role && role.hexColor) || '#000000'; + } + + /** + * The role of this member used to hoist them in a separate category in the users list + * @type {?Role} + * @readonly + */ + get hoistRole() { + const hoistedRoles = this.roles.filter(role => role.hoist); + if (!hoistedRoles.size) return null; + return hoistedRoles.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 {Snowflake} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * The nickname of this 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 this member, taking only roles into account + * @type {Permissions} + * @readonly + */ + get permissions() { + if (this.user.id === this.guild.ownerID) return new Permissions(this, Permissions.ALL); + + let permissions = 0; + const roles = this.roles; + for (const role of roles.values()) permissions |= role.permissions; + + return new Permissions(this, permissions); + } + + /** + * Whether this member is manageable in terms of role hierarchy by the client user + * @type {boolean} + * @readonly + */ + get manageable() { + if (this.user.id === this.guild.ownerID) return false; + if (this.user.id === this.client.user.id) return false; + if (this.client.user.id === this.guild.ownerID) return true; + return this.guild.me.highestRole.comparePositionTo(this.highestRole) > 0; + } + + /** + * Whether this member is kickable by the client user + * @type {boolean} + * @readonly + */ + get kickable() { + return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS); + } + + /** + * Whether this member is bannable by the client user + * @type {boolean} + * @readonly + */ + get bannable() { + return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.BAN_MEMBERS); + } + + /** + * Returns `channel.permissionsFor(guildMember)`. Returns permissions for this member in a guild channel, + * taking into account roles and permission overwrites. + * @param {ChannelResolvable} channel The guild channel to use as context + * @returns {?Permissions} + */ + 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 this member's roles have a permission. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission + * **(deprecated)** + * @param {boolean} [checkAdmin] Whether to allow the administrator permission to override + * (takes priority over `explicit`) + * @param {boolean} [checkOwner] Whether to allow being the guild's owner to override + * (takes priority over `explicit`) + * @returns {boolean} + */ + hasPermission(permission, explicit = false, checkAdmin, checkOwner) { + if (typeof checkAdmin === 'undefined') checkAdmin = !explicit; + if (typeof checkOwner === 'undefined') checkOwner = !explicit; + if (checkOwner && this.user.id === this.guild.ownerID) return true; + return this.roles.some(r => r.hasPermission(permission, undefined, checkAdmin)); + } + + /** + * Checks whether the roles of this 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} + * @deprecated + */ + hasPermissions(permissions, explicit = false) { + if (!explicit && this.user.id === this.guild.ownerID) return true; + return this.hasPermission(permissions, explicit); + } + + /** + * Checks whether the roles of this 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) { + if (!(permissions instanceof Array)) permissions = [permissions]; + return this.permissions.missing(permissions, explicit); + } + + /** + * The data for editing this member. + * @typedef {Object} GuildMemberEditData + * @property {string} [nick] The nickname to set for the member + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles or role IDs to apply + * @property {boolean} [mute] Whether or not the member should be muted + * @property {boolean} [deaf] Whether or not the member should be deafened + * @property {ChannelResolvable|null} [channel] Channel to move member to (if they are connected to voice), or `null` + * if you want to kick them from voice + */ + + /** + * Edits this member. + * @param {GuildMemberEditData} data The data to edit the member with + * @param {string} [reason] Reason for editing this user + * @returns {Promise<GuildMember>} + * @example + * // Set a member's nickname and clear their roles + * message.member.edit({ + * nick: 'Cool Name', + * roles: [] + * }) + * .then(console.log) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateGuildMember(this, data, reason); + } + + /** + * Mute/unmute this member. + * @param {boolean} mute Whether or not the member should be muted + * @param {string} [reason] Reason for muting or unmuting + * @returns {Promise<GuildMember>} + * @example + * // Mute a member with a reason + * message.member.setMute(true, 'It needed to be done') + * .then(() => console.log(`Muted ${message.member.displayName}`))) + * .catch(console.error); + */ + setMute(mute, reason) { + return this.edit({ mute }, reason); + } + + /** + * Deafen/undeafen this member. + * @param {boolean} deaf Whether or not the member should be deafened + * @param {string} [reason] Reason for deafening or undeafening + * @returns {Promise<GuildMember>} + * @example + * // Deafen a member + * message.member.setDeaf(true) + * .then(() => console.log(`Deafened ${message.member.displayName}`)) + * .catch(console.error); + */ + setDeaf(deaf, reason) { + return this.edit({ deaf }, reason); + } + + /** + * Moves this member to the given channel. + * @param {ChannelResolvable|null} channel Channel to move the member to, or `null` if you want to kick them from + * voice + * @returns {Promise<GuildMember>} + * @example + * // Moves a member to a voice channel + * member.setVoiceChannel('174674066072928256') + * .then(() => console.log(`Moved ${member.displayName}`)) + * .catch(console.error); + */ + setVoiceChannel(channel) { + return this.edit({ channel }); + } + + /** + * Sets the roles applied to this member. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to apply + * @param {string} [reason] Reason for applying the roles + * @returns {Promise<GuildMember>} + * @example + * // Set the member's roles to a single role + * guildMember.setRoles(['391156570408615936']) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all of the member's roles + * guildMember.setRoles([]) + * .then(member => console.log(`${member.displayName} now has ${member.roles.size} roles`)) + * .catch(console.error); + */ + setRoles(roles, reason) { + return this.edit({ roles }, reason); + } + + /** + * Adds a single role to this member. + * @param {RoleResolvable} role The role or ID of the role to add + * @param {string} [reason] Reason for adding the role + * @returns {Promise<GuildMember>} + * @example + * // Give a role to a member + * message.member.addRole('193654001089118208') + * .then(console.log) + * .catch(console.error); + */ + addRole(role, reason) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); + return this.client.rest.methods.addMemberRole(this, role, reason); + } + + /** + * Adds multiple roles to this member. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to add + * @param {string} [reason] Reason for adding the roles + * @returns {Promise<GuildMember>} + * @example + * // Gives a member a few roles + * message.member.addRoles(['193654001089118208', '369308579892690945']) + * .then(console.log) + * .catch(console.error); + */ + addRoles(roles, reason) { + 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 }, reason); + } + + /** + * Removes a single role from this member. + * @param {RoleResolvable} role The role or ID of the role to remove + * @param {string} [reason] Reason for removing the role + * @returns {Promise<GuildMember>} + * @example + * // Remove a role from a member + * message.member.removeRole('193654001089118208') + * .then(console.log) + * .catch(console.error); + */ + removeRole(role, reason) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); + return this.client.rest.methods.removeMemberRole(this, role, reason); + } + + /** + * Removes multiple roles from this member. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to remove + * @param {string} [reason] Reason for removing the roles + * @returns {Promise<GuildMember>} + * @example + * // Removes a few roles from the member + * message.member.removeRoles(['193654001089118208', '369308579892690945']) + * .then(console.log) + * .catch(console.error); + */ + removeRoles(roles, reason) { + 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 }, reason); + } + + /** + * Set the nickname for this member. + * @param {string} nick The nickname for the guild member + * @param {string} [reason] Reason for setting the nickname + * @returns {Promise<GuildMember>} + * @example + * // Update the member's nickname + * message.member.setNickname('Cool Name') + * .then(console.log) + * .catch(console.error); + */ + setNickname(nick, reason) { + return this.edit({ nick }, reason); + } + + /** + * Creates a DM channel between the client and this member. + * @returns {Promise<DMChannel>} + */ + createDM() { + return this.user.createDM(); + } + + /** + * Deletes any DMs with this guild member. + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.user.deleteDM(); + } + + /** + * Kick this member from the guild. + * @param {string} [reason] Reason for kicking user + * @returns {Promise<GuildMember>} + * @example + * // Kick a member + * member.kick() + * .then(() => console.log(`Kicked ${member.displayName}`)) + * .catch(console.error); + */ + kick(reason) { + return this.client.rest.methods.kickGuildMember(this.guild, this, reason); + } + + /** + * Ban this member. + * @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a + * string, the ban reason. Supplying an object allows you to do both. + * @param {number} [options.days=0] Number of days of messages to delete + * @param {string} [options.reason] Reason for banning + * @returns {Promise<GuildMember>} + * @example + * // Ban a guild member + * member.ban(7) + * .then(() => console.log(`Banned ${member.displayName}`)) + * .catch(console.error); + */ + ban(options) { + return this.guild.ban(this, options); + } + + /** + * 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 + /* eslint-disable no-empty-function */ + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendCode() {} +} + +TextBasedChannel.applyToClass(GuildMember); + +GuildMember.prototype.hasPermissions = util.deprecate(GuildMember.prototype.hasPermissions, + 'GuildMember#hasPermissions is deprecated - use GuildMember#hasPermission, it now takes an array'); +GuildMember.prototype.missingPermissions = util.deprecate(GuildMember.prototype.missingPermissions, + 'GuildMember#missingPermissions is deprecated - use GuildMember#permissions.missing, it now takes an array'); + +module.exports = GuildMember; diff --git a/node_modules/discord.js/src/structures/Integration.js b/node_modules/discord.js/src/structures/Integration.js new file mode 100644 index 0000000..96af017 --- /dev/null +++ b/node_modules/discord.js/src/structures/Integration.js @@ -0,0 +1,151 @@ +/** + * The information account for an integration + * @typedef {Object} IntegrationAccount + * @property {string} id The id of the account + * @property {string} name The name of the account + */ + +/** + * Represents a guild integration. + */ +class Integration { + constructor(client, data, guild) { + /** + * The client that created this integration + * @name Integration#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The guild this integration belongs to + * @type {Guild} + */ + this.guild = guild; + + /** + * The integration id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The integration name + * @type {string} + */ + this.name = data.name; + /** + * The integration type (twitch, youtube, etc) + * @type {string} + */ + this.type = data.type; + + /** + * Whether this integration is enabled + * @type {boolean} + */ + this.enabled = data.enabled; + + /** + * Whether this integration is syncing + * @type {boolean} + */ + this.syncing = data.syncing; + + /** + * The role that this integration uses for subscribers + * @type {Role} + */ + this.role = this.guild.roles.get(data.role_id); + + /** + * The user for this integration + * @type {User} + */ + this.user = this.client.dataManager.newUser(data.user); + + /** + * The account integration information + * @type {IntegrationAccount} + */ + this.account = data.account; + + /** + * The last time this integration was last synced + * @type {number} + */ + this.syncedAt = data.synced_at; + this._patch(data); + } + + _patch(data) { + /** + * The behavior of expiring subscribers + * @type {number} + */ + this.expireBehavior = data.expire_behavior; + + /** + * The grace period before expiring subscribers + * @type {number} + */ + this.expireGracePeriod = data.expire_grace_period; + } + + /** + * Syncs this integration + * @returns {Promise<Integration>} + */ + sync() { + this.syncing = true; + return this.client.rest.methods.syncIntegration(this) + .then(() => { + this.syncing = false; + this.syncedAt = Date.now(); + return this; + }); + } + + /** + * The data for editing an integration. + * @typedef {Object} IntegrationEditData + * @property {number} [expireBehavior] The new behaviour of expiring subscribers + * @property {number} [expireGracePeriod] The new grace period before expiring subscribers + */ + + /** + * Edits this integration. + * @param {IntegrationEditData} data The data to edit this integration with + * @param {string} reason Reason for editing this integration + * @returns {Promise<Integration>} + */ + edit(data, reason) { + if ('expireBehavior' in data) { + data.expire_behavior = data.expireBehavior; + data.expireBehavior = undefined; + } + if ('expireGracePeriod' in data) { + data.expire_grace_period = data.expireGracePeriod; + data.expireGracePeriod = undefined; + } + // The option enable_emoticons is only available for Twitch at this moment + return this.client.rest.methods.editIntegration(this, data, reason) + .then(() => { + this._patch(data); + return this; + }); + } + + /** + * Deletes this integration. + * @returns {Promise<Integration>} + * @param {string} [reason] Reason for deleting this integration + */ + delete(reason) { + return this.client.rest.methods.deleteIntegration(this, reason) + .then(() => this); + } +} + +module.exports = Integration; 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..1d84cf5 --- /dev/null +++ b/node_modules/discord.js/src/structures/Invite.js @@ -0,0 +1,164 @@ +const PartialGuild = require('./PartialGuild'); +const PartialGuildChannel = require('./PartialGuildChannel'); +const Constants = require('../util/Constants'); + +/** + * Represents an invitation to a guild channel. + * <warn>The only guaranteed properties are `code`, `url`, `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; + + /** + * The approximate number of online members of the guild this invite is for + * @type {number} + */ + this.presenceCount = data.approximate_presence_count; + + /** + * The approximate total number of members of the guild this invite is for + * @type {number} + */ + this.memberCount = data.approximate_member_count; + + /** + * The number of text channels the guild this invite goes to has + * @type {number} + */ + this.textChannelCount = data.guild.text_channel_count; + + /** + * The number of voice channels the guild this invite goes to has + * @type {number} + */ + this.voiceChannelCount = data.guild.voice_channel_count; + + /** + * 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. + * @param {string} [reason] Reason for deleting this invite + * @returns {Promise<Invite>} + */ + delete(reason) { + return this.client.rest.methods.deleteInvite(this, reason); + } + + /** + * 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..c71844f --- /dev/null +++ b/node_modules/discord.js/src/structures/Message.js @@ -0,0 +1,661 @@ +const Mentions = require('./MessageMentions'); +const Attachment = require('./MessageAttachment'); +const Embed = require('./MessageEmbed'); +const RichEmbed = require('./RichEmbed'); +const MessageReaction = require('./MessageReaction'); +const ReactionCollector = require('./ReactionCollector'); +const Util = require('../util/Util'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); +const Permissions = require('../util/Permissions'); +const MessageFlags = require('../util/MessageFlags'); +let GuildMember; + +/** + * 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; + + /** + * Whether this message has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this.setup(data); + } + + setup(data) { // eslint-disable-line complexity + /** + * The ID of the message + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The type of the message + * @type {MessageType} + */ + 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, !data.webhook_id); + + /** + * 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 or string used for checking message delivery + * <warn>This is only received after the message was sent successfully, and + * lost if re-fetched</warn> + * @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 !== 0; + + /** + * 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<Snowflake, 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; + + /** + * A collection of reactions to this message, mapped by the reaction ID + * @type {Collection<Snowflake, 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)); + } + } + + /** + * All valid mentions that the message contains + * @type {MessageMentions} + */ + this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone, data.mention_channels); + + /** + * ID of the webhook that sent the message, if applicable + * @type {?Snowflake} + */ + this.webhookID = data.webhook_id || null; + + /** + * Whether this message is a hit in a search + * @type {?boolean} + */ + this.hit = typeof data.hit === 'boolean' ? data.hit : null; + + /** + * Flags that are applied to the message + * @type {Readonly<MessageFlags>} + */ + this.flags = new MessageFlags(data.flags).freeze(); + + /** + * Reference data sent in a crossposted message. + * @typedef {Object} MessageReference + * @property {string} channelID ID of the channel the message was crossposted from + * @property {?string} guildID ID of the guild the message was crossposted from + * @property {?string} messageID ID of the message that was crossposted + */ + + /** + * Message reference data + * @type {?MessageReference} + */ + this.reference = data.message_reference ? { + channelID: data.message_reference.channel_id, + guildID: data.message_reference.guild_id, + messageID: data.message_reference.message_id, + } : null; + + /** + * The previous versions of the message, sorted with the most recent first + * @type {Message[]} + * @private + */ + this._edits = []; + + if (data.member && this.guild && this.author && !this.guild.members.has(this.author.id)) { + this.guild._addMember(Object.assign(data.member, { user: this.author }), false); + } + + /** + * 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; + } + + /** + * Updates the message. + * @param {Object} data Raw Discord message update data + * @private + */ + patch(data) { + const clone = Util.cloneObject(this); + this._edits.unshift(clone); + + if ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime(); + if ('content' in data) this.content = data.content; + if ('pinned' in data) this.pinned = data.pinned; + if ('tts' in data) this.tts = data.tts; + if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(this, e)); + else this.embeds = this.embeds.slice(); + + if ('attachments' in data) { + this.attachments = new Collection(); + for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment)); + } else { + this.attachments = new Collection(this.attachments); + } + + this.mentions = new Mentions( + this, + 'mentions' in data ? data.mentions : this.mentions.users, + 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, + 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, + 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels + ); + + this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); + } + + /** + * 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 url to jump to the message + * @type {string} + * @readonly + */ + get url() { + return `https://discordapp.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`; + } + + /** + * 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; + }); + } + + /** + * Creates a reaction collector. + * @param {CollectorFilter} filter The filter to apply + * @param {ReactionCollectorOptions} [options={}] Options to send to the collector + * @returns {ReactionCollector} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID' + * const collector = message.createReactionCollector(filter, { time: 15000 }); + * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createReactionCollector(filter, options = {}) { + return new ReactionCollector(this, filter, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {ReactionCollectorOptions} AwaitReactionsOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createMessageCollector but in promise form. + * Resolves with a collection of reactions that pass the specified filter. + * @param {CollectorFilter} filter The filter function to use + * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise<Collection<string, MessageReaction>>} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID' + * message.awaitReactions(filter, { time: 15000 }) + * .then(collected => console.log(`Collected ${collected.size} reactions`)) + * .catch(console.error); + */ + awaitReactions(filter, options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createReactionCollector(filter, options); + collector.once('end', (reactions, reason) => { + if (options.errors && options.errors.includes(reason)) reject(reactions); + else resolve(reactions); + }); + }); + } + + /** + * 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.deleted && (this.author.id === this.client.user.id || (this.guild && + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES) + )); + } + + /** + * Whether the message is pinnable by the client user + * @type {boolean} + * @readonly + */ + get pinnable() { + return this.type === 'DEFAULT' && (!this.guild || + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.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 The member/user to check for a mention of + * @returns {boolean} + */ + isMemberMentioned(member) { + // Lazy-loading is used here to get around a circular dependency that breaks things + if (!GuildMember) GuildMember = require('./GuildMember'); + if (this.mentions.everyone) return true; + if (this.mentions.users.has(member.id)) return true; + if (member instanceof 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 + * @property {MessageFlagsResolvable} [flags] Message flags to apply + */ + + /** + * Edit the content of the message. + * @param {StringResolvable} [content] The new content for the message + * @param {MessageEditOptions|RichEmbed} [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(`New message content: ${msg}`)) + * .catch(console.error); + */ + edit(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + if (options instanceof RichEmbed) options = { embed: options }; + return this.client.rest.methods.updateMessage(this, content, options); + } + + /** + * Edit the content of the message, with a code block. + * @param {string} lang The language for the code block + * @param {StringResolvable} content The new content for the message + * @returns {Promise<Message>} + * @deprecated + */ + editCode(lang, content) { + content = Util.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 The emoji to react with + * @returns {Promise<MessageReaction>} + * @example + * // React to a message with a unicode emoji + * message.react('🤔') + * .then(console.log) + * .catch(console.error); + * @example + * // React to a message with a custom emoji + * message.react(message.guild.emojis.get('123456789012345678')) + * .then(console.log) + * .catch(console.error); + */ + 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.username}`)) + * .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(sent => console.log(`Sent a reply to ${sent.author.username}`)) + * .catch(console.error); + */ + reply(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.channel.send(content, Object.assign(options, { reply: this.member || this.author })); + } + + /** + * Marks the message as read. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<Message>} + * @deprecated + */ + acknowledge() { + return this.client.rest.methods.ackMessage(this); + } + + /** + * 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); + } + + /** + * Suppresses or unsuppresses embeds on a message + * @param {boolean} [suppress=true] If the embeds should be suppressed or not + * @returns {Promise<Message>} + */ + suppressEmbeds(suppress = true) { + const flags = new MessageFlags(this.flags.bitfield); + + if (suppress) { + flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } else { + flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } + + return this.edit(undefined, { flags }); + } + + /** + * 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; + } + + _removeReaction(emoji, user) { + const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; + if (this.reactions.has(emojiID)) { + const reaction = this.reactions.get(emojiID); + if (!user) { + this.reactions.delete(emojiID); + return reaction; + } + if (reaction.users.has(user.id)) { + reaction.users.delete(user.id); + reaction.count--; + if (user.id === this.client.user.id) reaction.me = false; + if (reaction.count <= 0) this.reactions.delete(emojiID); + 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..6db15c8 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageAttachment.js @@ -0,0 +1,79 @@ +const { basename } = require('path'); + +/** + * 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 {Snowflake} + */ + 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; + } + + /** + * Whether or not this attachment has been marked as a spoiler + * @type {boolean} + * @readonly + */ + get spoiler() { + return basename(this.url).startsWith('SPOILER_'); + } +} + +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..acd82be --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageCollector.js @@ -0,0 +1,97 @@ +const Collector = require('./interfaces/Collector'); +const util = require('util'); + +/** + * @typedef {CollectorOptions} MessageCollectorOptions + * @property {number} max The maximum amount of messages to process + * @property {number} maxMatches The maximum amount of messages to collect + */ + +/** + * Collects messages on a channel. + * @extends {Collector} + */ +class MessageCollector extends Collector { + /** + * @param {TextChannel|DMChannel|GroupDMChannel} channel The channel + * @param {CollectorFilter} filter The filter to be applied to this collector + * @param {MessageCollectorOptions} options The options to be applied to this collector + * @emits MessageCollector#message + */ + constructor(channel, filter, options = {}) { + super(channel.client, filter, options); + + /** + * The channel + * @type {TextBasedChannel} + */ + this.channel = channel; + + /** + * Total number of messages that were received in the channel during message collection + * @type {number} + */ + this.received = 0; + + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); + this.client.on('message', this.listener); + + this._reEmitter = message => { + /** + * Emitted when the collector receives a message. + * @event MessageCollector#message + * @param {Message} message The message + * @deprecated + */ + this.emit('message', message); + }; + this.on('collect', this._reEmitter); + } + + // Remove in v12 + on(eventName, listener) { + if (eventName === 'message') { + listener = util.deprecate(listener, 'MessageCollector will soon no longer emit "message", use "collect" instead'); + } + super.on(eventName, listener); + } + + /** + * Handle an incoming message for possible collection. + * @param {Message} message The message that could be collected + * @returns {?{key: Snowflake, value: Message}} + * @private + */ + handle(message) { + if (message.channel.id !== this.channel.id) return null; + this.received++; + return { + key: message.id, + value: message, + }; + } + + /** + * Check after collection to see if the collector is done. + * @returns {?string} Reason to end the collector, if any + * @private + */ + postCheck() { + // Consider changing the end reasons for v12 + if (this.options.maxMatches && this.collected.size >= this.options.maxMatches) return 'matchesLimit'; + if (this.options.max && this.received >= this.options.max) return 'limit'; + return null; + } + + /** + * Removes event listeners. + * @private + */ + cleanup() { + this.removeListener('collect', this._reEmitter); + this.client.removeListener('message', this.listener); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); + } +} + +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..2a87679 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageEmbed.js @@ -0,0 +1,386 @@ +/** + * Represents an embed in a message (image/video preview, rich embed, etc.) + * <info>This class is only used for *received* embeds. If you wish to send one, use the {@link RichEmbed} class.</info> + */ +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 + * @type {?string} + */ + this.title = data.title; + + /** + * The description of this embed + * @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.timestamp = data.timestamp; + + /** + * The thumbnail of this embed + * @type {?MessageEmbedThumbnail} + */ + this.thumbnail = data.thumbnail ? new MessageEmbedThumbnail(this, data.thumbnail) : null; + + /** + * The image of this embed + * @type {?MessageEmbedImage} + */ + this.image = data.image ? new MessageEmbedImage(this, data.image) : null; + + /** + * The video of this embed + * @type {?MessageEmbedVideo} + */ + this.video = data.video ? new MessageEmbedVideo(this, data.video) : null; + + /** + * The author of this embed + * @type {?MessageEmbedAuthor} + */ + this.author = data.author ? new MessageEmbedAuthor(this, data.author) : null; + + /** + * The provider of this embed + * @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} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The hexadecimal version of the embed color, with a leading hash + * @type {?string} + * @readonly + */ + get hexColor() { + if (!this.color) return null; + 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 an image for a message embed. + */ +class MessageEmbedImage { + constructor(embed, data) { + /** + * The embed this image is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The URL for this image + * @type {string} + */ + this.url = data.url; + + /** + * The Proxy URL for this image + * @type {string} + */ + this.proxyURL = data.proxy_url; + + /** + * The height of the image + * @type {number} + */ + this.height = data.height; + + /** + * The width of the image + * @type {number} + */ + this.width = data.width; + } +} + +/** + * Represents a video for a message embed. + */ +class MessageEmbedVideo { + constructor(embed, data) { + /** + * The embed this video is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The source URL for this video + * @type {string} + */ + this.url = data.url; + + /** + * The height of the video + * @type {number} + */ + this.height = data.height; + + /** + * The width of the video + * @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.Image = MessageEmbedImage; +MessageEmbed.Video = MessageEmbedVideo; +MessageEmbed.Provider = MessageEmbedProvider; +MessageEmbed.Author = MessageEmbedAuthor; +MessageEmbed.Field = MessageEmbedField; +MessageEmbed.Footer = MessageEmbedFooter; + +module.exports = MessageEmbed; diff --git a/node_modules/discord.js/src/structures/MessageMentions.js b/node_modules/discord.js/src/structures/MessageMentions.js new file mode 100644 index 0000000..47aff55 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageMentions.js @@ -0,0 +1,185 @@ +const Collection = require('../util/Collection'); +const { ChannelTypes } = require('../util/Constants'); + +/** + * Keeps track of mentions in a {@link Message}. + */ +class MessageMentions { + constructor(message, users, roles, everyone, crosspostedChannels) { + /** + * Whether `@everyone` or `@here` were mentioned + * @type {boolean} + */ + this.everyone = Boolean(everyone); + + if (users) { + if (users instanceof Collection) { + /** + * Any users that were mentioned + * <info>Order as received from the API, not as they appear in the message content</info> + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(users); + } else { + this.users = new Collection(); + for (const mention of users) { + let user = message.client.users.get(mention.id); + if (!user) user = message.client.dataManager.newUser(mention); + this.users.set(user.id, user); + if (mention.member && message.guild && !message.guild.members.has(mention.id)) { + message.guild._addMember(Object.assign(mention.member, { user }), false); + } + } + } + } else { + this.users = new Collection(); + } + + if (roles) { + if (roles instanceof Collection) { + /** + * Any roles that were mentioned + * <info>Order as received from the API, not as they appear in the message content</ + * @type {Collection<Snowflake, Role>} + */ + this.roles = new Collection(roles); + } else { + this.roles = new Collection(); + for (const mention of roles) { + const role = message.channel.guild.roles.get(mention); + if (role) this.roles.set(role.id, role); + } + } + } else { + this.roles = new Collection(); + } + + /** + * Content of the message + * @type {Message} + * @private + */ + this._content = message.content; + + /** + * The client the message is from + * @type {Client} + * @private + */ + this._client = message.client; + + /** + * The guild the message is in + * @type {?Guild} + * @private + */ + this._guild = message.channel.guild; + + /** + * Cached members for {@MessageMention#members} + * @type {?Collection<Snowflake, GuildMember>} + * @private + */ + this._members = null; + + /** + * Cached channels for {@MessageMention#channels} + * @type {?Collection<Snowflake, GuildChannel>} + * @private + */ + this._channels = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {Snowflake} channelID ID of the mentioned channel + * @property {Snowflake} guildID ID of the guild that has the channel + * @property {string} type Type of the channel + * @property {string} name Name of the channel + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * @type {Collection<Snowflake, CrosspostedChannel>} + */ + this.crosspostedChannels = new Collection(crosspostedChannels); + } else { + this.crosspostedChannels = new Collection(); + const channelTypes = Object.keys(ChannelTypes); + for (const d of crosspostedChannels) { + const type = channelTypes[d.type]; + this.crosspostedChannels.set(d.id, { + channelID: d.id, + guildID: d.guild_id, + type: type ? type.toLowerCase() : 'unknown', + name: d.name, + }); + } + } + } else { + this.crosspostedChannels = new Collection(); + } + } + + /** + * Any members that were mentioned (only in {@link TextChannel}s) + * <info>Order as received from the API, not as they appear in the message content</ + * @type {?Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + if (this._members) return this._members; + if (!this._guild) return null; + this._members = new Collection(); + this.users.forEach(user => { + const member = this._guild.member(user); + if (member) this._members.set(member.user.id, member); + }); + return this._members; + } + + /** + * Any channels that were mentioned + * <info>Order as they appear first in the message content</info> + * @type {Collection<Snowflake, GuildChannel>} + * @readonly + */ + get channels() { + if (this._channels) return this._channels; + this._channels = new Collection(); + let matches; + while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) { + const chan = this._client.channels.get(matches[1]); + if (chan) this._channels.set(chan.id, chan); + } + return this._channels; + } +} + +/** + * Regular expression that globally matches `@everyone` and `@here` + * @type {RegExp} + */ +MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; + +/** + * Regular expression that globally matches user mentions like `<@81440962496172032>` + * @type {RegExp} + */ +MessageMentions.USERS_PATTERN = /<@!?[0-9]+>/g; + +/** + * Regular expression that globally matches role mentions like `<@&297577916114403338>` + * @type {RegExp} + */ +MessageMentions.ROLES_PATTERN = /<@&[0-9]+>/g; + +/** + * Regular expression that globally matches channel mentions like `<#222079895583457280>` + * @type {RegExp} + */ +MessageMentions.CHANNELS_PATTERN = /<#([0-9]+)>/g; + +module.exports = MessageMentions; 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..9109e18 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageReaction.js @@ -0,0 +1,107 @@ +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<Snowflake, User>} + */ + this.users = new Collection(); + + this._emoji = new ReactionEmoji(this, emoji); + } + + /** + * 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} + * @readonly + */ + 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] The user to remove the reaction of + * @returns {Promise<MessageReaction>} + */ + remove(user = this.message.client.user) { + const message = this.message; + const userID = this.message.client.resolver.resolveUserID(user); + if (!userID) return Promise.reject(new Error('Couldn\'t resolve the user ID to remove from the reaction.')); + return message.client.rest.methods.removeMessageReaction( + message, this.emoji.identifier, userID + ); + } + + /** + * Removes this reaction from the message + * @returns {Promise<MessageReaction>} + */ + removeAll() { + const message = this.message; + return message.client.rest.methods.removeMessageReactionEmoji( + message, this.emoji.identifier + ); + } + + /** + * 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 + * @param {Object} [options] Options to fetch users + * @param {Snowflake} [options.before] Limit fetching users to those with an id lower than the supplied id + * @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id + * @returns {Promise<Collection<Snowflake, User>>} + */ + fetchUsers(limit = 100, { after, before } = {}) { + const message = this.message; + return message.client.rest.methods.getMessageReactionUsers( + message, this.emoji.identifier, { after, before, limit } + ).then(data => { + const users = new Collection(); + for (const rawUser of data) { + const user = this.message.client.dataManager.newUser(rawUser); + this.users.set(user.id, user); + users.set(user.id, user); + } + return users; + }); + } +} + +module.exports = MessageReaction; diff --git a/node_modules/discord.js/src/structures/NewsChannel.js b/node_modules/discord.js/src/structures/NewsChannel.js new file mode 100644 index 0000000..bcbfbf3 --- /dev/null +++ b/node_modules/discord.js/src/structures/NewsChannel.js @@ -0,0 +1,24 @@ +const TextChannel = require('./TextChannel'); + +/** + * Represents a guild news channel on Discord. + * @extends {TextChannel} + */ +class NewsChannel extends TextChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'news'; + } + + setup(data) { + super.setup(data); + + /** + * The ratelimit per user for this channel (always 0) + * @type {number} + */ + this.rateLimitPerUser = 0; + } +} + +module.exports = NewsChannel; 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..710aae8 --- /dev/null +++ b/node_modules/discord.js/src/structures/OAuth2Application.js @@ -0,0 +1,157 @@ +const Snowflake = require('../util/Snowflake'); +const Team = require('./Team'); +const util = require('util'); + +/** + * 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 {Snowflake} + */ + 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 {?string[]} + */ + this.rpcOrigins = data.rpc_origins; + + /** + * The app's redirect URIs + * @type {string[]} + */ + this.redirectURIs = data.redirect_uris; + + /** + * If this app's bot requires a code grant when using the OAuth2 flow + * @type {boolean} + */ + this.botRequireCodeGrant = data.bot_require_code_grant; + + /** + * If this app's bot is public + * @type {boolean} + */ + this.botPublic = data.bot_public; + + /** + * If this app can use rpc + * @type {boolean} + */ + this.rpcApplicationState = data.rpc_application_state; + + /** + * Object containing basic info about this app's bot + * @type {Object} + */ + this.bot = data.bot; + + /** + * The flags for the app + * @type {number} + */ + this.flags = data.flags; + + /** + * OAuth2 secret for the application + * @type {boolean} + */ + this.secret = data.secret; + + if (data.owner) { + /** + * The owner of this OAuth application + * @type {?User} + */ + this.owner = this.client.dataManager.newUser(data.owner); + } + + /** + * The owning team of this OAuth application + * <info>In v12.0.0 this property moves to `Team#owner`.</info> + * @type {?Team} + * @deprecated + */ + this.team = data.team ? new Team(this.client, data.team) : null; + } + + /** + * The timestamp the app was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the app was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Reset the app's secret and bot token. + * <warn>This is only available when using a user account.</warn> + * @returns {OAuth2Application} + * @deprecated + */ + reset() { + return this.client.rest.methods.resetApplication(this.id); + } + + /** + * When concatenated with a string, this automatically concatenates the app name rather than the app object. + * @returns {string} + */ + toString() { + return this.name; + } +} + +OAuth2Application.prototype.reset = + util.deprecate(OAuth2Application.prototype.reset, 'OAuth2Application#reset: userbot methods will be removed'); + +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..3eb64f2 --- /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 {Snowflake} + */ + this.id = data.id; + + /** + * The name of this guild + * @type {string} + */ + this.name = data.name; + + /** + * The hash of this guild's icon + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image (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..c30c054 --- /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 {Snowflake} + */ + 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..ebb78fb --- /dev/null +++ b/node_modules/discord.js/src/structures/PermissionOverwrites.js @@ -0,0 +1,69 @@ +const Permissions = require('../util/Permissions'); + +/** + * 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 {Snowflake} + */ + this.id = data.id; + + /** + * The type of this overwrite + * @type {string} + */ + this.type = data.type; + + /** + * The permissions that are denied for the user or role as a bitfield. + * @type {number} + */ + this.deny = data.deny; + + /** + * The permissions that are allowed for the user or role as a bitfield. + * @type {number} + */ + this.allow = data.allow; + + /** + * The permissions that are denied for the user or role. + * @type {Permissions} + * @deprecated + */ + this.denied = new Permissions(data.deny).freeze(); + + /** + * The permissions that are allowed for the user or role. + * @type {Permissions} + * @deprecated + */ + this.allowed = new Permissions(data.allow).freeze(); + } + + /** + * Delete this Permission Overwrite. + * @param {string} [reason] Reason for deleting this overwrite + * @returns {Promise<PermissionOverwrites>} + */ + delete(reason) { + return this.channel.client.rest.methods.deletePermissionOverwrites(this, reason); + } +} + +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..18cc3cc --- /dev/null +++ b/node_modules/discord.js/src/structures/Presence.js @@ -0,0 +1,301 @@ +const { ActivityFlags, Endpoints } = require('../util/Constants'); +const ReactionEmoji = require('./ReactionEmoji'); + +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`offline`** - user is offline or invisible + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} PresenceStatus + */ + +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} ClientPresenceStatus + */ + +/** + * Represents a user's presence. + */ +class Presence { + constructor(data = {}, client) { + /** + * The client that instantiated this + * @name Presence#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.update(data); + } + + update(data) { + /** + * The status of this presence: + * @type {PresenceStatus} + */ + this.status = data.status || this.status || 'offline'; + + /** + * The game that the user is playing + * @type {?Game} + * @deprecated + */ + this.game = data.game ? new Game(data.game, this) : null; + + if (data.activities) { + /** + * The activities of this presence + * @type {Game[]} + */ + this.activities = data.activities.map(activity => new Game(activity, this)); + } else if (data.activity || data.game) { + this.activities = [new Game(data.activity || data.game, this)]; + } else { + this.activities = []; + } + + /** + * The devices this presence is on + * @type {?Object} + * @property {?ClientPresenceStatus} web The current presence in the web application + * @property {?ClientPresenceStatus} mobile The current presence in the mobile application + * @property {?ClientPresenceStatus} desktop The current presence in the desktop application + */ + this.clientStatus = data.client_status || null; + } + + /** + * Whether this presence is equal to another + * @param {Presence} presence The presence to compare with + * @returns {boolean} + */ + equals(presence) { + return this === presence || ( + presence && + this.status === presence.status && + this.activities.length === presence.activities.length && + this.activities.every((activity, index) => activity.equals(presence.activities[index])) && + this.clientStatus.web === presence.clientStatus.web && + this.clientStatus.mobile === presence.clientStatus.mobile && + this.clientStatus.desktop === presence.clientStatus.desktop + ); + } +} + +/** + * Represents a game that is part of a user's presence. + */ +class Game { + constructor(data, presence) { + Object.defineProperty(this, 'presence', { value: presence }); + + /** + * The name of the game being played + * @type {string} + */ + this.name = data.name; + + /** + * The type of the game status, its possible values: + * - 0: Playing + * - 1: Streaming + * - 2: Listening + * - 3: Watching + * @type {number} + */ + this.type = data.type; + + /** + * If the game is being streamed, a link to the stream + * @type {?string} + */ + this.url = data.url || null; + + /** + * Details about the activity + * @type {?string} + */ + this.details = data.details || null; + + /** + * State of the activity + * @type {?string} + */ + this.state = data.state || null; + + /** + * Application ID associated with this activity + * @type {?Snowflake} + */ + this.applicationID = data.application_id || null; + + /** + * Timestamps for the activity + * @type {?Object} + * @prop {?Date} start When the activity started + * @prop {?Date} end When the activity will end + */ + this.timestamps = data.timestamps ? { + start: data.timestamps.start ? new Date(Number(data.timestamps.start)) : null, + end: data.timestamps.end ? new Date(Number(data.timestamps.end)) : null, + } : null; + + /** + * Party of the activity + * @type {?Object} + * @prop {?string} id ID of the party + * @prop {number[]} size Size of the party as `[current, max]` + */ + this.party = data.party || null; + + /** + * Assets for rich presence + * @type {?RichPresenceAssets} + */ + this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null; + + if (data.emoji) { + /** + * Emoji for a custom activity + * <warn>There is no `reaction` property for this emoji.</warn> + * @type {?ReactionEmoji} + */ + this.emoji = new ReactionEmoji({ message: { client: this.presence.client } }, data.emoji); + this.emoji.reaction = null; + } else { + this.emoji = null; + } + + + /** + * Creation date of the activity + * @type {number} + */ + this.createdTimestamp = new Date(data.created_at).getTime(); + + this.syncID = data.sync_id; + this._flags = data.flags; + } + + /** + * The time the activity was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Flags that describe the activity + * @type {ActivityFlags[]} + */ + get flags() { + const flags = []; + for (const [name, flag] of Object.entries(ActivityFlags)) { + if ((this._flags & flag) === flag) flags.push(name); + } + return flags; + } + + /** + * Whether or not the game is being streamed + * @type {boolean} + * @readonly + */ + get streaming() { + return this.type === 1; + } + + /** + * When concatenated with a string, this automatically returns the game's name instead of the Game object. + * @returns {string} + */ + toString() { + return this.name; + } + + /** + * Whether this game is equal to another game + * @param {Game} game The game to compare with + * @returns {boolean} + */ + equals(game) { + return this === game || ( + game && + this.name === game.name && + this.type === game.type && + this.url === game.url + ); + } +} + +/** + * Assets for a rich presence + */ +class RichPresenceAssets { + constructor(game, assets) { + Object.defineProperty(this, 'game', { value: game }); + + /** + * Hover text for the large image + * @type {?string} + */ + this.largeText = assets.large_text || null; + + /** + * Hover text for the small image + * @type {?string} + */ + this.smallText = assets.small_text || null; + + /** + * ID of the large image asset + * @type {?Snowflake} + */ + this.largeImage = assets.large_image || null; + + /** + * ID of the small image asset + * @type {?Snowflake} + */ + this.smallImage = assets.small_image || null; + } + + /** + * The URL of the small image asset + * @type {?string} + * @readonly + */ + get smallImageURL() { + if (!this.smallImage) return null; + return Endpoints.CDN(this.game.presence.client.options.http.cdn) + .AppAsset(this.game.applicationID, this.smallImage); + } + + /** + * The URL of the large image asset + * @type {?string} + * @readonly + */ + get largeImageURL() { + if (!this.largeImage) return null; + if (/^spotify:/.test(this.largeImage)) { + return `https://i.scdn.co/image/${this.largeImage.slice(8)}`; + } else if (/^twitch:/.test(this.largeImage)) { + return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`; + } + return Endpoints.CDN(this.game.presence.client.options.http.cdn) + .AppAsset(this.game.applicationID, this.largeImage); + } +} + +exports.Presence = Presence; +exports.Game = Game; +exports.RichPresenceAssets = RichPresenceAssets; diff --git a/node_modules/discord.js/src/structures/ReactionCollector.js b/node_modules/discord.js/src/structures/ReactionCollector.js new file mode 100644 index 0000000..2b3235e --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionCollector.js @@ -0,0 +1,89 @@ +const Collector = require('./interfaces/Collector'); +const Collection = require('../util/Collection'); + +/** + * @typedef {CollectorOptions} ReactionCollectorOptions + * @property {number} max The maximum total amount of reactions to collect + * @property {number} maxEmojis The maximum number of emojis to collect + * @property {number} maxUsers The maximum number of users to react + */ + +/** + * Collects reactions on messages. + * @extends {Collector} + */ +class ReactionCollector extends Collector { + /** + * @param {Message} message The message upon which to collect reactions + * @param {CollectorFilter} filter The filter to apply to this collector + * @param {ReactionCollectorOptions} [options={}] The options to apply to this collector + */ + constructor(message, filter, options = {}) { + super(message.client, filter, options); + + /** + * The message + * @type {Message} + */ + this.message = message; + + /** + * The users which have reacted + * @type {Collection} + */ + this.users = new Collection(); + + /** + * The total number of reactions collected + * @type {number} + */ + this.total = 0; + + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); + this.client.on('messageReactionAdd', this.listener); + + this.on('fullCollect', (reaction, user) => { + this.users.set(user.id, user); + this.total++; + }); + } + + /** + * Handle an incoming reaction for possible collection. + * @param {MessageReaction} reaction The reaction to possibly collect + * @returns {?{key: Snowflake, value: MessageReaction}} + * @private + */ + handle(reaction) { + if (reaction.message.id !== this.message.id) return null; + return { + key: reaction.emoji.id || reaction.emoji.name, + value: reaction, + }; + } + + /** + * Check after collection to see if the collector is done. + * @param {MessageReaction} reaction The reaction that was collected + * @param {User} user The user that reacted + * @returns {?string} Reason to end the collector, if any + * @private + */ + postCheck() { + if (this.options.max && this.total >= this.options.max) return 'limit'; + if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) return 'emojiLimit'; + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; + return null; + } + + /** + * Remove event listeners. + * @private + */ + cleanup() { + this.client.removeListener('messageReactionAdd', this.listener); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); + } +} + +module.exports = ReactionCollector; 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..9f7597e --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionEmoji.js @@ -0,0 +1,98 @@ +const Constants = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +/** + * 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, emoji) { + /** + * The client that instantiated this object + * @name ReactionEmoji#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: reaction.message.client }); + + /** + * The message reaction this emoji refers to + * @type {MessageReaction} + */ + this.reaction = reaction; + + /** + * The name of this reaction emoji + * @type {string} + */ + this.name = emoji.name; + + /** + * The ID of this reaction emoji + * @type {?Snowflake} + */ + this.id = emoji.id; + + /** + * Whether this reaction emoji is animated + * @type {boolean} + */ + this.animated = emoji.animated || false; + } + + /** + * The timestamp the reaction emoji was created at, or null if unicode + * @type {?number} + * @readonly + */ + get createdTimestamp() { + if (!this.id) return null; + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the reaction emoji was created, or null if unicode + * @type {?Date} + * @readonly + */ + get createdAt() { + if (!this.id) return null; + return new Date(this.createdTimestamp); + } + + /** + * The URL to the reaction emoji file, or null if unicode + * @type {string} + * @readonly + */ + get url() { + if (!this.id) return null; + return Constants.Endpoints.CDN(this.client.options.http.cdn).Emoji(this.id, this.animated ? 'gif' : 'png'); + } + + /** + * The identifier of this emoji, used for message reactions + * @type {string} + * @readonly + */ + get identifier() { + if (this.id) return `${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * 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.send(`The emoji used is ${reaction.emoji}`); + * @returns {string} + */ + toString() { + if (!this.id) return this.name; + + return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; + } +} + +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..5ccb22d --- /dev/null +++ b/node_modules/discord.js/src/structures/RichEmbed.js @@ -0,0 +1,331 @@ +const Attachment = require('./Attachment'); +const MessageEmbed = require('./MessageEmbed'); +const util = require('../util/Util'); +let ClientDataResolver; + +/** + * A rich embed to be sent with a message with a fluent interface for creation. + * @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; + + /** + * File to upload alongside this Embed + * @type {FileOptions|string|Attachment} + */ + this.file = data.file; + + /** + * The files to upload alongside this Embed + * @type {Array<FileOptions|string|Attachment>} + */ + this.files = []; + } + + /** + * Sets the title of this embed. + * @param {StringResolvable} title The title + * @returns {RichEmbed} This embed + */ + setTitle(title) { + title = util.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 = util.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 {ColorResolvable} color The color of the embed + * @returns {RichEmbed} This embed + */ + setColor(color) { + if (!ClientDataResolver) ClientDataResolver = require('../client/ClientDataResolver'); + this.color = ClientDataResolver.resolveColor(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: util.resolveString(name), icon_url: icon, url }; + return this; + } + + /** + * Sets the timestamp of this embed. + * @param {Date|number} [timestamp=Date.now()] The timestamp or date + * @returns {RichEmbed} This embed + */ + setTimestamp(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + 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.'); + this.fields.push(this.constructor.normalizeField(name, value, inline)); + return this; + } + + /** + * Convenience function for `<RichEmbed>.addField('\u200B', '\u200B', inline)`. + * @param {boolean} [inline=false] Set the field to display inline + * @returns {RichEmbed} This embed + */ + addBlankField(inline = false) { + return this.addField('\u200B', '\u200B', inline); + } + + /** + * @typedef {Object} EmbedField + * @property {string} name The name of this field + * @property {string} value The value of this field + * @property {boolean} inline If this field will be displayed inline + */ + + /** + * @typedef {Object} EmbedFieldData + * @property {StringResolvable} name The name of this field + * @property {StringResolvable} value The value of this field + * @property {boolean} [inline=false] If this field will be displayed inline + */ + + /** + * Removes, replaces, and inserts fields in the embed (max 25). + * @param {number} index The index to start at + * @param {number} deleteCount The number of fields to remove + * @param {...EmbedFieldData} [fields] The replacing field objects + * @returns {RichEmbed} + */ + spliceFields(index, deleteCount, ...fields) { + if (fields) { + const mapper = ({ name, value, inline }) => this.constructor.normalizeField(name, value, inline); + this.fields.splice(index, deleteCount, ...fields.map(mapper)); + } else { + this.fields.splice(index, deleteCount); + } + 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 image + * @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 = util.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; + } + + /** + * Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when + * setting an embed image or author/footer icons. Only one file may be attached. + * @param {FileOptions|string|Attachment} file Local path or URL to the file to attach, + * or valid FileOptions for a file to attach + * @returns {RichEmbed} This embed + */ + attachFile(file) { + if (this.file) throw new RangeError('You may not upload more than one file at once.'); + if (file instanceof Attachment) file = file.file; + this.file = file; + return this; + } + + /** + * Sets the files to upload alongside the embed. A file can be accessed via `attachment://fileName.extension` when + * setting an embed image or author/footer icons. Multiple files can be attached. + * @param {Array<FileOptions|string|Attachment>} files Files to attach + * @returns {RichEmbed} + */ + attachFiles(files) { + files = files.map(file => file instanceof Attachment ? file.file : file); + this.files = this.files.concat(files); + return this; + } + + /** + * The accumulated length for the embed title, description, fields, author and footer text + * @type {number} + * @readonly + */ + get length() { + return ( + (this.title ? this.title.length : 0) + + (this.description ? this.description.length : 0) + + (this.fields.length >= 1 ? this.fields.reduce((prev, curr) => + prev + curr.name.length + curr.value.length, 0) : 0) + + (this.footer ? this.footer.text.length : 0) + + (this.author ? this.author.name.length : 0)); + } + + /** + * Transforms the embed to a plain object. + * @returns {Object} The raw data of this embed + */ + toJSON() { + return { + title: this.title, + type: 'rich', + description: this.description, + url: this.url, + timestamp: this.timestamp ? new Date(this.timestamp) : null, + color: this.color, + fields: this.fields ? + this.fields.map(field => ({ name: field.name, value: field.value, inline: field.inline })) : + null, + thumbnail: this.thumbnail ? { + url: this.thumbnail.url, + } : null, + image: this.image ? { + url: this.image.url, + } : null, + author: this.author ? { + name: this.author.name, + url: this.author.url, + icon_url: this.author instanceof MessageEmbed.Author ? this.author.iconURL : this.author.icon_url, + } : null, + footer: this.footer ? { + text: this.footer.text, + icon_url: this.footer instanceof MessageEmbed.Footer ? this.footer.iconURL : this.footer.icon_url, + } : null, + }; + } + + /** + * Normalizes field input and resolves strings. + * @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 {EmbedField} + */ + static normalizeField(name, value, inline = false) { + name = util.resolveString(name); + if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); + if (!/\S/.test(name)) throw new RangeError('RichEmbed field names may not be empty.'); + value = util.resolveString(value); + if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); + if (!/\S/.test(value)) throw new RangeError('RichEmbed field values may not be empty.'); + return { name, value, inline }; + } +} + +module.exports = RichEmbed; 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..056cf98 --- /dev/null +++ b/node_modules/discord.js/src/structures/Role.js @@ -0,0 +1,376 @@ +const Snowflake = require('../util/Snowflake'); +const Permissions = require('../util/Permissions'); +const util = require('util'); + +/** + * 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; + + /** + * Whether the role has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of the role (unique to the guild it is part of) + * @type {Snowflake} + */ + 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 from the API + * @type {number} + */ + this.position = data.position; + + /** + * The permissions bitfield of the role + * @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 Snowflake.deconstruct(this.id).timestamp; + } + + /** + * 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<Snowflake, 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.permissions.has(Permissions.FLAGS.MANAGE_ROLES_OR_PERMISSIONS)) return false; + return clientMember.highestRole.comparePositionTo(this) > 0; + } + + /** + * The position of the role in the role manager + * @type {number} + * @readonly + */ + get calculatedPosition() { + const sorted = this.guild._sortedRoles; + return sorted.array().indexOf(sorted.get(this.id)); + } + + /** + * Get an object mapping permission names to whether or not the role enables that permission. + * @returns {Object<string, boolean>} + * @example + * // Print the serialized role permissions + * console.log(role.serialize()); + */ + serialize() { + return new Permissions(this.permissions).serialize(); + } + + /** + * Checks if the role has a permission. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission + * **(deprecated)** + * @param {boolean} [checkAdmin] Whether to allow the administrator permission to override + * (takes priority over `explicit`) + * @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, checkAdmin) { + return new Permissions(this.permissions).has( + permission, typeof checkAdmin !== 'undefined' ? checkAdmin : !explicit + ); + } + + /** + * 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} + * @deprecated + */ + hasPermissions(permissions, explicit = false) { + return new Permissions(this.permissions).has(permissions, !explicit); + } + + /** + * Compares this role's position to another role's. + * @param {Role} role Role to compare to this one + * @returns {number} Negative number if this role's position is lower (other role's is higher), + * positive number if 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 {ColorResolvable} [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 {PermissionResolvable|number} [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 + * @param {string} [reason] The reason for editing this role + * @returns {Promise<Role>} + * @example + * // Edit name of a role + * role.edit({ name: 'New Name' }) + * .then(updated => console.log(`Edited role name from ${role.name} to ${updated.name}`)) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateGuildRole(this, data, reason); + } + + /** + * Set a new name for the role. + * @param {string} name The new name of the role + * @param {string} [reason] Reason for changing the role's name + * @returns {Promise<Role>} + * @example + * // Set the name of the role + * role.setName('New Name') + * .then(updated => console.log(`Edited role name from ${role.name} to ${updated.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Set a new color for the role. + * @param {ColorResolvable} color The color of the role + * @param {string} [reason] Reason for changing the role's color + * @returns {Promise<Role>} + * @example + * // Set the color of a role + * role.setColor('#FF0000') + * .then(updated => console.log(`Set color of role to ${role.color}`)) + * .catch(console.error); + */ + setColor(color, reason) { + return this.edit({ color }, reason); + } + + /** + * Set whether or not the role should be hoisted. + * @param {boolean} hoist Whether or not to hoist the role + * @param {string} [reason] Reason for setting whether or not the role should be hoisted + * @returns {Promise<Role>} + * @example + * // Set the hoist of the role + * role.setHoist(true) + * .then(updated => console.log(`Role hoisted: ${updated.hoist}`)) + * .catch(console.error); + */ + setHoist(hoist, reason) { + return this.edit({ hoist }, reason); + } + + /** + * Set the position of the role. + * @param {number} position The position of the role + * @param {boolean} [relative=false] Move the position relative to its current value + * @returns {Promise<Role>} + * @example + * // Set the position of the role + * role.setPosition(1) + * .then(updated => console.log(`Role position: ${updated.position}`)) + * .catch(console.error); + */ + setPosition(position, relative) { + return this.guild.setRolePosition(this, position, relative).then(() => this); + } + + /** + * Set the permissions of the role. + * @param {PermissionResolvable} permissions The permissions of the role + * @param {string} [reason] Reason for changing the role's permissions + * @returns {Promise<Role>} + * @example + * // Set the permissions of the role + * role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS']) + * .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`)) + * .catch(console.error); + * @example + * // Remove all permissions from a role + * role.setPermissions(0) + * .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`)) + * .catch(console.error); + */ + setPermissions(permissions, reason) { + return this.edit({ permissions }, reason); + } + + /** + * Set whether this role is mentionable. + * @param {boolean} mentionable Whether this role should be mentionable + * @param {string} [reason] Reason for setting whether or not this role should be mentionable + * @returns {Promise<Role>} + * @example + * // Make the role mentionable + * role.setMentionable(true, 'Role needs to be pinged') + * .then(updated => console.log(`Role mentionable: ${updated.mentionable}`)) + * .catch(console.error); + */ + setMentionable(mentionable, reason) { + return this.edit({ mentionable }, reason); + } + + /** + * Deletes the role. + * @param {string} [reason] Reason for deleting the role + * @returns {Promise<Role>} + * @example + * // Delete a role + * role.delete('The role needed to go') + * .then(deleted => console.log(`Deleted role ${deleted.name}`)) + * .catch(console.error); + */ + delete(reason) { + return this.client.rest.methods.deleteGuildRole(this, reason); + } + + /** + * 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; + } +} + +Role.prototype.hasPermissions = util + .deprecate(Role.prototype.hasPermissions, + 'Role#hasPermissions is deprecated - use Role#hasPermission instead, it now takes an array'); + +module.exports = Role; diff --git a/node_modules/discord.js/src/structures/StoreChannel.js b/node_modules/discord.js/src/structures/StoreChannel.js new file mode 100644 index 0000000..8985c0f --- /dev/null +++ b/node_modules/discord.js/src/structures/StoreChannel.js @@ -0,0 +1,25 @@ +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild store channel on Discord. + * @extends {GuildChannel} + */ +class StoreChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'store'; + } + + setup(data) { + super.setup(data); + + /** + * If the guild considers this channel NSFW + * @type {boolean} + * @readonly + */ + this.nsfw = data.nsfw; + } +} + +module.exports = StoreChannel; diff --git a/node_modules/discord.js/src/structures/Team.js b/node_modules/discord.js/src/structures/Team.js new file mode 100644 index 0000000..e0dc0e0 --- /dev/null +++ b/node_modules/discord.js/src/structures/Team.js @@ -0,0 +1,109 @@ +const Snowflake = require('../util/Snowflake'); +const Collection = require('../util/Collection'); +const TeamMember = require('./TeamMember'); +const Constants = require('../util/Constants'); + +/** + * Represents a Client OAuth2 Application Team. + */ +class Team { + constructor(client, data) { + /** + * The client that instantiated the team + * @name Team#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this._patch(data); + } + + _patch(data) { + /** + * The ID of the Team + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The name of the Team + * @type {string} + */ + this.name = data.name; + + /** + * The Team's icon hash + * @type {?string} + */ + this.icon = data.icon || null; + + /** + * The Team's owner id + * @type {?string} + */ + this.ownerID = data.owner_user_id || null; + + /** + * The Team's members + * @type {Collection<Snowflake, TeamMember>} + */ + this.members = new Collection(); + + for (const memberData of data.members) { + const member = new TeamMember(this.client, this, memberData); + this.members.set(member.id, member); + } + } + + /** + * The owner of the team + * @type {?TeamMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID) || null; + } + + /** + * The timestamp the team was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the team was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the teams's icon. + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.CDN(this.client.options.http.cdn).TeamIcon(this.id, this.icon); + } + + /** + * When concatenated with a string, this automatically returns the Team's name instead of the + * Team object. + * @returns {string} + * @example + * // Logs: Team name: My Team + * console.log(`Team name: ${team}`); + */ + toString() { + return this.name; + } +} + +module.exports = Team; diff --git a/node_modules/discord.js/src/structures/TeamMember.js b/node_modules/discord.js/src/structures/TeamMember.js new file mode 100644 index 0000000..5dbc2ce --- /dev/null +++ b/node_modules/discord.js/src/structures/TeamMember.js @@ -0,0 +1,67 @@ +const { MembershipStates } = require('../util/Constants'); + +/** + * Represents a Client OAuth2 Application Team Member. + */ +class TeamMember { + constructor(client, team, data) { + /** + * The client that instantiated the Team Member + * @name TeamMember#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The Team this member is part of + * @type {Team} + */ + this.team = team; + + this._patch(data); + } + + _patch(data) { + /** + * The permissions this Team Member has with regard to the team + * @type {string[]} + */ + this.permissions = data.permissions; + + /** + * The membership state this Team Member has with regard to the team + * @type {MembershipStates} + */ + this.membershipState = MembershipStates[data.membership_state]; + + /** + * The user for this Team Member + * @type {User} + */ + this.user = this.client.dataManager.newUser(data.user); + } + + /** + * The ID of the Team Member + * @type {Snowflake} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * When concatenated with a string, this automatically returns the team members's mention instead of the + * TeamMember object. + * @returns {string} + * @example + * // Logs: Team Member's mention: <@123456789> + * console.log(`Team Member's mention: ${teamMember}`); + */ + toString() { + return this.user.toString(); + } +} + +module.exports = TeamMember; 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..d16dac8 --- /dev/null +++ b/node_modules/discord.js/src/structures/TextChannel.js @@ -0,0 +1,154 @@ +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interfaces/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'; + /** + * A collection containing the messages sent to this channel + * @type {Collection<Snowflake, Message>} + */ + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The topic of the text channel + * @type {?string} + */ + this.topic = data.topic; + + /** + * If the Discord considers this channel NSFW + * @type {boolean} + * @readonly + */ + this.nsfw = Boolean(data.nsfw); + + /** + * The ID of the last message sent in this channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; + + /** + * The ratelimit per user for this channel in seconds + * @type {number} + */ + this.rateLimitPerUser = data.rate_limit_per_user || 0; + } + + /** + * A collection of members that can see this channel, mapped by their ID + * @type {Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + const members = new Collection(); + for (const member of this.guild.members.values()) { + if (this.permissionsFor(member).has('READ_MESSAGES')) { + members.set(member.id, member); + } + } + return members; + } + + /** + * Fetch all webhooks for the channel. + * @returns {Promise<Collection<Snowflake, Webhook>>} + * @example + * // Fetch webhooks + * channel.fetchWebhooks() + * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) + * .catch(console.error); + */ + fetchWebhooks() { + return this.client.rest.methods.getChannelWebhooks(this); + } + + /** + * Sets whether this channel is flagged as NSFW. + * @param {boolean} nsfw Whether the channel should be considered NSFW + * @param {string} [reason] Reason for changing the channel's NSFW flag + * @returns {Promise<TextChannel>} + */ + setNSFW(nsfw, reason) { + return this.edit({ nsfw }, reason); + } + + /** + * Create a webhook for the channel. + * @param {string} name The name of the webhook + * @param {BufferResolvable|Base64Resolvable} [avatar] The avatar for the webhook + * @param {string} [reason] Reason for creating this webhook + * @returns {Promise<Webhook>} webhook The created webhook + * @example + * channel.createWebhook('Snek', 'https://i.imgur.com/mI8XcpG.jpg') + * .then(webhook => console.log(`Created webhook ${webhook}`)) + * .catch(console.error) + */ + createWebhook(name, avatar, reason) { + if (typeof avatar === 'string' && avatar.startsWith('data:')) { + return this.client.rest.methods.createWebhook(this, name, avatar, reason); + } else { + return this.client.resolver.resolveImage(avatar).then(data => + this.client.rest.methods.createWebhook(this, name, data, reason) + ); + } + } + + /** + * Sets the rate limit per user for this channel. + * @param {number} rateLimitPerUser The new ratelimit in seconds + * @param {string} [reason] Reason for changing the channel's ratelimits + * @returns {Promise<TextChannel>} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser }, reason); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() { } + sendMessage() { } + sendEmbed() { } + sendFile() { } + sendFiles() { } + sendCode() { } + fetchMessage() { } + fetchMessages() { } + fetchPinnedMessages() { } + search() { } + startTyping() { } + stopTyping() { } + get typing() { } + get typingCount() { } + createCollector() { } + createMessageCollector() { } + awaitMessages() { } + bulkDelete() { } + acknowledge() { } + _cacheMessage() { } +} + +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..86c2bc0 --- /dev/null +++ b/node_modules/discord.js/src/structures/User.js @@ -0,0 +1,336 @@ +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const Constants = require('../util/Constants'); +const Presence = require('./Presence').Presence; +const Snowflake = require('../util/Snowflake'); +const util = require('util'); + +/** + * Represents a user on Discord. + * @implements {TextBasedChannel} + */ +class User { + constructor(client, data) { + /** + * The client that created the instance of 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 {Snowflake} + */ + 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); + + /** + * Whether this is an Official Discord System user (part of the urgent message system) + * @type {?boolean} + * @name User#system + */ + if (typeof data.system !== 'undefined') this.system = Boolean(data.system); + + /** + * The ID of the last message sent by the user, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The Message object of the last message sent by the user, if one was sent + * @type {?Message} + */ + this.lastMessage = 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 Snowflake.deconstruct(this.id).timestamp; + } + + /** + * 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(undefined, this.client); + } + + /** + * A link to the user's avatar + * @type {?string} + * @readonly + */ + get avatarURL() { + if (!this.avatar) return null; + return Constants.Endpoints.User(this).Avatar(this.client.options.http.cdn, this.avatar); + } + + /** + * A link to the user's default avatar + * @type {string} + * @readonly + */ + get defaultAvatarURL() { + const avatars = Object.keys(Constants.DefaultAvatars); + const avatar = avatars[this.discriminator % avatars.length]; + return Constants.Endpoints.CDN(this.client.options.http.host).Asset(`${Constants.DefaultAvatars[avatar]}.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 Discord "tag" (e.g. `hydrabolt#0001`) for this user + * @type {string} + * @readonly + */ + get tag() { + return `${this.username}#${this.discriminator}`; + } + + /** + * The note that is set for the user + * <warn>This is only available when using a user account.</warn> + * @type {?string} + * @readonly + * @deprecated + */ + 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} + * @readonly + */ + get dmChannel() { + return this.client.channels.find(c => c.type === 'dm' && c.recipient.id === this.id); + } + + /** + * Creates a DM channel between the client and the user. + * @returns {Promise<DMChannel>} + */ + createDM() { + return this.client.rest.methods.createDM(this); + } + + /** + * 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>} + * @deprecated + */ + 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>} + * @deprecated + */ + 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>} + * @deprecated + */ + 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>} + * @deprecated + */ + 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>} + * @deprecated + */ + 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>} + * @deprecated + */ + 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 + /* eslint-disable no-empty-function */ + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendCode() {} +} + +TextBasedChannel.applyToClass(User); + +User.prototype.block = + util.deprecate(User.prototype.block, 'User#block: userbot methods will be removed'); + +User.prototype.unblock = + util.deprecate(User.prototype.unblock, 'User#unblock: userbot methods will be removed'); + +User.prototype.addFriend = + util.deprecate(User.prototype.addFriend, 'User#addFriend: userbot methods will be removed'); + +User.prototype.removeFriend = + util.deprecate(User.prototype.removeFriend, 'User#removeFriend: userbot methods will be removed'); + +User.prototype.setNote = + util.deprecate(User.prototype.setNote, 'User#setNote, userbot methods will be removed'); + +User.prototype.fetchProfile = + util.deprecate(User.prototype.fetchProfile, 'User#fetchProfile: userbot methods will be removed'); + +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..ea6c65f --- /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; + + /** + * Partial server integrations (not yet implemented) + * @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..a27d4d0 --- /dev/null +++ b/node_modules/discord.js/src/structures/UserProfile.js @@ -0,0 +1,62 @@ +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 UserProfile + * @name UserProfile#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: user.client }); + + /** + * The guilds that the client user and the user share + * @type {Collection<Snowflake, Guild>} + */ + this.mutualGuilds = new Collection(); + + /** + * The user's connections + * @type {Collection<Snowflake, UserConnection>} + */ + this.connections = new Collection(); + + this.setup(data); + } + + setup(data) { + /** + * If the user has Discord Premium + * @type {boolean} + */ + this.premium = data.premium; + + /** + * The date since which the user has had Discord Premium + * @type {?Date} + */ + this.premiumSince = data.premium_since ? new Date(data.premium_since) : null; + + 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..a89dafa --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceChannel.js @@ -0,0 +1,146 @@ +const GuildChannel = require('./GuildChannel'); +const Collection = require('../util/Collection'); +const Permissions = require('../util/Permissions'); + +/** + * 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<Snowflake, 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 * 0.001; + + /** + * 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 voice channel is full + * @type {boolean} + * @readonly + */ + get full() { + return this.userLimit > 0 && this.members.size >= this.userLimit; + } + + /** + * Whether the channel is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + return super.deletable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT); + } + + /** + * Checks if the client has permission join the voice channel + * @type {boolean} + * @readonly + */ + get joinable() { + if (this.client.browser) return false; + if (!this.permissionsFor(this.client.user).has('CONNECT')) return false; + if (this.full && !this.permissionsFor(this.client.user).has('MOVE_MEMBERS')) return false; + return true; + } + + /** + * Checks if the client has permission to send audio to the voice channel + * @type {boolean} + * @readonly + */ + get speakable() { + return this.permissionsFor(this.client.user).has('SPEAK'); + } + + /** + * Sets the bitrate of the channel (in kbps). + * @param {number} bitrate The new bitrate + * @param {string} [reason] Reason for changing the channel's bitrate + * @returns {Promise<VoiceChannel>} + * @example + * // Set the bitrate of a voice channel + * voiceChannel.setBitrate(48) + * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`)) + * .catch(console.error); + */ + setBitrate(bitrate, reason) { + bitrate *= 1000; + return this.edit({ bitrate }, reason); + } + + /** + * Sets the user limit of the channel. + * @param {number} userLimit The new user limit + * @param {string} [reason] Reason for changing the 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, reason) { + return this.edit({ userLimit }, reason); + } + + /** + * 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/VoiceRegion.js b/node_modules/discord.js/src/structures/VoiceRegion.js new file mode 100644 index 0000000..dc6b461 --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceRegion.js @@ -0,0 +1,50 @@ +/** + * Represents a Discord voice region for guilds. + */ +class VoiceRegion { + constructor(data) { + /** + * The ID of the region + * @type {string} + */ + this.id = data.id; + + /** + * Name of the region + * @type {string} + */ + this.name = data.name; + + /** + * Whether the region is VIP-only + * @type {boolean} + */ + this.vip = data.vip; + + /** + * Whether the region is deprecated + * @type {boolean} + */ + this.deprecated = data.deprecated; + + /** + * Whether the region is optimal + * @type {boolean} + */ + this.optimal = data.optimal; + + /** + * Whether the region is custom + * @type {boolean} + */ + this.custom = data.custom; + + /** + * A sample hostname for what a connection might look like + * @type {string} + */ + this.sampleHostname = data.sample_hostname; + } +} + +module.exports = VoiceRegion; 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..5d82b0c --- /dev/null +++ b/node_modules/discord.js/src/structures/Webhook.js @@ -0,0 +1,377 @@ +const EventEmitter = require('events'); +const path = require('path'); +const Util = require('../util/Util'); +const Attachment = require('./Attachment'); +const RichEmbed = require('./RichEmbed'); +const Constants = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents a webhook. + */ +class Webhook extends EventEmitter { + constructor(client, dataOrID, token) { + super(); + 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 + * @name Webhook#token + * @type {?string} + */ + Object.defineProperty(this, 'token', { value: data.token || null, writable: true, configurable: true }); + + /** + * The avatar for the webhook + * @type {?string} + */ + this.avatar = data.avatar; + + /** + * The ID of the webhook + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The type of the webhook + * @type {WebhookTypes} + */ + this.type = Constants.WebhookTypes[data.type]; + + /** + * The guild the webhook belongs to + * @type {Snowflake} + */ + this.guildID = data.guild_id; + + /** + * The channel the webhook belongs to + * @type {Snowflake} + */ + this.channelID = data.channel_id; + + if (data.user) { + /** + * The owner of the webhook + * @type {?User|Object} + */ + this.owner = this.client.users ? this.client.users.get(data.user.id) : data.user; + } else { + this.owner = null; + } + } + + /** + * The timestamp the webhook was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the webhook was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the webhook user's avatar + * @type {?stirng} + * @readonly + */ + get avatarURL() { + if (!this.avatar) return null; + return Constants.Endpoints.CDN(this.client.options.http.cdn).Avatar(this.id, this.avatar); + } + + /** + * The url of this webhook + * @type {string} + * @readonly + */ + get url() { + const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`; + return API + Constants.Endpoints.Webhook(this.id, this.token); + } + + /** + * Options that can be passed into send, sendMessage, sendFile, sendEmbed, and sendCode. + * @typedef {Object} WebhookMessageOptions + * @property {string} [username=this.name] Username override for the message + * @property {string} [avatarURL] Avatar URL override for the message + * @property {boolean} [tts=false] Whether or not the message should be spoken aloud + * @property {string} [nonce=''] The nonce for the message + * @property {Array<RichEmbed|Object>} [embeds] An array of embeds 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|BufferResolvable|Attachment} [file] A file to send with the message **(deprecated)** + * @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files 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. + */ + + /** + * Send a message with this webhook. + * @param {StringResolvable} content The content to send + * @param {WebhookMessageOptions|Attachment|RichEmbed} [options] The options to provide, + * can also be just a RichEmbed or Attachment + * @returns {Promise<Message|Message[]|Object|Object[]>} + * @example + * // Send a basic message + * webhook.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * webhook.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * webhook.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * webhook.send('This is an embed', { + * embeds: [{ + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * }], + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + send(content, options) { // eslint-disable-line complexity + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + + if (options instanceof Attachment) options = { files: [options] }; + if (options instanceof RichEmbed) options = { embeds: [options] }; + + if (content) { + content = this.client.resolver.resolveString(content); + let { split, code, disableEveryone } = options; + if (split && typeof split !== 'object') split = {}; + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(content, true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + if (split) { + split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; + split.append = '\n```'; + } + } + if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + if (split) content = Util.splitMessage(content, split); + } + + if (options.file) { + if (options.files) options.files.push(options.file); + else options.files = [options.file]; + } + + if (options.embeds) { + const files = []; + for (const embed of options.embeds) { + if (embed.file) files.push(embed.file); + } + if (options.files) options.files.push(...files); + else options.files = files; + } + + if (options.embeds) options.embeds = options.embeds.map(e => new RichEmbed(e).toJSON()); + + if (options.files) { + for (let i = 0; i < options.files.length; i++) { + let file = options.files[i]; + if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file }; + if (!file.name) { + if (typeof file.attachment === 'string') { + file.name = path.basename(file.attachment); + } else if (file.attachment && file.attachment.path) { + file.name = path.basename(file.attachment.path); + } else if (file instanceof Attachment) { + file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' }; + } else { + file.name = 'file.jpg'; + } + } else if (file instanceof Attachment) { + file = file.file; + } + options.files[i] = file; + } + + return Promise.all(options.files.map(file => + this.client.resolver.resolveFile(file.attachment).then(resource => { + file.file = resource; + return file; + }) + )).then(files => this.client.rest.methods.sendWebhookMessage(this, content, options, files)); + } + + return this.client.rest.methods.sendWebhookMessage(this, content, options); + } + + /** + * Send a message with this webhook + * @param {StringResolvable} content The content to send + * @param {WebhookMessageOptions} [options={}] The options to provide + * @returns {Promise<Message|Message[]>} + * @deprecated + * @example + * // Send a message + * webhook.sendMessage('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + sendMessage(content, options = {}) { + return this.send(content, options); + } + + /** + * Send a file with this webhook. + * @param {BufferResolvable} attachment The file to send + * @param {string} [name='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>} + * @deprecated + */ + sendFile(attachment, name, content, options = {}) { + return this.send(content, Object.assign(options, { file: { attachment, name } })); + } + + /** + * 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[]>} + * @deprecated + */ + sendCode(lang, content, options = {}) { + return this.send(content, Object.assign(options, { code: lang })); + } + + /** + * 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); + } + + /** + * Options provided to edit a webhook. + * @property {string} [name] The new name for the webhook + * @property {BufferResolvable} [avatar] The new avatar for the webhook + * @property {ChannelResolvable} [channel] The new channel for the webhook + * @typedef {Object} WebhookEditOptions + */ + + /** + * Edit the webhook. + * @param {string|WebhookEditOptions} nameOrOptions The new name for the webhook **(deprecated, use options)** + * Alternatively options for the webhook, overriding the avatar parameter. + * @param {BufferResolvable|string} [avatarOrReason] The new avatar for the webhook **(deprecated, use options)** + * Alternatively a reason to edit, if using options as first parameter. + * @returns {Promise<Webhook>} + */ + edit(nameOrOptions = this.name, avatarOrReason) { + if (typeof nameOrOptions !== 'object') { + process.emitWarning('Webhook#edit: Use options object instead of separate parameters.'); + nameOrOptions = { + name: nameOrOptions, + avatar: avatarOrReason, + }; + // Parameter was an avatar here; Clear the now reason parameter + avatarOrReason = undefined; + } + + if (nameOrOptions.channel) { + nameOrOptions.channel_id = this.client.resolver.resolveChannelID(nameOrOptions.channel); + nameOrOptions.channel = undefined; + } + + if (nameOrOptions.avatar) { + return this.client.resolver.resolveImage(nameOrOptions.avatar).then(data => { + nameOrOptions.avatar = data; + return this.client.rest.methods.editWebhook(this, nameOrOptions, avatarOrReason); + }); + } + + return this.client.rest.methods.editWebhook(this, nameOrOptions, avatarOrReason); + } + + /** + * Delete the webhook. + * @param {string} [reason] Reason for deleting the webhook + * @returns {Promise} + */ + delete(reason) { + return this.client.rest.methods.deleteWebhook(this, reason); + } +} + +module.exports = Webhook; diff --git a/node_modules/discord.js/src/structures/interfaces/Collector.js b/node_modules/discord.js/src/structures/interfaces/Collector.js new file mode 100644 index 0000000..6767b42 --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/Collector.js @@ -0,0 +1,208 @@ +const Collection = require('../../util/Collection'); +const EventEmitter = require('events').EventEmitter; + +/** + * Filter to be applied to the collector. + * @typedef {Function} CollectorFilter + * @param {...*} args Any arguments received by the listener + * @param {Collection} collection The items collected by this collector + * @returns {boolean} + */ + +/** + * Options to be applied to the collector. + * @typedef {Object} CollectorOptions + * @property {number} [time] How long to run the collector for + * @property {number} [idle] How long to stop the collector after inactivity in milliseconds + */ + +/** + * Abstract class for defining a new Collector. + * @abstract + */ +class Collector extends EventEmitter { + constructor(client, filter, options = {}) { + super(); + + /** + * The client + * @name Collector#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The filter applied to this collector + * @type {CollectorFilter} + */ + this.filter = filter; + + /** + * The options of this collector + * @type {CollectorOptions} + */ + this.options = options; + + /** + * The items collected by this collector + * @type {Collection} + */ + this.collected = new Collection(); + + /** + * Whether this collector has finished collecting + * @type {boolean} + */ + this.ended = false; + + /** + * Timeout for cleanup + * @type {?Timeout} + * @private + */ + this._timeout = null; + + /** + * Timeout for cleanup due to inactivity + * @type {?Timeout} + * @private + */ + this._idletimeout = null; + + /** + * Call this to handle an event as a collectable element + * Accepts any event data as parameters + * @type {Function} + * @private + */ + this.listener = this._handle.bind(this); + if (options.time) this._timeout = this.client.setTimeout(() => this.stop('time'), options.time); + if (options.idle) this._idletimeout = this.client.setTimeout(() => this.stop('idle'), options.idle); + } + + /** + * @param {...*} args The arguments emitted by the listener + * @emits Collector#collect + * @private + */ + _handle(...args) { + const collect = this.handle(...args); + if (collect && this.filter(...args, this.collected)) { + this.collected.set(collect.key, collect.value); + + /** + * Emitted whenever an element is collected. + * @event Collector#collect + * @param {*} element The element that got collected + * @param {Collector} collector The collector + */ + this.emit('collect', collect.value, this); + + /** + * Emitted whenever an element is collected. + * @event Collector#fullCollect + * @param {...*} args The arguments emitted by the listener + * @private + */ + this.emit('fullCollect', ...args, this); + + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), this.options.idle); + } + } + + const post = this.postCheck(...args); + if (post) this.stop(post); + } + + /** + * Return a promise that resolves with the next collected element; + * rejects with collected elements if the collector finishes without receiving a next element + * @type {Promise} + * @readonly + */ + get next() { + return new Promise((resolve, reject) => { + if (this.ended) { + reject(this.collected); + return; + } + + const cleanup = () => { + this.removeListener('collect', onCollect); + this.removeListener('end', onEnd); + }; + + const onCollect = item => { + cleanup(); + resolve(item); + }; + + const onEnd = () => { + cleanup(); + reject(this.collected); // eslint-disable-line prefer-promise-reject-errors + }; + + this.on('collect', onCollect); + this.on('end', onEnd); + }); + } + + /** + * Stop this collector and emit the `end` event. + * @param {string} [reason='user'] The reason this collector is ending + * @emits Collector#end + */ + stop(reason = 'user') { + if (this.ended) return; + + if (this._timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = null; + } + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = null; + } + this.ended = true; + this.cleanup(); + + /** + * Emitted when the collector is finished collecting. + * @event Collector#end + * @param {Collection} collected The elements collected by the collector + * @param {string} reason The reason the collector ended + */ + this.emit('end', this.collected, reason); + } + + /* eslint-disable no-empty-function, valid-jsdoc */ + /** + * Handles incoming events from the `listener` function. Returns null if the event should not be collected, + * or returns an object describing the data that should be stored. + * @see Collector#listener + * @param {...*} args Any args the event listener emits + * @returns {?{key: string, value}} Data to insert into collection, if any + * @abstract + */ + handle() {} + + /** + * This method runs after collection to see if the collector should finish. + * @param {...*} args Any args the event listener emits + * @returns {?string} Reason to end the collector, if any + * @abstract + */ + postCheck() {} + + /** + * Called when the collector is ending. + * @abstract + */ + cleanup() {} + /* eslint-enable no-empty-function, valid-jsdoc */ +} + +module.exports = Collector; diff --git a/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js b/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js new file mode 100644 index 0000000..ceaa73b --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -0,0 +1,635 @@ +const path = require('path'); +const Message = require('../Message'); +const MessageCollector = require('../MessageCollector'); +const Collection = require('../../util/Collection'); +const Attachment = require('../../structures/Attachment'); +const RichEmbed = require('../../structures/RichEmbed'); +const Snowflake = require('../../util/Snowflake'); +const util = require('util'); + +/** + * Interface for classes that have text-channel-like features. + * @interface + */ +class TextBasedChannel { + constructor() { + /** + * A collection containing the messages sent to this channel + * @type {Collection<Snowflake, Message>} + */ + this.messages = new Collection(); + + /** + * The ID of the last message in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The Message object of the last message in the channel, if one was sent + * @type {?Message} + */ + this.lastMessage = null; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = null; + } + + /** + * Options provided when sending or editing a message. + * @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 {RichEmbed|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|BufferResolvable|Attachment} [file] A file to send with the message **(deprecated)** + * @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files 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 + * @property {UserResolvable} [reply] User to reply to (prefixes the message with a mention, except in DMs) + */ + + /** + * @typedef {Object} FileOptions + * @property {BufferResolvable} attachment File to attach + * @property {string} [name='file.jpg'] Filename of the attachment + */ + + /** + * 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|Attachment|RichEmbed} [options] Options for the message, + * can also be just a RichEmbed or Attachment + * @returns {Promise<Message|Message[]>} + * @example + * // Send a basic message + * channel.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * channel.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * channel.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * channel.send('This is an embed', { + * embed: { + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * }, + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + // eslint-disable-next-line complexity + send(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + + const { reply } = options; + if (options instanceof Attachment) options = { files: [options.file] }; + if (options instanceof RichEmbed) { + if (options.reply) options.reply = undefined; + options = { embed: options }; + } + options.reply = reply; + + if (options.embed) { + if (options.embed.file) { + if (options.files) options.files.push(options.embed.file); + else options.files = [options.embed.file]; + } + if (options.embed.files) { + if (options.files) options.files = options.files.concat(options.embed.files); + else options.files = options.embed.files; + } + } + + if (options.file) { + if (options.files) options.files.push(options.file); + else options.files = [options.file]; + } + + if (options.embed) options.embed = new RichEmbed(options.embed).toJSON(); + + if (options.files) { + for (let i = 0; i < options.files.length; i++) { + let file = options.files[i]; + if (!file || typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file }; + if (!file.name) { + if (typeof file.attachment === 'string') { + file.name = path.basename(file.attachment); + } else if (file.attachment && file.attachment.path) { + file.name = path.basename(file.attachment.path); + } else if (file instanceof Attachment) { + file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' }; + } else { + file.name = 'file.jpg'; + } + } else if (file instanceof Attachment) { + file = file.file; + } + options.files[i] = file; + } + + return Promise.all(options.files.map(file => + this.client.resolver.resolveFile(file.attachment).then(resource => { + file.file = resource; + return file; + }) + )).then(files => this.client.rest.methods.sendMessage(this, content, options, files)); + } + + return this.client.rest.methods.sendMessage(this, content, options); + } + + /** + * Gets a single message from this channel, regardless of it being cached or not. + * @param {Snowflake} 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) { + if (!this.client.user.bot) { + return this.fetchMessages({ limit: 1, around: messageID }).then(messages => { + const msg = messages.get(messageID); + if (!msg) throw new Error('Message not found.'); + return msg; + }); + } + 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 {Snowflake} [before] ID of a message to get the messages that were posted before it + * @property {Snowflake} [after] ID of a message to get the messages that were posted after it + * @property {Snowflake} [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. + * <info>The returned Collection does not contain reaction users of the messages if they were not cached. + * Those need to be fetched separately in such a case.</info> + * @param {ChannelLogsQueryOptions} [options={}] Query parameters to pass in + * @returns {Promise<Collection<Snowflake, Message>>} + * @example + * // Get messages + * channel.fetchMessages({ limit: 10 }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Get messages and filter by user ID + * channel.fetchMessages() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').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. + * <info>The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately.</info> + * @returns {Promise<Collection<Snowflake, Message>>} + * @example + * // Get pinned messages + * channel.fetchPinnedMessages() + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + 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; + }); + } + + /** + * @typedef {Object} MessageSearchOptions + * @property {string} [content] Message content + * @property {Snowflake} [maxID] Maximum ID for the filter + * @property {Snowflake} [minID] Minimum ID for the filter + * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, + * or add `-` to negate (e.g. `-file`) + * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) + * @property {UserResolvable} [author] Author to limit search + * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) + * @property {string} [sortBy='recent'] `recent` or `relevant` + * @property {string} [sortOrder='desc'] `asc` or `desc` + * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) + * @property {number} [limit=25] Maximum number of results to get (1 to 25) + * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) + * @property {UserResolvable} [mentions] Mentioned user filter + * @property {boolean} [mentionsEveryone] If everyone is mentioned + * @property {string} [linkHostname] Filter links by hostname + * @property {string} [embedProvider] The name of an embed provider + * @property {string} [embedType] one of `image`, `video`, `url`, `rich` + * @property {string} [attachmentFilename] The name of an attachment + * @property {string} [attachmentExtension] The extension of an attachment + * @property {Date} [before] Date to find messages before + * @property {Date} [after] Date to find messages before + * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) + * @property {boolean} [nsfw=false] Include results from NSFW channels + */ + + /** + * @typedef {Object} MessageSearchResult + * @property {number} totalResults Total result count + * @property {Message[][]} messages Array of message results + * The message which has triggered the result will have the `hit` property set to `true` + */ + + /** + * Performs a search within the channel. + * <warn>This is only available when using a user account.</warn> + * @param {MessageSearchOptions} [options={}] Options to pass to the search + * @returns {Promise<MessageSearchResult>} + * @deprecated + * @example + * channel.search({ + * content: 'discord.js', + * before: '2016-11-17' + * }).then(res => { + * const hit = res.messages[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); + * }).catch(console.error); + */ + search(options = {}) { + return this.client.rest.methods.search(this, options); + } + + /** + * 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)) { + const entry = this.client.user._typing.get(this.id); + entry.count = count || entry.count + 1; + return; + } + + const entry = { + count: count || 1, + interval: this.client.setInterval(() => { + this.client.rest.methods.sendTyping(this.id).catch(() => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + }); + }, 9000), + }; + this.client.rest.methods.sendTyping(this.id).catch(() => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + }); + this.client.user._typing.set(this.id, entry); + } + + /** + * 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 + * // Reduce the typing count by one and stop typing if it reached 0 + * 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; + } + + /** + * The Message object of the last message in the channel, if one was sent + * @type {?Message} + * @readonly + */ + get lastMessage() { + return this.messages.get(this.lastMessageID) || null; + } + + /** + * The date when the last pinned message was pinned, if there was one + * @type {?Date} + * @readonly + */ + get lastPinAt() { + return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null; + } + + /** + * Creates a Message Collector + * @param {CollectorFilter} filter The filter to create the collector with + * @param {MessageCollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @deprecated + */ + createCollector(filter, options) { + return this.createMessageCollector(filter, options); + } + + /** + * Creates a Message Collector. + * @param {CollectorFilter} filter The filter to create the collector with + * @param {MessageCollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @example + * // Create a message collector + * const filter = m => m.content.includes('discord'); + * const collector = channel.createMessageCollector(filter, { time: 15000 }); + * collector.on('collect', m => console.log(`Collected ${m.content}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageCollector(filter, options = {}) { + return new MessageCollector(this, filter, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {MessageCollectorOptions} 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 {CollectorFilter} filter The filter function to use + * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise<Collection<Snowflake, 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.once('end', (collection, reason) => { + if (options.errors && options.errors.includes(reason)) { + reject(collection); + } else { + resolve(collection); + } + }); + }); + } + + /** + * Bulk delete given messages that are newer than two weeks. + * <warn>This is only available when using a bot account.</warn> + * @param {Collection<Snowflake, Message>|Message[]|Snowflake[]|number} messages + * Messages or number of messages to delete + * @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically + * @returns {Promise<Collection<Snowflake, Message>>} Deleted messages + * @example + * // Bulk delete messages + * channel.bulkDelete(5) + * .then(messages => console.log(`Bulk deleted ${messages.size} messages`)) + * .catch(console.error); + */ + bulkDelete(messages, filterOld = false) { + if (messages instanceof Array || messages instanceof Collection) { + let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id || m); + if (filterOld) { + messageIDs = messageIDs.filter(id => Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000); + } + if (messageIDs.length === 0) return Promise.resolve(new Collection()); + if (messageIDs.length === 1) { + return this.fetchMessage(messageIDs[0]).then(m => m.delete()).then(m => new Collection([[m.id, m]])); + } + return this.client.rest.methods.bulkDeleteMessages(this, messageIDs); + } + if (!isNaN(messages)) return this.fetchMessages({ limit: messages }).then(msgs => this.bulkDelete(msgs, filterOld)); + throw new TypeError('The messages must be an Array, Collection, or number.'); + } + + /** + * Marks all messages in this channel as read. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<TextChannel|GroupDMChannel|DMChannel>} + * @deprecated + */ + acknowledge() { + if (!this.lastMessageID) return Promise.resolve(this); + return this.client.rest.methods.ackTextChannel(this); + } + + _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; + } +} + +/** @lends TextBasedChannel.prototype */ +const Deprecated = { + /** + * Send a message to this channel. + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options={}] Options for the message + * @returns {Promise<Message|Message[]>} + * @deprecated + * @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>} + * @deprecated + */ + sendEmbed(embed, content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.send(content, Object.assign(options, { embed })); + }, + + /** + * Send files to this channel. + * @param {FileOptions[]|string[]} files Files to send with the message + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options] Options for the message + * @returns {Promise<Message>} + * @deprecated + */ + sendFiles(files, content, options = {}) { + return this.send(content, Object.assign(options, { files })); + }, + + /** + * 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>} + * @deprecated + */ + sendFile(attachment, name, content, options = {}) { + return this.send({ files: [{ attachment, name }], content, options }); + }, + + /** + * 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[]>} + * @deprecated + */ + sendCode(lang, content, options = {}) { + return this.send(content, Object.assign(options, { code: lang })); + }, +}; + +for (const key of Object.keys(Deprecated)) { + TextBasedChannel.prototype[key] = util.deprecate(Deprecated[key], `TextChannel#${key}: use TextChannel#send instead`); +} + +exports.applyToClass = (structure, full = false, ignore = []) => { + const props = ['send', 'sendMessage', 'sendEmbed', 'sendFile', 'sendFiles', 'sendCode']; + if (full) { + props.push( + '_cacheMessage', + 'acknowledge', + 'fetchMessages', + 'fetchMessage', + 'search', + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'startTyping', + 'stopTyping', + 'typing', + 'typingCount', + 'fetchPinnedMessages', + 'createCollector', + 'createMessageCollector', + 'awaitMessages' + ); + } + for (const prop of props) { + if (ignore.includes(prop)) continue; + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop)); + } +}; + +TextBasedChannel.prototype.acknowledge = util.deprecate( + TextBasedChannel.prototype.acknowledge, 'TextBasedChannel#acknowledge: userbot methods will be removed' +); + +TextBasedChannel.prototype.search = + util.deprecate(TextBasedChannel.prototype.search, 'TextBasedChannel#search: userbot methods will be removed'); diff --git a/node_modules/discord.js/src/structures/shared/resolvePermissions.js b/node_modules/discord.js/src/structures/shared/resolvePermissions.js new file mode 100644 index 0000000..c266d46 --- /dev/null +++ b/node_modules/discord.js/src/structures/shared/resolvePermissions.js @@ -0,0 +1,26 @@ +const Permissions = require('../../util/Permissions'); +const Collection = require('../../util/Collection'); + +module.exports = function resolvePermissions(overwrites, guild) { + if (overwrites instanceof Collection || overwrites instanceof Array) { + overwrites = overwrites.map(overwrite => { + const role = this.client.resolver.resolveRole(guild, overwrite.id); + if (role) { + overwrite.id = role.id; + overwrite.type = 'role'; + } else { + overwrite.id = this.client.resolver.resolveUserID(overwrite.id); + overwrite.type = 'member'; + } + + return { + allow: Permissions.resolve(overwrite.allow || overwrite.allowed || 0), + deny: Permissions.resolve(overwrite.deny || overwrite.denied || 0), + type: overwrite.type, + id: overwrite.id, + }; + }); + } + + return overwrites; +}; diff --git a/node_modules/discord.js/src/util/BitField.js b/node_modules/discord.js/src/util/BitField.js new file mode 100644 index 0000000..e0f86b1 --- /dev/null +++ b/node_modules/discord.js/src/util/BitField.js @@ -0,0 +1,160 @@ +/** + * Data structure that makes it easy to interact with a bitfield. + */ +class BitField { + /** + * @param {BitFieldResolvable} [bits=0] Bits(s) to read from + */ + constructor(bits) { + /** + * Bitfield of the packed bits + * @type {number} + */ + this.bitfield = this.constructor.resolve(bits); + } + + /** + * Checks whether the bitfield has a bit, or any of multiple bits. + * @param {BitFieldResolvable} bit Bit(s) to check for + * @returns {boolean} + */ + any(bit) { + return (this.bitfield & this.constructor.resolve(bit)) !== 0; + } + + /** + * Checks if this bitfield equals another + * @param {BitFieldResolvable} bit Bit(s) to check for + * @returns {boolean} + */ + equals(bit) { + return this.bitfield === this.constructor.resolve(bit); + } + + /** + * Checks whether the bitfield has a bit, or multiple bits. + * @param {BitFieldResolvable} bit Bit(s) to check for + * @returns {boolean} + */ + has(bit) { + if (Array.isArray(bit)) return bit.every(p => this.has(p)); + bit = this.constructor.resolve(bit); + return (this.bitfield & bit) === bit; + } + + /** + * Gets all given bits that are missing from the bitfield. + * @param {BitFieldResolvable} bits Bits(s) to check for + * @param {...*} hasParams Additional parameters for the has method, if any + * @returns {string[]} + */ + missing(bits, ...hasParams) { + if (!Array.isArray(bits)) bits = new this.constructor(bits).toArray(false); + return bits.filter(p => !this.has(p, ...hasParams)); + } + + /** + * Freezes these bits, making them immutable. + * @returns {Readonly<BitField>} These bits + */ + freeze() { + return Object.freeze(this); + } + + /** + * Adds bits to these ones. + * @param {...BitFieldResolvable} [bits] Bits to add + * @returns {BitField} These bits or new BitField if the instance is frozen. + */ + add(...bits) { + let total = 0; + for (const bit of bits) { + total |= this.constructor.resolve(bit); + } + if (Object.isFrozen(this)) return new this.constructor(this.bitfield | total); + this.bitfield |= total; + return this; + } + + /** + * Removes bits from these. + * @param {...BitFieldResolvable} [bits] Bits to remove + * @returns {BitField} These bits or new BitField if the instance is frozen. + */ + remove(...bits) { + let total = 0; + for (const bit of bits) { + total |= this.constructor.resolve(bit); + } + if (Object.isFrozen(this)) return new this.constructor(this.bitfield & ~total); + this.bitfield &= ~total; + return this; + } + + /** + * Gets an object mapping field names to a {@link boolean} indicating whether the + * bit is available. + * @param {...*} hasParams Additional parameters for the has method, if any + * @returns {Object} + */ + serialize(...hasParams) { + const serialized = {}; + for (const flag of Object.keys(this.constructor.FLAGS)) { + serialized[flag] = this.has(this.constructor.FLAGS[flag], ...hasParams); + } + return serialized; + } + + /** + * Gets an {@link Array} of bitfield names based on the bits available. + * @param {...*} hasParams Additional parameters for the has method, if any + * @returns {string[]} + */ + toArray(...hasParams) { + return Object.keys(this.constructor.FLAGS).filter(bit => this.has(bit, ...hasParams)); + } + + toJSON() { + return this.bitfield; + } + + valueOf() { + return this.bitfield; + } + + *[Symbol.iterator]() { + yield* this.toArray(); + } + + /** + * Data that can be resolved to give a bitfield. This can be: + * * A string (see {@link BitField.FLAGS}) + * * A bit number + * * An instance of BitField + * * An Array of BitFieldResolvable + * @typedef {string|number|BitField|BitFieldResolvable[]} BitFieldResolvable + */ + + /** + * Resolves bitfields to their numeric form. + * @param {BitFieldResolvable} [bit=0] - bit(s) to resolve + * @returns {number} + */ + static resolve(bit = 0) { + if (typeof bit === 'number' && bit >= 0) return bit; + if (bit instanceof BitField) return bit.bitfield; + if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); + if (typeof bit === 'string' && typeof this.FLAGS[bit] !== 'undefined') return this.FLAGS[bit]; + throw new RangeError('Invalid bitfield flag or number.'); + } +} + +/** + * Numeric bitfield flags. + * <info>Defined in extension classes</info> + * @type {Object} + * @abstract + */ +BitField.FLAGS = {}; + +module.exports = BitField; 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..612b67c --- /dev/null +++ b/node_modules/discord.js/src/util/Collection.js @@ -0,0 +1,532 @@ +const util = require('util'); + +/** + * 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 + * @name Collection#_array + * @type {?Array} + * @private + */ + Object.defineProperty(this, '_array', { value: null, writable: true, configurable: true }); + + /** + * Cached array for the `keyArray()` method - will be reset to `null` whenever `set()` or `delete()` are called + * @name Collection#_keyArray + * @type {?Array} + * @private + */ + Object.defineProperty(this, '_keyArray', { value: null, writable: true, configurable: true }); + } + + 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 behavior, use `[...collection.values()]` or + * `Array.from(collection.values())` instead. + * @returns {Array} + */ + array() { + if (!this._array || this._array.length !== this.size) this._array = [...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 behavior, use `[...collection.keys()]` or + * `Array.from(collection.keys())` instead. + * @returns {Array} + */ + keyArray() { + if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = [...this.keys()]; + return this._keyArray; + } + + /** + * Obtains the first value(s) in this collection. + * @param {number} [count] Number of values to obtain from the beginning + * @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length + */ + first(count) { + if (count === undefined) return this.values().next().value; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + count = Math.min(this.size, count); + const arr = new Array(count); + const iter = this.values(); + for (let i = 0; i < count; i++) arr[i] = iter.next().value; + return arr; + } + + /** + * Obtains the first key(s) in this collection. + * @param {number} [count] Number of keys to obtain from the beginning + * @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length + */ + firstKey(count) { + if (count === undefined) return this.keys().next().value; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + count = Math.min(this.size, count); + const arr = new Array(count); + const iter = this.keys(); + for (let i = 0; i < count; i++) arr[i] = iter.next().value; + return arr; + } + + /** + * Obtains the last value(s) in this collection. This relies on {@link Collection#array}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of values to obtain from the end + * @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length + */ + last(count) { + const arr = this.array(); + if (count === undefined) return arr[arr.length - 1]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + return arr.slice(-count); + } + + /** + * Obtains the last key(s) in this collection. This relies on {@link Collection#keyArray}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of keys to obtain from the end + * @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length + */ + lastKey(count) { + const arr = this.keyArray(); + if (count === undefined) return arr[arr.length - 1]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + return arr.slice(-count); + } + + /** + * Obtains random value(s) from this collection. This relies on {@link Collection#array}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of values to obtain randomly + * @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length + */ + random(count) { + let arr = this.array(); + if (count === undefined) return arr[Math.floor(Math.random() * arr.length)]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + if (arr.length === 0) return []; + const rand = new Array(count); + arr = arr.slice(); + for (let i = 0; i < count; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; + return rand; + } + + /** + * Obtains random key(s) from this collection. This relies on {@link Collection#keyArray}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of keys to obtain randomly + * @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length + */ + randomKey(count) { + let arr = this.keyArray(); + if (count === undefined) return arr[Math.floor(Math.random() * arr.length)]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + if (arr.length === 0) return []; + const rand = new Array(count); + arr = arr.slice(); + for (let i = 0; i < count; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; + return rand; + } + + /** + * 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} + * @deprecated + * @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>All collections used in Discord.js are mapped using their `id` property, and if you want to find by id you + * should use the `get` method. 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.'); + 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'); + */ + findKey(propOrFn, value) { + /* eslint-enable max-len */ + 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} + * @deprecated + * @example + * if (collection.exists('username', 'Bob')) { + * console.log('user here!'); + * } + */ + exists(prop, value) { + return Boolean(this.find(prop, value)); + } + + /** + * Removes entries that satisfy the provided filter function. + * @param {Function} fn Function used to test (should return a boolean) + * @param {Object} [thisArg] Value to use as `this` when executing function + * @returns {number} The number of removed entries + */ + sweep(fn, thisArg) { + if (thisArg) fn = fn.bind(thisArg); + const previousSize = this.size; + for (const [key, val] of this) { + if (fn(val, key, this)) this.delete(key); + } + return previousSize - this.size; + } + + /** + * 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} + * @deprecated + */ + 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; + } + + /** + * Partitions the collection into two collections where the first collection + * contains the items that passed and the second contains the items that failed. + * @param {Function} fn Function used to test (should return a boolean) + * @param {*} [thisArg] Value to use as `this` when executing function + * @returns {Collection[]} + * @example const [big, small] = collection.partition(guild => guild.memberCount > 250); + */ + partition(fn, thisArg) { + if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); + const results = [new Collection(), new Collection()]; + for (const [key, val] of this) { + if (fn(val, key, this)) { + results[0].set(key, val); + } else { + results[1].set(key, 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; + } + + /** + * Identical to + * [Map.forEach()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach), + * but returns the collection instead of undefined. + * @param {Function} fn Function to execute for each element + * @param {*} [thisArg] Value to use as `this` when executing function + * @returns {Collection} + * @example + * collection + * .tap(user => console.log(user.username)) + * .filter(user => user.bot) + * .tap(user => console.log(user.username)); + */ + tap(fn, thisArg) { + this.forEach(fn, thisArg); + return this; + } + + /** + * Creates an identical shallow copy of this collection. + * @returns {Collection} + * @example const newColl = someColl.clone(); + */ + clone() { + return new this.constructor(this); + } + + /** + * 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 = this.clone(); + 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)); + }); + } + + /** + * The sort() method sorts the elements of a collection in place and returns the collection. + * The sort is not necessarily stable. The default sort order is according to string Unicode code points. + * @param {Function} [compareFunction] Specifies a function that defines the sort order. + * if omitted, the collection is sorted according to each character's Unicode code point value, + * according to the string conversion of each element. + * @returns {Collection} + */ + sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) { + return new Collection([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); + } +} + +Collection.prototype.findAll = + util.deprecate(Collection.prototype.findAll, 'Collection#findAll: use Collection#filter instead'); + +Collection.prototype.filterArray = + util.deprecate(Collection.prototype.filterArray, 'Collection#filterArray: use Collection#filter instead'); + +Collection.prototype.exists = + util.deprecate(Collection.prototype.exists, 'Collection#exists: use Collection#some instead'); + +Collection.prototype.find = function find(propOrFn, value) { + if (typeof propOrFn === 'string') { + process.emitWarning('Collection#find: pass a function instead', 'DeprecationWarning'); + if (typeof value === 'undefined') throw new Error('Value must be specified.'); + 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.'); + } +}; + +Collection.prototype.findKey = function findKey(propOrFn, value) { + if (typeof propOrFn === 'string') { + process.emitWarning('Collection#findKey: pass a function instead', 'DeprecationWarning'); + 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.'); + } +}; + +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..3a2392b --- /dev/null +++ b/node_modules/discord.js/src/util/Constants.js @@ -0,0 +1,943 @@ +exports.Package = require('../../package.json'); + +/** + * Options for a client. + * @typedef {Object} ClientOptions + * @property {string} [apiRequestMethod='sequential'] One of `sequential` or `burst`. The sequential handler executes + * all requests in the order they are triggered, whereas the burst handler runs multiple in parallel, and doesn't + * provide the guarantee of any particular order. Burst mode is more likely to hit a 429 ratelimit error by its nature, + * and is therefore slightly riskier to use. + * @property {number} [shardId=0] ID of the shard to run + * @property {number} [shardCount=0] Total 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 a message should stay in the cache until it is considered + * sweepable (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 (should be avoided whenever possible) + * @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone} + * @property {boolean} [sync=false] Whether to periodically sync guilds (for user accounts) + * @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their + * corresponding websocket events + * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST + * requests (higher values will reduce rate-limiting errors on bad connections) + * @property {number} [retryLimit=Infinity] How many times to retry on 5XX errors + * (Infinity for indefinite amount of retries) + * @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 + * @property {HTTPOptions} [http] HTTP options + */ +exports.DefaultOptions = { + apiRequestMethod: 'sequential', + shardId: 0, + shardCount: 0, + messageCacheMaxSize: 200, + messageCacheLifetime: 0, + messageSweepInterval: 0, + fetchAllMembers: false, + disableEveryone: false, + sync: false, + restWsBridgeTimeout: 5000, + retryLimit: Infinity, + 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: require('os').platform() !== 'browser', + properties: { + $os: process ? process.platform : 'discord.js', + $browser: 'discord.js', + $device: 'discord.js', + $referrer: '', + $referring_domain: '', + }, + version: 6, + }, + + /** + * HTTP options + * @typedef {Object} HTTPOptions + * @property {number} [version=7] API version to use + * @property {string} [api='https://discordapp.com/api'] Base url of the API + * @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN + * @property {string} [invite='https://discord.gg'] Base url of invites + */ + http: { + version: 7, + host: 'https://discordapp.com', + cdn: 'https://cdn.discordapp.com', + }, +}; + +exports.WSCodes = { + 1000: 'Connection gracefully closed', + 4004: 'Tried to identify with an invalid token', + 4010: 'Sharding data provided was invalid', + 4011: 'Shard would be on too many guilds if connected', +}; + +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.', + SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.', + INVALID_TOKEN: 'An invalid token was provided.', +}; + +const Endpoints = exports.Endpoints = { + User: userID => { + if (userID.id) userID = userID.id; + const base = `/users/${userID}`; + return { + toString: () => base, + channels: `${base}/channels`, + profile: `${base}/profile`, + relationships: `${base}/relationships`, + settings: `${base}/settings`, + Relationship: uID => `${base}/relationships/${uID}`, + Guild: guildID => ({ + toString: () => `${base}/guilds/${guildID}`, + settings: `${base}/guilds/${guildID}/settings`, + }), + Note: id => `${base}/notes/${id}`, + Mentions: (limit, roles, everyone, guildID) => + `${base}/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`, + Avatar: (root, hash) => { + if (userID === '1') return hash; + return Endpoints.CDN(root).Avatar(userID, hash); + }, + }; + }, + guilds: '/guilds', + Guild: guildID => { + if (guildID.id) guildID = guildID.id; + const base = `/guilds/${guildID}`; + return { + toString: () => base, + prune: `${base}/prune`, + embed: `${base}/embed`, + bans: `${base}/bans`, + integrations: `${base}/integrations`, + members: `${base}/members`, + channels: `${base}/channels`, + invites: `${base}/invites`, + roles: `${base}/roles`, + emojis: `${base}/emojis`, + search: `${base}/messages/search`, + vanityURL: `${base}/vanity-url`, + voiceRegions: `${base}/regions`, + webhooks: `${base}/webhooks`, + ack: `${base}/ack`, + settings: `${base}/settings`, + auditLogs: `${base}/audit-logs`, + Emoji: emojiID => `${base}/emojis/${emojiID}`, + Icon: (root, hash) => Endpoints.CDN(root).Icon(guildID, hash), + Banner: (root, hash) => Endpoints.CDN(root).Banner(guildID, hash), + Splash: (root, hash) => Endpoints.CDN(root).Splash(guildID, hash), + Role: roleID => `${base}/roles/${roleID}`, + Member: memberID => { + if (memberID.id) memberID = memberID.id; + const mbase = `${base}/members/${memberID}`; + return { + toString: () => mbase, + Role: roleID => `${mbase}/roles/${roleID}`, + nickname: `${base}/members/@me/nick`, + }; + }, + Integration: id => `${base}/integrations/${id}`, + }; + }, + channels: '/channels', + Channel: channelID => { + if (channelID.id) channelID = channelID.id; + const base = `/channels/${channelID}`; + return { + toString: () => base, + messages: { + toString: () => `${base}/messages`, + bulkDelete: `${base}/messages/bulk-delete`, + }, + invites: `${base}/invites`, + typing: `${base}/typing`, + permissions: `${base}/permissions`, + webhooks: `${base}/webhooks`, + search: `${base}/messages/search`, + pins: `${base}/pins`, + Icon: (root, hash) => Endpoints.CDN(root).GDMIcon(channelID, hash), + Pin: messageID => `${base}/pins/${messageID}`, + Recipient: recipientID => `${base}/recipients/${recipientID}`, + Message: messageID => { + if (messageID.id) messageID = messageID.id; + const mbase = `${base}/messages/${messageID}`; + return { + toString: () => mbase, + reactions: `${mbase}/reactions`, + ack: `${mbase}/ack`, + Reaction: emoji => { + const rbase = `${mbase}/reactions/${emoji}`; + return { + toString: () => rbase, + User: userID => `${rbase}/${userID}`, + }; + }, + }; + }, + }; + }, + Message: m => exports.Endpoints.Channel(m.channel).Message(m), + Member: m => exports.Endpoints.Guild(m.guild).Member(m), + CDN(root) { + return { + Emoji: (emojiID, format = 'png') => `${root}/emojis/${emojiID}.${format}`, + Asset: name => `${root}/assets/${name}`, + Avatar: (userID, hash) => `${root}/avatars/${userID}/${hash}.${hash.startsWith('a_') ? 'gif' : 'png?size=2048'}`, + Icon: (guildID, hash) => `${root}/icons/${guildID}/${hash}.jpg`, + Banner: (guildID, hash) => `${root}/banners/${guildID}/${hash}.jpg`, + AppIcon: (clientID, hash) => `${root}/app-icons/${clientID}/${hash}.png`, + AppAsset: (clientID, hash) => `${root}/app-assets/${clientID}/${hash}.png`, + GDMIcon: (channelID, hash) => `${root}/channel-icons/${channelID}/${hash}.jpg?size=2048`, + Splash: (guildID, hash) => `${root}/splashes/${guildID}/${hash}.jpg`, + TeamIcon: (teamID, hash) => `${root}/team-icons/${teamID}/${hash}.jpg`, + }; + }, + OAUTH2: { + Application: appID => { + const base = `/oauth2/applications/${appID}`; + return { + toString: () => base, + resetSecret: `${base}/reset`, + resetToken: `${base}/bot/reset`, + }; + }, + App: appID => `/oauth2/authorize?client_id=${appID}`, + }, + login: '/auth/login', + logout: '/auth/logout', + voiceRegions: '/voice/regions', + gateway: { + toString: () => '/gateway', + bot: '/gateway/bot', + }, + Invite: inviteID => `/invite/${inviteID}?with_counts=true`, + inviteLink: id => `https://discord.gg/${id}`, + Webhook: (webhookID, token) => `/webhooks/${webhookID}${token ? `/${token}` : ''}`, +}; + + +/** + * The current status of the client. Here are the available statuses: + * * READY: 0 + * * CONNECTING: 1 + * * RECONNECTING: 2 + * * IDLE: 3 + * * NEARLY: 4 + * * DISCONNECTED: 5 + * @typedef {number} Status + */ +exports.Status = { + READY: 0, + CONNECTING: 1, + RECONNECTING: 2, + IDLE: 3, + NEARLY: 4, + DISCONNECTED: 5, +}; + +/** + * The current status of a voice connection. Here are the available statuses: + * * CONNECTED: 0 + * * CONNECTING: 1 + * * AUTHENTICATING: 2 + * * RECONNECTING: 3 + * * DISCONNECTED: 4 + * @typedef {number} VoiceStatus + */ +exports.VoiceStatus = { + CONNECTED: 0, + CONNECTING: 1, + AUTHENTICATING: 2, + RECONNECTING: 3, + DISCONNECTED: 4, +}; + +exports.ChannelTypes = { + TEXT: 0, + DM: 1, + VOICE: 2, + GROUP_DM: 3, + CATEGORY: 4, + NEWS: 5, + STORE: 6, +}; + +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 = { + RATE_LIMIT: 'rateLimit', + READY: 'ready', + RESUME: 'resume', + 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_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', + 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', + INVITE_CREATE: 'inviteCreate', + INVITE_DELETE: 'inviteDelete', + 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_EMOJI: 'messageReactionRemoveEmoji', + MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', + USER_UPDATE: 'userUpdate', + USER_NOTE_UPDATE: 'userNoteUpdate', + USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', + USER_GUILD_SETTINGS_UPDATE: 'clientUserGuildSettingsUpdate', + PRESENCE_UPDATE: 'presenceUpdate', + VOICE_STATE_UPDATE: 'voiceStateUpdate', + TYPING_START: 'typingStart', + TYPING_STOP: 'typingStop', + WEBHOOKS_UPDATE: 'webhookUpdate', + DISCONNECT: 'disconnect', + RECONNECTING: 'reconnecting', + ERROR: 'error', + WARN: 'warn', + DEBUG: 'debug', +}; + +/** + * <info>Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users</info> + * The type of an activity of a users presence, e.g. `PLAYING`. Here are the available types: + * * PLAYING + * * STREAMING + * * LISTENING + * * WATCHING + * * CUSTOM_STATUS + * @typedef {string} ActivityType + */ +exports.ActivityTypes = [ + 'PLAYING', + 'STREAMING', + 'LISTENING', + 'WATCHING', + 'CUSTOM_STATUS', +]; + +/** + * Numeric activity flags. All available properties: + * * `INSTANCE` + * * `JOIN` + * * `SPECTATE` + * * `JOIN_REQUEST` + * * `SYNC` + * * `PLAY` + * @typedef {string} ActivityFlag + * @see {@link https://discordapp.com/developers/docs/topics/gateway#activity-object-activity-flags} + */ +exports.ActivityFlags = { + INSTANCE: 1 << 0, + JOIN: 1 << 1, + SPECTATE: 1 << 2, + JOIN_REQUEST: 1 << 3, + SYNC: 1 << 4, + PLAY: 1 << 5, +}; + +/** + * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: + * * READY + * * RESUMED + * * GUILD_SYNC + * * GUILD_CREATE + * * GUILD_DELETE + * * GUILD_UPDATE + * * GUILD_MEMBER_ADD + * * GUILD_MEMBER_REMOVE + * * GUILD_MEMBER_UPDATE + * * GUILD_MEMBERS_CHUNK + * * GUILD_INTEGRATIONS_UPDATE + * * 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_EMOJI + * * MESSAGE_REACTION_REMOVE_ALL + * * USER_UPDATE + * * USER_NOTE_UPDATE + * * USER_SETTINGS_UPDATE + * * PRESENCE_UPDATE + * * VOICE_STATE_UPDATE + * * TYPING_START + * * VOICE_SERVER_UPDATE + * * RELATIONSHIP_ADD + * * RELATIONSHIP_REMOVE + * * WEBHOOKS_UPDATE + * @typedef {string} WSEventType + */ +exports.WSEvents = { + READY: 'READY', + RESUMED: 'RESUMED', + 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_INTEGRATIONS_UPDATE: 'GUILD_INTEGRATIONS_UPDATE', + 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', + INVITE_CREATE: 'INVITE_CREATE', + INVITE_DELETE: 'INVITE_DELETE', + 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_EMOJI: 'MESSAGE_REACTION_REMOVE_EMOJI', + MESSAGE_REACTION_REMOVE_ALL: 'MESSAGE_REACTION_REMOVE_ALL', + USER_UPDATE: 'USER_UPDATE', + USER_NOTE_UPDATE: 'USER_NOTE_UPDATE', + USER_SETTINGS_UPDATE: 'USER_SETTINGS_UPDATE', + USER_GUILD_SETTINGS_UPDATE: 'USER_GUILD_SETTINGS_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', + WEBHOOKS_UPDATE: 'WEBHOOKS_UPDATE', +}; + +/** + * The type of a message, e.g. `DEFAULT`. Here are the available types: + * * DEFAULT + * * RECIPIENT_ADD + * * RECIPIENT_REMOVE + * * CALL + * * CHANNEL_NAME_CHANGE + * * CHANNEL_ICON_CHANGE + * * PINS_ADD + * * GUILD_MEMBER_JOIN + * * USER_PREMIUM_GUILD_SUBSCRIPTION + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 + * * CHANNEL_FOLLOW_ADD + * * GUILD_DISCOVERY_DISQUALIFIED + * * GUILD_DISCOVERY_REQUALIFIED + * @typedef {string} MessageType + */ +exports.MessageTypes = [ + 'DEFAULT', + 'RECIPIENT_ADD', + 'RECIPIENT_REMOVE', + 'CALL', + 'CHANNEL_NAME_CHANGE', + 'CHANNEL_ICON_CHANGE', + 'PINS_ADD', + 'GUILD_MEMBER_JOIN', + 'USER_PREMIUM_GUILD_SUBSCRIPTION', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', + 'CHANNEL_FOLLOW_ADD', + // 13 isn't yet documented + null, + 'GUILD_DISCOVERY_DISQUALIFIED', + 'GUILD_DISCOVERY_REQUALIFIED', +]; + +/** + * The type of a message notification setting. Here are the available types: + * * EVERYTHING + * * MENTIONS + * * NOTHING + * * INHERIT (only for GuildChannel) + * @typedef {string} MessageNotificationType + */ +exports.MessageNotificationTypes = [ + 'EVERYTHING', + 'MENTIONS', + 'NOTHING', + 'INHERIT', +]; + +exports.DefaultAvatars = { + BLURPLE: '6debd47ed13483642cf09e832ed0bc1b', + GREY: '322c936a8c8be1b803cd94861bdfa868', + GREEN: 'dd4dbc0016779df1378e7812eabaa04d', + ORANGE: '0e291f67c9274a1abdddeb3fd919cbaa', + RED: '1cbd08c76f8af6dddce02c5138971129', +}; + +exports.ExplicitContentFilterTypes = [ + 'DISABLED', + 'NON_FRIENDS', + 'FRIENDS_AND_NON_FRIENDS', +]; + +exports.UserSettingsMap = { + /** + * Automatically convert emoticons in your messages to emoji + * For example, when you type `:-)` Discord will convert it to 😃 + * @name ClientUserSettings#convertEmoticons + * @type {boolean} + */ + convert_emoticons: 'convertEmoticons', + + /** + * If new guilds should automatically disable DMs between you and its members + * @name ClientUserSettings#defaultGuildsRestricted + * @type {boolean} + */ + default_guilds_restricted: 'defaultGuildsRestricted', + + /** + * Automatically detect accounts from services like Steam and Blizzard when you open the Discord client + * @name ClientUserSettings#detectPlatformAccounts + * @type {boolean} + */ + detect_platform_accounts: 'detectPlatformAccounts', + + /** + * Developer Mode exposes context menu items helpful for people writing bots using the Discord API + * @name ClientUserSettings#developerMode + * @type {boolean} + */ + developer_mode: 'developerMode', + + /** + * Allow playback and usage of the `/tts` command + * @name ClientUserSettings#enableTTSCommand + * @type {boolean} + */ + enable_tts_command: 'enableTTSCommand', + + /** + * The theme of the client. Either `light` or `dark` + * @name ClientUserSettings#theme + * @type {string} + */ + theme: 'theme', + + /** + * Last status set in the client + * @name ClientUserSettings#status + * @type {PresenceStatus} + */ + status: 'status', + + /** + * Display currently running game as status message + * @name ClientUserSettings#showCurrentGame + * @type {boolean} + */ + show_current_game: 'showCurrentGame', + + /** + * Display images, videos, and lolcats when uploaded directly to Discord + * @name ClientUserSettings#inlineAttachmentMedia + * @type {boolean} + */ + inline_attachment_media: 'inlineAttachmentMedia', + + /** + * Display images, videos, and lolcats when uploaded posted as links in chat + * @name ClientUserSettings#inlineEmbedMedia + * @type {boolean} + */ + inline_embed_media: 'inlineEmbedMedia', + + /** + * Language the Discord client will use, as an RFC 3066 language identifier + * @name ClientUserSettings#locale + * @type {string} + */ + locale: 'locale', + + /** + * Display messages in compact mode + * @name ClientUserSettings#messageDisplayCompact + * @type {boolean} + */ + message_display_compact: 'messageDisplayCompact', + + /** + * Show emoji reactions on messages + * @name ClientUserSettings#renderReactions + * @type {boolean} + */ + render_reactions: 'renderReactions', + + /** + * Array of snowflake IDs for guilds, in the order they appear in the Discord client + * @name ClientUserSettings#guildPositions + * @type {Snowflake[]} + */ + guild_positions: 'guildPositions', + + /** + * Array of snowflake IDs for guilds which you will not recieve DMs from + * @name ClientUserSettings#restrictedGuilds + * @type {Snowflake[]} + */ + restricted_guilds: 'restrictedGuilds', + + explicit_content_filter: function explicitContentFilter(type) { // eslint-disable-line func-name-matching + /** + * Safe direct messaging; force people's messages with images to be scanned before they are sent to you. + * One of `DISABLED`, `NON_FRIENDS`, `FRIENDS_AND_NON_FRIENDS` + * @name ClientUserSettings#explicitContentFilter + * @type {string} + */ + return exports.ExplicitContentFilterTypes[type]; + }, + friend_source_flags: function friendSources(flags) { // eslint-disable-line func-name-matching + /** + * Who can add you as a friend + * @name ClientUserSettings#friendSources + * @type {Object} + * @property {boolean} all Mutual friends and mutual guilds + * @property {boolean} mutualGuilds Only mutual guilds + * @property {boolean} mutualFriends Only mutual friends + */ + return { + all: flags.all || false, + mutualGuilds: flags.all ? true : flags.mutual_guilds || false, + mutualFriends: flags.all ? true : flags.mutualFriends || false, + }; + }, +}; + +exports.UserGuildSettingsMap = { + message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching + /** + * The type of message that should notify you + * @name ClientUserGuildSettings#messageNotifications + * @type {MessageNotificationType} + */ + return exports.MessageNotificationTypes[type]; + }, + /** + * Whether to receive mobile push notifications + * @name ClientUserGuildSettings#mobilePush + * @type {boolean} + */ + mobile_push: 'mobilePush', + /** + * Whether the guild is muted + * @name ClientUserGuildSettings#muted + * @type {boolean} + */ + muted: 'muted', + /** + * Whether to suppress everyone mention + * @name ClientUserGuildSettings#suppressEveryone + * @type {boolean} + */ + suppress_everyone: 'suppressEveryone', + /** + * A collection containing all the channel overrides + * @name ClientUserGuildSettings#channelOverrides + * @type {Collection<ClientUserChannelOverride>} + */ + channel_overrides: 'channelOverrides', +}; + +exports.UserChannelOverrideMap = { + message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching + /** + * The type of message that should notify you + * @name ClientUserChannelOverride#messageNotifications + * @type {MessageNotificationType} + */ + return exports.MessageNotificationTypes[type]; + }, + /** + * Whether the channel is muted + * @name ClientUserChannelOverride#muted + * @type {boolean} + */ + muted: 'muted', +}; + +exports.Colors = { + DEFAULT: 0x000000, + WHITE: 0xFFFFFF, + AQUA: 0x1ABC9C, + GREEN: 0x2ECC71, + BLUE: 0x3498DB, + PURPLE: 0x9B59B6, + LUMINOUS_VIVID_PINK: 0xE91E63, + GOLD: 0xF1C40F, + ORANGE: 0xE67E22, + RED: 0xE74C3C, + GREY: 0x95A5A6, + NAVY: 0x34495E, + DARK_AQUA: 0x11806A, + DARK_GREEN: 0x1F8B4C, + DARK_BLUE: 0x206694, + DARK_PURPLE: 0x71368A, + DARK_VIVID_PINK: 0xAD1457, + DARK_GOLD: 0xC27C0E, + DARK_ORANGE: 0xA84300, + DARK_RED: 0x992D22, + DARK_GREY: 0x979C9F, + DARKER_GREY: 0x7F8C8D, + LIGHT_GREY: 0xBCC0C0, + DARK_NAVY: 0x2C3E50, + BLURPLE: 0x7289DA, + GREYPLE: 0x99AAB5, + DARK_BUT_NOT_BLACK: 0x2C2F33, + NOT_QUITE_BLACK: 0x23272A, +}; + +/** + * The value set for the verification levels for a guild: + * * None + * * Low + * * Medium + * * (╯°□°)╯︵ ┻━┻ + * * ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ + * @typedef {string} VerificationLevel + */ +exports.VerificationLevels = [ + 'None', + 'Low', + 'Medium', + '(╯°□°)╯︵ ┻━┻', + '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', +]; + +/** + * An error encountered while performing an API request. Here are the potential errors: + * * UNKNOWN_ACCOUNT + * * UNKNOWN_APPLICATION + * * UNKNOWN_CHANNEL + * * UNKNOWN_GUILD + * * UNKNOWN_INTEGRATION + * * UNKNOWN_INVITE + * * UNKNOWN_MEMBER + * * UNKNOWN_MESSAGE + * * UNKNOWN_OVERWRITE + * * UNKNOWN_PROVIDER + * * UNKNOWN_ROLE + * * UNKNOWN_TOKEN + * * UNKNOWN_USER + * * UNKNOWN_EMOJI + * * UNKNOWN_WEBHOOK + * * BOT_PROHIBITED_ENDPOINT + * * BOT_ONLY_ENDPOINT + * * MAXIMUM_GUILDS + * * MAXIMUM_FRIENDS + * * MAXIMUM_PINS + * * MAXIMUM_ROLES + * * MAXIMUM_REACTIONS + * * MAXIMUM_CHANNELS + * * MAXIMUM_INVITES + * * UNAUTHORIZED + * * USER_BANNED + * * MISSING_ACCESS + * * INVALID_ACCOUNT_TYPE + * * CANNOT_EXECUTE_ON_DM + * * EMBED_DISABLED + * * CANNOT_EDIT_MESSAGE_BY_OTHER + * * CANNOT_SEND_EMPTY_MESSAGE + * * CANNOT_MESSAGE_USER + * * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL + * * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH + * * OAUTH2_APPLICATION_BOT_ABSENT + * * MAXIMUM_OAUTH2_APPLICATIONS + * * INVALID_OAUTH_STATE + * * MISSING_PERMISSIONS + * * INVALID_AUTHENTICATION_TOKEN + * * NOTE_TOO_LONG + * * INVALID_BULK_DELETE_QUANTITY + * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL + * * INVALID_OR_TAKEN_INVITE_CODE + * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE + * * INVALID_OAUTH_TOKEN + * * BULK_DELETE_MESSAGE_TOO_OLD + * * INVALID_FORM_BODY + * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT + * * INVALID_API_VERSION + * * REACTION_BLOCKED + * * RESOURCE_OVERLOADED + * @typedef {string} APIError + */ +exports.APIErrors = { + UNKNOWN_ACCOUNT: 10001, + UNKNOWN_APPLICATION: 10002, + UNKNOWN_CHANNEL: 10003, + UNKNOWN_GUILD: 10004, + UNKNOWN_INTEGRATION: 10005, + UNKNOWN_INVITE: 10006, + UNKNOWN_MEMBER: 10007, + UNKNOWN_MESSAGE: 10008, + UNKNOWN_OVERWRITE: 10009, + UNKNOWN_PROVIDER: 10010, + UNKNOWN_ROLE: 10011, + UNKNOWN_TOKEN: 10012, + UNKNOWN_USER: 10013, + UNKNOWN_EMOJI: 10014, + UNKNOWN_WEBHOOK: 10015, + BOT_PROHIBITED_ENDPOINT: 20001, + BOT_ONLY_ENDPOINT: 20002, + MAXIMUM_GUILDS: 30001, + MAXIMUM_FRIENDS: 30002, + MAXIMUM_PINS: 30003, + MAXIMUM_ROLES: 30005, + MAXIMUM_REACTIONS: 30010, + MAXIMUM_CHANNELS: 30013, + MAXIMUM_INVITES: 30016, + UNAUTHORIZED: 40001, + USER_BANNED: 40007, + MISSING_ACCESS: 50001, + INVALID_ACCOUNT_TYPE: 50002, + CANNOT_EXECUTE_ON_DM: 50003, + EMBED_DISABLED: 50004, + CANNOT_EDIT_MESSAGE_BY_OTHER: 50005, + CANNOT_SEND_EMPTY_MESSAGE: 50006, + CANNOT_MESSAGE_USER: 50007, + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008, + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009, + OAUTH2_APPLICATION_BOT_ABSENT: 50010, + MAXIMUM_OAUTH2_APPLICATIONS: 50011, + INVALID_OAUTH_STATE: 50012, + MISSING_PERMISSIONS: 50013, + INVALID_AUTHENTICATION_TOKEN: 50014, + NOTE_TOO_LONG: 50015, + INVALID_BULK_DELETE_QUANTITY: 50016, + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, + INVALID_OR_TAKEN_INVITE_CODE: 50020, + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, + INVALID_OAUTH_TOKEN: 50025, + BULK_DELETE_MESSAGE_TOO_OLD: 50034, + INVALID_FORM_BODY: 50035, + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, + INVALID_API_VERSION: 50041, + REACTION_BLOCKED: 90001, + RESOURCE_OVERLOADED: 130000, +}; + +/** + * The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types: + * * ALL + * * MENTIONS + * @typedef {string} DefaultMessageNotifications + */ +exports.DefaultMessageNotifications = [ + 'ALL', + 'MENTIONS', +]; + +/** + * The value set for a team members's membership state: + * * INVITED + * * ACCEPTED + * @typedef {string} MembershipStates + */ +exports.MembershipStates = [ + // They start at 1 + null, + 'INVITED', + 'ACCEPTED', +]; + +/** + * The value set for a webhook's type: + * * Incoming + * * Channel Follower + * @typedef {string} WebhookTypes + */ +exports.WebhookTypes = [ + // They start at 1 + null, + 'Incoming', + 'Channel Follower', +]; diff --git a/node_modules/discord.js/src/util/MessageFlags.js b/node_modules/discord.js/src/util/MessageFlags.js new file mode 100644 index 0000000..88c8014 --- /dev/null +++ b/node_modules/discord.js/src/util/MessageFlags.js @@ -0,0 +1,36 @@ +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link Message#flags} bitfield. + * @extends {BitField} + */ +class MessageFlags extends BitField {} + +/** + * Data that can be resolved to give a permission number. This can be: + * * A string (see {@link MessageFlags.FLAGS}) + * * A message flag + * * An instance of MessageFlags + * * An array of MessageFlagsResolvable + * @typedef {string|number|MessageFlags|MessageFlagsResolvable[]} MessageFlagsResolvable + */ + +/** + * Numeric message flags. All available properties: + * * `CROSSPOSTED` + * * `IS_CROSSPOST` + * * `SUPPRESS_EMBEDS` + * * `SOURCE_MESSAGE_DELETED` + * * `URGENT` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/resources/channel#message-object-message-flags} + */ +MessageFlags.FLAGS = { + CROSSPOSTED: 1 << 0, + IS_CROSSPOST: 1 << 1, + SUPPRESS_EMBEDS: 1 << 2, + SOURCE_MESSAGE_DELETED: 1 << 3, + URGENT: 1 << 4, +}; + +module.exports = MessageFlags; diff --git a/node_modules/discord.js/src/util/Permissions.js b/node_modules/discord.js/src/util/Permissions.js new file mode 100644 index 0000000..fa1d0e0 --- /dev/null +++ b/node_modules/discord.js/src/util/Permissions.js @@ -0,0 +1,234 @@ +const BitField = require('./BitField'); +const util = require('util'); + +/** + * Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of + * permissions in their guild, and each channel in the guild may also have {@link PermissionOverwrites} for the member + * that override their default permissions. + * @extends {BitField} + */ +class Permissions extends BitField { + /** + * @param {GuildMember} [member] Member the permissions are for **(deprecated)** + * @param {number|PermissionResolvable} permissions Permissions or bitfield to read from + */ + constructor(member, permissions) { + super(typeof member === 'object' && !(member instanceof Array) ? permissions : member); + + Object.defineProperty(this, '_member', { + writable: true, + value: typeof member === 'object' && !(member instanceof Array) ? member : null, + }); + } + + /** + * Member the permissions are for + * @type {GuildMember} + * @deprecated + */ + get member() { + return this._member; + } + + set member(value) { + this._member = value; + } + + /** + * Bitfield of the packed permissions + * @type {number} + * @see {@link Permissions#bitfield} + * @deprecated + * @readonly + */ + get raw() { + return this.bitfield; + } + + set raw(raw) { + this.bitfield = raw; + } + + /** + * Checks whether the bitfield has a permission, or any of multiple permissions. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {boolean} + */ + any(permission, checkAdmin = true) { + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.any(permission); + } + + /** + * Checks whether the bitfield has a permission, or multiple permissions. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {boolean} + */ + has(permission, checkAdmin = true) { + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.has(permission); + } + + /** + * 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} + * @see {@link Permissions#has} + * @deprecated + */ + hasPermission(permission, explicit = false) { + return this.has(permission, !explicit); + } + + /** + * 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} + * @see {@link Permissions#has} + * @deprecated + */ + hasPermissions(permissions, explicit = false) { + return this.has(permissions, !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} + * @see {@link Permissions#missing} + * @deprecated + */ + missingPermissions(permissions, explicit = false) { + return this.missing(permissions, !explicit); + } +} + +/** + * Data that can be resolved to give a permission number. This can be: + * * A string (see {@link Permissions.FLAGS}) + * * A permission number + * @typedef {string|number|Permissions|PermissionResolvable[]} PermissionResolvable + */ + +/** + * Numeric permission flags. All available properties: + * - `ADMINISTRATOR` (implicitly has *all* permissions, and bypasses all channel overwrites) + * - `CREATE_INSTANT_INVITE` (create invitations to the guild) + * - `KICK_MEMBERS` + * - `BAN_MEMBERS` + * - `MANAGE_CHANNELS` (edit and reorder channels) + * - `MANAGE_GUILD` (edit the guild information, region, etc.) + * - `ADD_REACTIONS` (add new reactions to messages) + * - `VIEW_AUDIT_LOG` + * - `PRIORITY_SPEAKER` + * - `STREAM` + * - `VIEW_CHANNEL` + * - `READ_MESSAGES` **(deprecated)** + * - `SEND_MESSAGES` + * - `SEND_TTS_MESSAGES` + * - `MANAGE_MESSAGES` (delete messages and reactions) + * - `EMBED_LINKS` (links posted will have a preview embedded) + * - `ATTACH_FILES` + * - `READ_MESSAGE_HISTORY` (view messages that were posted prior to opening Discord) + * - `MENTION_EVERYONE` + * - `USE_EXTERNAL_EMOJIS` (use emojis from different guilds) + * - `EXTERNAL_EMOJIS` **(deprecated)** + * - `CONNECT` (connect to a voice channel) + * - `SPEAK` (speak in a voice channel) + * - `MUTE_MEMBERS` (mute members across all voice channels) + * - `DEAFEN_MEMBERS` (deafen members across all voice channels) + * - `MOVE_MEMBERS` (move members between voice channels) + * - `USE_VAD` (use voice activity detection) + * - `CHANGE_NICKNAME` + * - `MANAGE_NICKNAMES` (change other members' nicknames) + * - `MANAGE_ROLES` + * - `MANAGE_ROLES_OR_PERMISSIONS` **(deprecated)** + * - `MANAGE_WEBHOOKS` + * - `MANAGE_EMOJIS` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/topics/permissions} + */ +Permissions.FLAGS = { + 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, + VIEW_AUDIT_LOG: 1 << 7, + PRIORITY_SPEAKER: 1 << 8, + STREAM: 1 << 9, + + VIEW_CHANNEL: 1 << 10, + 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, + USE_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: 1 << 28, + MANAGE_ROLES_OR_PERMISSIONS: 1 << 28, + MANAGE_WEBHOOKS: 1 << 29, + MANAGE_EMOJIS: 1 << 30, +}; + +/** + * Bitfield representing every permission combined + * @type {number} + */ +Permissions.ALL = Object.keys(Permissions.FLAGS).reduce((all, p) => all | Permissions.FLAGS[p], 0); + +/** + * Bitfield representing the default permissions for users + * @type {number} + */ +Permissions.DEFAULT = 104324673; + +/** + * @class EvaluatedPermissions + * @classdesc The final evaluated permissions for a member in a channel + * @see {@link Permissions} + * @deprecated + */ + +Permissions.prototype.hasPermission = util.deprecate(Permissions.prototype.hasPermission, + 'EvaluatedPermissions#hasPermission is deprecated, use Permissions#has instead'); +Permissions.prototype.hasPermissions = util.deprecate(Permissions.prototype.hasPermissions, + 'EvaluatedPermissions#hasPermissions is deprecated, use Permissions#has instead'); +Permissions.prototype.missingPermissions = util.deprecate(Permissions.prototype.missingPermissions, + 'EvaluatedPermissions#missingPermissions is deprecated, use Permissions#missing instead'); +Object.defineProperty(Permissions.prototype, 'raw', { + get: util + .deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'raw').get, + 'EvaluatedPermissions#raw is deprecated use Permissions#bitfield instead'), + set: util.deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'raw').set, + 'EvaluatedPermissions#raw is deprecated use Permissions#bitfield instead'), +}); +Object.defineProperty(Permissions.prototype, 'member', { + get: util + .deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'member').get, + 'EvaluatedPermissions#member is deprecated'), + set: util + .deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'member').set, + 'EvaluatedPermissions#member is deprecated'), +}); + +module.exports = Permissions; diff --git a/node_modules/discord.js/src/util/Snowflake.js b/node_modules/discord.js/src/util/Snowflake.js new file mode 100644 index 0000000..2775266 --- /dev/null +++ b/node_modules/discord.js/src/util/Snowflake.js @@ -0,0 +1,82 @@ +const Long = require('long'); + +// Discord epoch (2015-01-01T00:00:00.000Z) +const EPOCH = 1420070400000; +let INCREMENT = 0; + +/** + * A container for useful snowflake-related methods. + */ +class SnowflakeUtil { + constructor() { + throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + } + + /** + * A Twitter snowflake, except the epoch is 2015-01-01T00:00:00.000Z + * ``` + * If we have a snowflake '266241948824764416' we can represent it as binary: + * + * 64 22 17 12 0 + * 000000111011000111100001101001000101000000 00001 00000 000000000000 + * number of ms since Discord epoch worker pid increment + * ``` + * @typedef {string} Snowflake + */ + + /** + * Generates a Discord snowflake. + * <info>This hardcodes the worker ID as 1 and the process ID as 0.</info> + * @param {number|Date} [timestamp=Date.now()] Timestamp or date of the snowflake to generate + * @returns {Snowflake} The generated snowflake + */ + static generate(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + if (typeof timestamp !== 'number' || isNaN(timestamp)) { + throw new TypeError( + `"timestamp" argument must be a number (received ${isNaN(timestamp) ? 'NaN' : typeof timestamp})` + ); + } + if (INCREMENT >= 4095) INCREMENT = 0; + const BINARY = `${pad((timestamp - EPOCH).toString(2), 42)}0000100000${pad((INCREMENT++).toString(2), 12)}`; + return Long.fromString(BINARY, 2).toString(); + } + + /** + * A deconstructed snowflake. + * @typedef {Object} DeconstructedSnowflake + * @property {number} timestamp Timestamp the snowflake was created + * @property {Date} date Date the snowflake was created + * @property {number} workerID Worker ID in the snowflake + * @property {number} processID Process ID in the snowflake + * @property {number} increment Increment in the snowflake + * @property {string} binary Binary representation of the snowflake + */ + + /** + * Deconstructs a Discord snowflake. + * @param {Snowflake} snowflake Snowflake to deconstruct + * @returns {DeconstructedSnowflake} Deconstructed snowflake + */ + static deconstruct(snowflake) { + const BINARY = pad(Long.fromString(snowflake).toString(2), 64); + const res = { + timestamp: parseInt(BINARY.substring(0, 42), 2) + EPOCH, + workerID: parseInt(BINARY.substring(42, 47), 2), + processID: parseInt(BINARY.substring(47, 52), 2), + increment: parseInt(BINARY.substring(52, 64), 2), + binary: BINARY, + }; + Object.defineProperty(res, 'date', { + get: function get() { return new Date(this.timestamp); }, + enumerable: true, + }); + return res; + } +} + +function pad(v, n, c = '0') { + return String(v).length >= n ? String(v) : (String(c).repeat(n) + v).slice(-n); +} + +module.exports = SnowflakeUtil; diff --git a/node_modules/discord.js/src/util/SystemChannelFlags.js b/node_modules/discord.js/src/util/SystemChannelFlags.js new file mode 100644 index 0000000..9f74984 --- /dev/null +++ b/node_modules/discord.js/src/util/SystemChannelFlags.js @@ -0,0 +1,31 @@ +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link Guild#systemChannelFlags} bitfield. + * <info>Note that all event message types are enabled by default, + * and by setting their corresponding flags you are disabling them</info> + * @extends {BitField} + */ +class SystemChannelFlags extends BitField {} + +/** + * Data that can be resolved to give a sytem channel flag bitfield. This can be: + * * A string (see {@link SystemChannelFlags.FLAGS}) + * * A sytem channel flag + * * An instance of SystemChannelFlags + * * An Array of SystemChannelFlagsResolvable + * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable + */ + +/** + * Numeric system channel flags. All available properties: + * * `WELCOME_MESSAGE_DISABLED` + * * `BOOST_MESSAGE_DISABLED` + * @type {Object} + */ +SystemChannelFlags.FLAGS = { + WELCOME_MESSAGE_DISABLED: 1 << 0, + BOOST_MESSAGE_DISABLED: 1 << 1, +}; + +module.exports = SystemChannelFlags; diff --git a/node_modules/discord.js/src/util/Util.js b/node_modules/discord.js/src/util/Util.js new file mode 100644 index 0000000..a286b07 --- /dev/null +++ b/node_modules/discord.js/src/util/Util.js @@ -0,0 +1,238 @@ +const snekfetch = require('snekfetch'); +const Constants = require('./Constants'); +const ConstantsHttp = Constants.DefaultOptions.http; + +/** + * Contains various general-purpose utility methods. These functions are also available on the base `Discord` object. + */ +class Util { + constructor() { + throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + } + + /** + * Splits a string into multiple chunks at a designated character that do not exceed a specific length. + * @param {string} text Content to split + * @param {SplitOptions} [options] Options controlling the behaviour of the split + * @returns {string|string[]} + */ + static splitMessage(text, { maxLength = 1950, char = '\n', prepend = '', append = '' } = {}) { + if (text.length <= maxLength) return text; + const splitText = text.split(char); + if (splitText.some(chunk => chunk.length > maxLength)) { + 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; + } + + /** + * 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} + */ + static resolveString(data) { + if (typeof data === 'string') return data; + if (Array.isArray(data)) return data.join('\n'); + return String(data); + } + + /** + * Escapes any Discord-flavour markdown in a string. + * @param {string} text Content to escape + * @param {boolean} [onlyCodeBlock=false] Whether to only escape codeblocks (takes priority) + * @param {boolean} [onlyInlineCode=false] Whether to only escape inline code + * @returns {string} + */ + static 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'); + } + + /** + * Gets the recommended shard count from Discord. + * @param {string} token Discord auth token + * @param {number} [guildsPerShard=1000] Number of guilds per shard + * @returns {Promise<number>} The recommended number of shards + */ + static fetchRecommendedShards(token, guildsPerShard = 1000) { + return new Promise((resolve, reject) => { + if (!token) throw new Error('A token must be provided.'); + snekfetch.get(`${ConstantsHttp.host}/api/v${ConstantsHttp.version}${Constants.Endpoints.gateway.bot}`) + .set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`) + .end((err, res) => { + if (err) reject(err); + resolve(res.body.shards * (1000 / guildsPerShard)); + }); + }); + } + + /** + * Parses emoji info out of a string. The string must be one of: + * * A UTF-8 emoji (no ID) + * * A URL-encoded UTF-8 emoji (no ID) + * * A Discord custom emoji (`<:name:id>` or `<a:name:id>`) + * @param {string} text Emoji string to parse + * @returns {?Object} Object with `animated`, `name`, and `id` properties + * @private + */ + static parseEmoji(text) { + if (text.includes('%')) text = decodeURIComponent(text); + if (!text.includes(':')) return { animated: false, name: text, id: null }; + const m = text.match(/<?(a:)?(\w{2,32}):(\d{17,19})>?/); + if (!m) return null; + return { animated: Boolean(m[1]), name: m[2], id: m[3] }; + } + + /** + * Checks whether two arrays are equal or have the same elements. + * @param {Array<*>} a The first array. + * @param {Array<*>} b The second array. + * @returns {boolean} Whether the arrays are equal. + * @private + */ + static arraysEqual(a, b) { + if (a === b) return true; + if (a.length !== b.length) return false; + + const setA = new Set(a); + const setB = new Set(b); + + return a.every(e => setB.has(e)) && b.every(e => setA.has(e)); + } + + /** + * Shallow-copies an object with its class/prototype intact. + * @param {Object} obj Object to clone + * @returns {Object} + * @private + */ + static cloneObject(obj) { + return Object.assign(Object.create(obj), obj); + } + + /** + * Sets default properties on an object that aren't already specified. + * @param {Object} def Default properties + * @param {Object} given Object to assign defaults to + * @returns {Object} + * @private + */ + static mergeDefault(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] = this.mergeDefault(def[key], given[key]); + } + } + + return given; + } + + /** + * Converts an ArrayBuffer or string to a Buffer. + * @param {ArrayBuffer|string} ab ArrayBuffer to convert + * @returns {Buffer} + * @private + */ + static convertToBuffer(ab) { + if (typeof ab === 'string') ab = this.str2ab(ab); + return Buffer.from(ab); + } + + /** + * Converts a string to an ArrayBuffer. + * @param {string} str String to convert + * @returns {ArrayBuffer} + * @private + */ + static 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; + } + + /** + * Makes an Error from a plain info object. + * @param {Object} obj Error info + * @param {string} obj.name Error type + * @param {string} obj.message Message for the error + * @param {string} obj.stack Stack for the error + * @returns {Error} + * @private + */ + static makeError(obj) { + const err = new Error(obj.message); + err.name = obj.name; + err.stack = obj.stack; + return err; + } + + /** + * Makes a plain error info object from an Error. + * @param {Error} err Error to get info from + * @returns {Object} + * @private + */ + static makePlainError(err) { + const obj = {}; + obj.name = err.name; + obj.message = err.message; + obj.stack = err.stack; + return obj; + } + + /** + * Moves an element in an array *in place*. + * @param {Array<*>} array Array to modify + * @param {*} element Element to move + * @param {number} newIndex Index or offset to move the element to + * @param {boolean} [offset=false] Move the element by an offset amount rather than to a set index + * @returns {number} + * @private + */ + static moveElementInArray(array, element, newIndex, offset = false) { + const index = array.indexOf(element); + newIndex = (offset ? index : 0) + newIndex; + if (newIndex > -1 && newIndex < array.length) { + const removedElement = array.splice(index, 1)[0]; + array.splice(newIndex, 0, removedElement); + } + return array.indexOf(element); + } + + /** + * Creates a Promise that resolves after a specified duration. + * @param {number} ms How long to wait before resolving (in milliseconds) + * @returns {Promise<void>} + * @private + */ + static delayFor(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); + } +} + +module.exports = Util; diff --git a/node_modules/discord.js/typings/discord.js-test.ts b/node_modules/discord.js/typings/discord.js-test.ts new file mode 100644 index 0000000..afa9573 --- /dev/null +++ b/node_modules/discord.js/typings/discord.js-test.ts @@ -0,0 +1,69 @@ +/// <reference path='index.d.ts' /> + +import { Collector, Message, CollectorFilter, Client, CollectorHandler, MessageReaction, Collection, User, ReactionCollectorOptions, Snowflake } from 'discord.js'; + +const client = new Client({ + disableEveryone: false, + disabledEvents: ['GUILD_MEMBER_ADD'] +}); + +client.on('message', (message) => { + if (message.content === 'hello') { + message.channel.sendMessage('o/'); + } + + const collector: ReactionCollector = new ReactionCollector(message, + (reaction: MessageReaction) => reaction.emoji.toString() === '👌', + { time: 30e3 }); + collector.on('end', collected => console.log(collected)); +}); + +client.login('dsfsd754.4fds4f68d4f6sd46f4s.7878easfdsgdfFDSIJIO'); + +export class TestCollector extends Collector<Snowflake, Message> { + public filter: CollectorFilter; + public constructor(client: Client, filter: CollectorFilter) { + super(client, filter); + } + + public handle(message: Message): CollectorHandler<Snowflake, Message> { + return { key: message.id, value: message }; + } + + public cleanup(): void {} + public postCheck(): null { return null; } +} + +class ReactionCollector extends Collector<Snowflake, MessageReaction> { + public message: Message; + public users: Collection<Snowflake, User>; + public total: number; + public options: ReactionCollectorOptions; + public constructor(message: Message, filter: CollectorFilter, options?: ReactionCollectorOptions) { + super(message.client, filter, options || {}); + this.message = message; + this.users = new Collection<Snowflake, User>(); + this.total = 0; + this.client.on('messageReactionAdd', this.listener); + } + + public handle(reaction: MessageReaction): CollectorHandler<Snowflake, MessageReaction> { + if (reaction.message.id !== this.message.id) { return null; } + return { + key: reaction.emoji.id || reaction.emoji.name, + value: reaction + }; + } + + public postCheck(reaction: MessageReaction, user: User): string { + this.users.set(user.id, user); + if (this.options.max && ++this.total >= this.options.max) { return 'limit'; } + if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) { return 'emojiLimit'; } + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) { return 'userLimit'; } + return null; + } + + public cleanup(): void { + this.client.removeListener('messageReactionAdd', this.listener as any); + } +} diff --git a/node_modules/discord.js/typings/index.d.ts b/node_modules/discord.js/typings/index.d.ts new file mode 100644 index 0000000..f316f19 --- /dev/null +++ b/node_modules/discord.js/typings/index.d.ts @@ -0,0 +1,2440 @@ +// Type definitions for discord.js 11.6.2 +// Project: https://github.com/discordjs/discord.js +// Definitions by: +// acdenisSK <[email protected]> (https://github.com/acdenisSK) +// Zack Campbell <[email protected]> (https://github.com/zajrik) +// License: MIT + +declare module 'discord.js' { + import { EventEmitter } from 'events'; + import { Stream, Readable as ReadableStream } from 'stream'; + import { ChildProcess } from 'child_process'; + + export const version: string; + +//#region Classes + + class Attachment { + constructor(file: BufferResolvable | Stream, name?: string); + private _attach(file: BufferResolvable | Stream, name: string): void; + + public readonly attachment: BufferResolvable | Stream; + public readonly name: string; + public setAttachment(file: BufferResolvable | Stream, name: string): this; + public setFile(attachment: BufferResolvable | Stream): this; + public setName(name: string): this; + } + + class AudioPlayer extends EventEmitter { + constructor(voiceConnection: VoiceConnection); + public readonly dispatcher: StreamDispatcher; + public opusEncoder: object; + public prism: object; + public readonly transcoder: object; + public voiceConnection: VoiceConnection; + public setBitrate(value: number | 'auto'): void; + } + + class BaseOpus { + constructor(options?: { bitrate?: number, fec?: boolean, plp?: number }); + public bitrate: number; + public options: object; + } + + export class BitField<S extends string> { + constructor(bits?: BitFieldResolvable<S>); + public bitfield: number; + public add(...bits: BitFieldResolvable<S>[]): BitField<S>; + public any(bit: BitFieldResolvable<S>): boolean; + public equals(bit: BitFieldResolvable<S>): boolean; + public freeze(): Readonly<BitField<S>>; + public has(bit: BitFieldResolvable<S>): boolean; + public missing(bits: BitFieldResolvable<S>, ...hasParam: readonly unknown[]): S[]; + public remove(...bits: BitFieldResolvable<S>[]): BitField<S>; + public serialize(...hasParam: readonly unknown[]): Record<S, boolean>; + public toArray(...hasParam: readonly unknown[]): S[]; + public toJSON(): number; + public valueOf(): number; + public [Symbol.iterator](): IterableIterator<S>; + public static FLAGS: object; + public static resolve(bit?: BitFieldResolvable<any>): number; + } + + export class CategoryChannel extends GuildChannel { + public readonly children: Collection<Snowflake, GuildChannel>; + } + + export class Channel { + constructor(client: Client, data: object); + public readonly client: Client; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public deleted: boolean; + public id: Snowflake; + public type: 'dm' | 'group' | GuildChannelType; + public delete(): Promise<Channel>; + } + + export class Client extends EventEmitter { + constructor(options?: ClientOptions); + private _intervals: Set<NodeJS.Timer>; + private _pingTimestamp: number; + private _timeouts: Set<NodeJS.Timer>; + private actions: object; + private dataManager: object; + private manager: ClientManager; + private resolver: ClientDataResolver; + private rest: object; + private voice: ClientVoiceManager; + private ws: object; + private _eval(script: string): any; + private _pong(startTime: number): void; + private _setPresence(id: Snowflake, presence: object): void; + private _validateOptions(options?: ClientOptions): void; + + public broadcasts: VoiceBroadcast[]; + public readonly browser: boolean; + public channels: Collection<Snowflake, Channel>; + public readonly emojis: Collection<Snowflake, Emoji>; + public guilds: Collection<Snowflake, Guild>; + public options: ClientOptions; + public readonly ping: number; + public pings: number[]; + public presences: Collection<Snowflake, Presence>; + public readyAt: Date; + public readonly readyTimestamp: number; + public shard: ShardClientUtil; + public readonly status: number; + public token: string; + public readonly uptime: number; + public user: ClientUser; + public users: Collection<Snowflake, User>; + public readonly voiceConnections: Collection<Snowflake, VoiceConnection>; + public clearInterval(interval: NodeJS.Timer): void; + public clearTimeout(timeout: NodeJS.Timer): void; + public createVoiceBroadcast(): VoiceBroadcast; + public destroy(): Promise<void>; + public fetchApplication(id?: Snowflake): Promise<OAuth2Application>; + public fetchInvite(invite: InviteResolvable): Promise<Invite>; + public fetchUser(id: Snowflake, cache?: boolean): Promise<User>; + public fetchVoiceRegions(): Promise<Collection<string, VoiceRegion>>; + public fetchWebhook(id: Snowflake, token?: string): Promise<Webhook>; + public generateInvite(permissions?: PermissionResolvable): Promise<string>; + public login(token?: string): Promise<string>; + public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public sweepMessages(lifetime?: number): number; + public syncGuilds(guilds?: Guild[] | Collection<Snowflake, Guild>): void; + + public on(event: 'channelCreate', listener: (channel: Channel) => void): this; + public on(event: 'channelDelete', listener: (channel: Channel) => void): this; + public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; + public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public on(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; + public on(event: 'clientUserSettingsUpdate', listener: (clientUserSettings: ClientUserSettings) => void): this; + public on(event: 'debug', listener: (info: string) => void): this; + public on(event: 'disconnect', listener: (event: any) => void): this; + public on(event: 'emojiCreate', listener: (emoji: Emoji) => void): this; + public on(event: 'emojiDelete', listener: (emoji: Emoji) => void): this; + public on(event: 'emojiUpdate', listener: (oldEmoji: Emoji, newEmoji: Emoji) => void): this; + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'guildBanAdd', listener: (guild: Guild, user: User) => void): this; + public on(event: 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public on(event: 'guildCreate', listener: (guild: Guild) => void): this; + public on(event: 'guildDelete', listener: (guild: Guild) => void): this; + public on(event: 'guildMemberAdd', listener: (member: GuildMember) => void): this; + public on(event: 'guildMemberAvailable', listener: (member: GuildMember) => void): this; + public on(event: 'guildMemberRemove', listener: (member: GuildMember) => void): this; + public on(event: 'guildMembersChunk', listener: (members: GuildMember[], guild: Guild) => void): this; + public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildUnavailable', listener: (guild: Guild) => void): this; + public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; + public on(event: 'inviteCreate' | 'inviteDelete', listener: (invite: Invite) => void): this; + public on(event: 'message', listener: (message: Message) => void): this; + public on(event: 'messageDelete', listener: (message: Message) => void): this; + public on(event: 'messageDeleteBulk', listener: (messages: Collection<Snowflake, Message>) => void): this; + public on(event: 'messageReactionAdd', listener: (messageReaction: MessageReaction, user: User) => void): this; + public on(event: 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; + public on(event: 'messageReactionRemoveEmoji', listener: (messageReaction: MessageReaction) => void): this; + public on(event: 'messageReactionRemoveAll', listener: (message: Message) => void): this; + public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public on(event: 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'rateLimit', listener: (rateLimit: RateLimitInfo) => void): this; + public on(event: 'ready', listener: () => void): this; + public on(event: 'reconnecting', listener: () => void): this; + public on(event: 'resume', listener: (replayed: number) => void): this; + public on(event: 'roleCreate', listener: (role: Role) => void): this; + public on(event: 'roleDelete', listener: (role: Role) => void): this; + public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public on(event: 'typingStart', listener: (channel: Channel, user: User) => void): this; + public on(event: 'typingStop', listener: (channel: Channel, user: User) => void): this; + public on(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; + public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public on(event: 'voiceStateUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'warn', listener: (info: string) => void): this; + public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'channelCreate', listener: (channel: Channel) => void): this; + public once(event: 'channelDelete', listener: (channel: Channel) => void): this; + public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; + public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public once(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; + public once(event: 'clientUserSettingsUpdate', listener: (clientUserSettings: ClientUserSettings) => void): this; + public once(event: 'debug', listener: (info: string) => void): this; + public once(event: 'disconnect', listener: (event: any) => void): this; + public once(event: 'emojiCreate', listener: (emoji: Emoji) => void): this; + public once(event: 'emojiDelete', listener: (emoji: Emoji) => void): this; + public once(event: 'emojiUpdate', listener: (oldEmoji: Emoji, newEmoji: Emoji) => void): this; + public once(event: 'error', listener: (error: Error) => void): this; + public once(event: 'guildBanAdd', listener: (guild: Guild, user: User) => void): this; + public once(event: 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public once(event: 'guildCreate', listener: (guild: Guild) => void): this; + public once(event: 'guildDelete', listener: (guild: Guild) => void): this; + public once(event: 'guildMemberAdd', listener: (member: GuildMember) => void): this; + public once(event: 'guildMemberAvailable', listener: (member: GuildMember) => void): this; + public once(event: 'guildMemberRemove', listener: (member: GuildMember) => void): this; + public once(event: 'guildMembersChunk', listener: (members: GuildMember[], guild: Guild) => void): this; + public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildUnavailable', listener: (guild: Guild) => void): this; + public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; + public once(event: 'message', listener: (message: Message) => void): this; + public once(event: 'messageDelete', listener: (message: Message) => void): this; + public once(event: 'messageDeleteBulk', listener: (messages: Collection<Snowflake, Message>) => void): this; + public once(event: 'messageReactionAdd', listener: (messageReaction: MessageReaction, user: User) => void): this; + public once(event: 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; + public once(event: 'messageReactionRemoveEmoji', listener: (messageReaction: MessageReaction) => void): this; + public once(event: 'messageReactionRemoveAll', listener: (message: Message) => void): this; + public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public once(event: 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'rateLimit', listener: (rateLimit: RateLimitInfo) => void): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'reconnecting', listener: () => void): this; + public once(event: 'resume', listener: (replayed: number) => void): this; + public once(event: 'roleCreate', listener: (role: Role) => void): this; + public once(event: 'roleDelete', listener: (role: Role) => void): this; + public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public once(event: 'typingStart', listener: (channel: Channel, user: User) => void): this; + public once(event: 'typingStop', listener: (channel: Channel, user: User) => void): this; + public once(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; + public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'warn', listener: (info: string) => void): this; + public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; + public once(event: string, listener: Function): this; + } + + class ClientDataResolver { + constructor(client: Client); + public resolveBase64(data: Base64Resolvable): string; + public resolveChannel(channel: ChannelResolvable): Channel; + public resolveChannelID(channel: ChannelResolvable): Snowflake; + public resolveColor(color: ColorResolvable): number; + public resolveString(data: StringResolvable): string; + public resolveEmojiIdentifier(emoji: EmojiIdentifierResolvable): string; + public resolveFile(resource: BufferResolvable | Stream): Promise<Buffer>; + public resolveGuild(guild: GuildResolvable): Guild; + public resolveGuildMember(guild: GuildResolvable, user: UserResolvable): GuildMember; + public resolveImage(imge: BufferResolvable | Base64Resolvable): Promise<string>; + public resolveInviteCode(data: InviteResolvable): string; + public resolveString(data: StringResolvable): string; + public resolveUser(user: UserResolvable): User; + public resolveUserID(user: UserResolvable): Snowflake; + + public static resolveColor(color: ColorResolvable): number; + } + + class ClientManager { + constructor(client: Client); + public client: Client; + public heartbeatInterval: number; + public status: number; + public connectToWebSocket(token: string, resolve: Function, reject: Function): void; + } + + export class ClientUser extends User { + public blocked: Collection<Snowflake, User>; + public email: string; + public friends: Collection<Snowflake, User>; + public guildSettings: Collection<Snowflake, ClientUserGuildSettings>; + public mfaEnabled: boolean; + public mobile: boolean; + public notes: Collection<Snowflake, string>; + public premium: boolean; + public settings: ClientUserSettings; + public verified: boolean; + public acceptInvite(invite: Invite | string): Promise<Guild>; + public addFriend(user?: UserResolvable): Promise<User>; + public createGroupDM(recipients: GroupDMRecipientOptions[]): Promise<GroupDMChannel>; + public createGuild(name: string, region: string, icon?: BufferResolvable | Base64Resolvable): Promise<Guild>; + public fetchMentions(options?: { limit?: number; roles?: boolean, everyone?: boolean; guild?: Guild | Snowflake }): Promise<Message[]>; + public removeFriend(user?: UserResolvable): Promise<User>; + public setActivity(name: string | null, options?: { url?: string, type?: ActivityType | number }): Promise<Presence>; + public setAFK(afk: boolean): Promise<ClientUser>; + public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise<ClientUser>; + public setEmail(email: string, password: string): Promise<ClientUser>; + public setGame(game: string | null, streamingURL?: string): Promise<ClientUser>; + public setPassword(newPassword: string, oldPassword: string): Promise<ClientUser>; + public setPresence(data: PresenceData): Promise<ClientUser>; + public setStatus(status: PresenceStatus): Promise<ClientUser>; + public setUsername(username: string, password?: string): Promise<ClientUser>; + } + + class ClientUserChannelOverride { + constructor(user: User, data: object); + private patch(data: object): void; + + public messageNotifications: GuildChannelMessageNotifications; + public muted: boolean; + } + + class ClientUserGuildSettings { + constructor(data: object, guild: Guild); + private patch(data: object): void; + + public channelOverrides: Collection<Snowflake, ClientUserChannelOverride>; + public readonly client: Client; + public guildID: Snowflake; + public messageNotifications: GuildChannelMessageNotifications; + public mobilePush: boolean; + public muted: boolean; + public suppressEveryone: boolean; + public update(name: string, value: any): Promise<object>; + } + + export class ClientUserSettings { + constructor(user: User, data: object); + private patch(data: object): void; + + public convertEmoticons: boolean; + public defaultGuildsRestricted: boolean; + public detectPlatformAccounts: boolean; + public developerMode: boolean; + public enableTTSCommand: boolean; + public explicitContentFilter: 'DISABLED' | 'NON_FRIENDS' | 'FRIENDS_AND_NON_FRIENDS' | string; + public friendsSources: { all: boolean, mutualGuilds: boolean, mutualFriends: boolean }; + public guildsPositions: Snowflake[]; + public inlineAttachmentMedia: boolean; + public inlineEmbedMedia: boolean; + public locale: string; + public messageDisplayCompact: boolean; + public renderReactions: boolean; + public restrictedGuilds: Snowflake[]; + public showCurrentGame: boolean; + public status: PresenceStatus; + public theme: string; + public addRestrictedGuild(guild: Guild): Promise<Guild>; + public removeRestrictedGuild(guild: Guild): Promise<Guild>; + public setGuildPosition(guild: Guild, position: number, relative?: boolean): Promise<Guild>; + public update(name: string, value: any): Promise<object>; + } + + class ClientVoiceManager { + constructor(client: Client); + public client: Client; + public connections: Collection<Snowflake, VoiceConnection>; + public joinChannel(channel: VoiceChannel): Promise<VoiceConnection>; + } + + export class Collection<K, V> extends Map<K, V> { + private _array: V[]; + private _keyArray: K[]; + + public array(): V[]; + public clone(): Collection<K, V>; + public concat(...collections: Collection<K, V>[]): Collection<K, V>; + public deleteAll(): Promise<V>[]; + public equals(collection: Collection<any, any>): boolean; + public every(fn: (value: V, key: K, collection: Collection<K, V>) => boolean, thisArg?: any): boolean; + public exists(prop: keyof V, value: any): boolean; + public filter(fn: (value: V, key: K, collection: Collection<K, V>) => boolean, thisArg?: any): Collection<K, V>; + public filterArray(fn: (value: V, key: K, collection: Collection<K, V>) => boolean, thisArg?: any): V[]; + public find(prop: keyof V, value: any): V; + public find(fn: (value: V, key: K, collection: Collection<K, V>) => boolean): V; + public findAll(prop: keyof V, value: any): V[]; + public findKey(prop: keyof V, value: any): K; + public findKey(fn: (value: V, key: K, collection: Collection<K, V>) => boolean): K; + public first(): V; + public first(count: number): V[]; + public firstKey(): K; + public firstKey(count: number): K[]; + public keyArray(): K[]; + public last(): V; + public last(count: number): V[]; + public lastKey(): K; + public lastKey(count: number): K[]; + public map<T>(fn: (value: V, key: K, collection: Collection<K, V>) => T, thisArg?: any): T[]; + public partition(fn: (value: V, key: K, collection: Collection<K, V>) => boolean, thisArg?: any): [Collection<K, V>, Collection<K, V>]; + public random(): V; + public random(count: number): V[]; + public randomKey(): K; + public randomKey(count: number): K[]; + public reduce<T>(fn: (accumulator: any, value: V, key: K, collection: Collection<K, V>) => T, initialValue?: any): T; + public some(fn: (value: V, key: K, collection: Collection<K, V>) => boolean, thisArg?: any): boolean; + public sort(compareFunction?: (a: V, b: V, c?: K, d?: K) => number): Collection<K, V>; + public sweep(fn: (value: V, key: K, collection: Collection<K, V>) => boolean, thisArg?: any): number; + public tap(fn: (value: V, key: K, map: Collection<K, V>) => void, thisArg?: any): Collection<K, V>; + } + + abstract class Collector<K, V> extends EventEmitter { + constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); + private _timeout: NodeJS.Timer | null; + private _idletimeout: NodeJS.Timer | null; + private _handle(...args: any[]): void; + + public readonly client: Client; + public collected: Collection<K, V>; + public ended: boolean; + public filter: CollectorFilter; + public readonly next: Promise<V>; + public options: CollectorOptions; + public stop(reason?: string): void; + + protected listener: Function; + public abstract cleanup(): void; + public abstract handle(...args: any[]): CollectorHandler<K, V>; + public abstract postCheck(...args: any[]): string | null; + + public on(event: 'collect', listener: (element: V, collector: Collector<K, V>) => void): this; + public on(event: 'end', listener: (collected: Collection<K, V>, reason: string) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'collect', listener: (element: V, collector: Collector<K, V>) => void): this; + public once(event: 'end', listener: (collected: Collection<K, V>, reason: string) => void): this; + public once(event: string, listener: Function): this; + } + + class DiscordAPIError extends Error { + constructor(error: object); + private static flattenErrors(obj: object, key: string): string[]; + + public code: number; + public method: string; + public path: string; + } + + export class DMChannel extends TextBasedChannel(Channel) { + constructor(client: Client, data: object); + public lastMessageID: Snowflake; + public messages: Collection<Snowflake, Message>; + public recipient: User; + public toString(): string; + } + + export class Emoji { + constructor(guild: Guild, data: object); + public animated: boolean; + public available: boolean; + public readonly client: Client; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public readonly deletable: boolean; + public deleted: boolean; + public guild: Guild; + public id: Snowflake; + public readonly identifier: string; + public managed: boolean; + public name: string; + public requiresColons: boolean; + public readonly roles: Collection<Snowflake, Role>; + public readonly url: string; + public addRestrictedRole(role: Role): Promise<Emoji>; + public addRestrictedRoles(roles: Role[]): Promise<Emoji>; + public edit(data: EmojiEditData, reason?: string): Promise<Emoji>; + public equals(other: Emoji | object): boolean; + public delete(reason?: string): Promise<this>; + public fetchAuthor(): Promise<User>; + public removeRestrictedRole(role: Role): Promise<Emoji>; + public removeRestrictedRoles(roles: Role[]): Promise<Emoji>; + public setName(name: string, reason?: string): Promise<Emoji>; + public toString(): string; + } + + export class Game { + constructor(data: object, presence: Presence); + private _flags: string[]; + private syncID: string; + + public applicationID: string; + public assets: RichPresenceAssets; + public details: string; + public emoji: Omit<ReactionEmoji, 'reaction'> | null; + public name: string; + public readonly streaming: boolean; + public party: { + id: string; + size: [number, number]; + }; + public state: string; + public timestamps: { + start: Date; + end: Date; + }; + public readonly flags: string[]; + public type: number; + public url: string; + public equals(game: Game): boolean; + public toString(): string; + } + + export class GroupDMChannel extends TextBasedChannel(Channel) { + constructor(client: Client, data: object); + public applicationID: string; + public icon: string; + public lastMessageID: string; + public managed: boolean; + public messages: Collection<Snowflake, Message>; + public name: string; + public nicks: Collection<Snowflake, string>; + public readonly owner: User; + public ownerID: string; + public recipients: Collection<Snowflake, User>; + public addUser(accessTokenOrID: UserResolvable | string, nick?: string): Promise<GroupDMChannel>; + public equals(channel: GroupDMChannel): boolean; + public setIcon(icon: Base64Resolvable | BufferResolvable): Promise<GroupDMChannel>; + public toString(): string; + } + + export class Guild { + constructor(client: Client, data: object); + private readonly _sortedRoles: Collection<Snowflake, Role>; + private _sortedChannels(type: string): Collection<Snowflake, GuildChannel>; + private _sortPositionWithID(collection: Collection<any, any>): Collection<any, any>; + + protected setup(data: any): void; + + public readonly afkChannel: VoiceChannel; + public afkChannelID: string; + public afkTimeout: number; + public applicationID: string; + public available: boolean; + public banner: string | null; + public readonly bannerURL: string | null; + public deleted: boolean; + public description: string | null; + public channels: Collection<Snowflake, GuildChannel>; + public defaultMessageNotifications: DefaultMessageNotifications | number; + public readonly client: Client; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public readonly defaultChannel: TextChannel; + public readonly defaultRole: Role; + public readonly embedChannel: TextChannel | null; + public embedChannelID: Snowflake | null; + public embedEnabled: boolean; + public emojis: Collection<Snowflake, Emoji>; + public explicitContentFilter: number; + public features: string[]; + public icon: string; + public readonly iconURL: string; + public id: Snowflake; + public readonly joinedAt: Date; + public joinedTimestamp: number; + public large: boolean; + public maximumMembers?: number; + public maximumPresences?: number; + public readonly me: GuildMember; + public memberCount: number; + public members: Collection<Snowflake, GuildMember>; + public readonly messageNotifications: MessageNotifications; + public readonly mobilePush: boolean; + public readonly muted: boolean; + public name: string; + public readonly nameAcronym: string; + public readonly owner: GuildMember; + public ownerID: string; + public premiumSubscriptionCount: number | null; + public premiumTier: PremiumTier; + public readonly position: number; + public presences: Collection<Snowflake, Presence>; + public readonly publicUpdatesChannel: TextChannel | null; + public publicUpdatesChannelID: Snowflake | null; + public region: string; + public roles: Collection<Snowflake, Role>; + public readonly rulesChannel: TextChannel | null; + public rulesChannelID: Snowflake | null; + public splash: string; + public readonly splashURL: string; + public readonly suppressEveryone: boolean; + public readonly systemChannel: GuildChannel; + public systemChannelFlags: Readonly<SystemChannelFlags>; + public systemChannelID: Snowflake; + public vanityURLCode: string; + public readonly verified: boolean; + public verificationLevel: number; + public readonly voiceConnection: VoiceConnection; + public readonly widgetChannel: TextChannel | null; + public widgetChannelID?: Snowflake; + public widgetEnabled?: boolean; + public acknowledge(): Promise<Guild>; + public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise<GuildMember>; + public allowDMs(allow: boolean): Promise<Guild>; + public ban(user: UserResolvable, options?: BanOptions | number | string): Promise<GuildMember | User | string>; + public createChannel(name: string, options?: ChannelData): Promise<CategoryChannel | TextChannel | VoiceChannel>; + public createChannel(name: string, type?: GuildChannelType, permissionOverwrites?: PermissionOverwrites[] | ChannelCreationOverwrites[], reason?: string): Promise<CategoryChannel | TextChannel | VoiceChannel>; + public createEmoji(attachment: BufferResolvable | Base64Resolvable, name: string, roles?: Collection<Snowflake, Role> | Role[], reason?: string): Promise<Emoji>; + public createIntegration(data: IntegrationData, reason?: string): Promise<Guild>; + public createRole(data?: RoleData, reason?: string): Promise<Role>; + public delete(): Promise<Guild>; + public deleteEmoji(emoji: Emoji | string, reason?: string): Promise<void>; + public edit(data: GuildEditData, reason?: string): Promise<Guild>; + public equals(guild: Guild): boolean; + public fetch(): Promise<Guild>; + public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise<GuildAuditLogs>; + public fetchBan(user: UserResolvable): Promise<BanInfo>; + public fetchBans(withReasons?: false): Promise<Collection<Snowflake, User>>; + public fetchBans(withReasons: true): Promise<Collection<Snowflake, BanInfo>>; + public fetchBans(withReasons: boolean): Promise<Collection<Snowflake, BanInfo | User>>; + public fetchEmbed(): Promise<GuildEmbedData>; + public fetchIntegrations(): Promise<Collection<string, Integration>>; + public fetchInvites(): Promise<Collection<Snowflake, Invite>>; + public fetchMember(user: UserResolvable, cache?: boolean): Promise<GuildMember>; + public fetchMembers(query?: string, limit?: number): Promise<Guild>; + public fetchVanityCode(): Promise<string>; + public fetchVoiceRegions(): Promise<Collection<string, VoiceRegion>>; + public fetchWebhooks(): Promise<Collection<Snowflake, Webhook>>; + public leave(): Promise<Guild>; + public member(user: UserResolvable): GuildMember; + public pruneMembers(days: number, dry?: boolean, reason?: string): Promise<number>; + public search(options?: MessageSearchOptions): Promise<MessageSearchResult>; + public setAFKChannel(afkChannel: ChannelResolvable, reason?: string): Promise<Guild>; + public setAFKTimeout(afkTimeout: number, reason?: string): Promise<Guild>; + public setBanner(banner: Base64Resolvable, reason?: string): Promise<Guild>; + public setChannelPosition(channel: string | GuildChannel, position: number, relative?: boolean): Promise<Guild>; + public setChannelPositions(channelPositions: ChannelPosition[]): Promise<Guild>; + public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications, reason?: string): Promise<Guild>; + public setEmbed(embed: GuildEmbedData, reason?: string): Promise<Guild>; + public setExplicitContentFilter(explicitContentFilter: number, reason?: string): Promise<Guild>; + public setIcon(icon: Base64Resolvable, reason?: string): Promise<Guild>; + public setName(name: string, reason?: string): Promise<Guild>; + public setOwner(owner: GuildMemberResolvable, reason?: string): Promise<Guild>; + public setPosition(position: number, relative?: boolean): Promise<Guild>; + public setRegion(region: string, reason?: string): Promise<Guild>; + public setRolePosition(role: string | Role, position: number, relative?: boolean): Promise<Guild>; + public setRolePositions(rolePositions: RolePosition[]): Promise<Guild>; + public setSplash(splash: Base64Resolvable, reason?: string): Promise<Guild>; + public setSystemChannel(systemChannel: ChannelResolvable, reason?: string): Promise<Guild>; + public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise<Guild>; + public setVerificationLevel(verificationLevel: number, reason?: string): Promise<Guild>; + public sync(): void; + public toString(): string; + public unban(user: UserResolvable, reason?: string): Promise<User>; + } + + export class GuildAuditLogs { + constructor(guild: Guild, data: object); + private webhooks: Collection<Snowflake, Webhook>; + private integrations: Collection<Snowflake, Integration>; + + public entries: Collection<Snowflake, GuildAuditLogsEntry>; + + public static Actions: GuildAuditLogsActions; + public static Targets: GuildAuditLogsTargets; + public static Entry: typeof GuildAuditLogsEntry; + public static actionType(action: number): GuildAuditLogsActionType; + public static build(...args: any[]): Promise<GuildAuditLogs>; + public static targetType(target: number): GuildAuditLogsTarget; + } + + class GuildAuditLogsEntry { + constructor(logs: GuildAuditLogs, guild: Guild, data: object); + public action: GuildAuditLogsAction; + public actionType: GuildAuditLogsActionType; + public changes: AuditLogChange[]; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public executor: User; + public extra: object | Role | GuildMember; + public id: Snowflake; + public reason: string; + public target: Guild | User | Role | Emoji | Invite | Webhook | Integration | null; + public targetType: GuildAuditLogsTarget; + } + + export class GuildChannel extends Channel { + constructor(guild: Guild, data: object); + public readonly calculatedPosition: number; + public readonly deletable: boolean; + public guild: Guild; + public readonly manageable: boolean; + public readonly messageNotifications: GuildChannelMessageNotifications; + public readonly muted: boolean; + public name: string; + public readonly parent: CategoryChannel | null; + public parentID: Snowflake | null; + public permissionOverwrites: Collection<Snowflake, PermissionOverwrites>; + public position: number; + public readonly permissionsLocked: boolean | null; + public clone(options: GuildChannelCloneOptions): Promise<GuildChannel>; + public clone(name?: string, withPermissions?: boolean, withTopic?: boolean, reason?: string): Promise<GuildChannel>; + public createInvite(options?: InviteOptions, reason?: string): Promise<Invite>; + public delete(reason?: string): Promise<GuildChannel>; + public edit(data: ChannelData, reason?: string): Promise<GuildChannel>; + public equals(channel: GuildChannel): boolean; + public fetchInvites(): Promise<Collection<string, Invite>>; + public lockPermissions(): Promise<GuildChannel>; + public memberPermissions(member: GuildMemberResolvable): Permissions | null; + public overwritePermissions(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOptions, reason?: string): Promise<void>; + public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Permissions | null; + public replacePermissionOverwrites(options?: { overwrites?: (PermissionOverwrites | ChannelCreationOverwrites)[] | Collection<Snowflake, ChannelCreationOverwrites>, reason?: string }): Promise<GuildChannel>; + public rolePermissions(role: RoleResolvable): Permissions; + public setName(name: string, reason?: string): Promise<GuildChannel>; + public setParent(parent: ChannelResolvable, reason?: string): Promise<GuildChannel>; + public setPosition(position: number, relative?: boolean): Promise<GuildChannel>; + public setTopic(topic: string, reason?: string): Promise<GuildChannel>; + public toString(): string; + } + + export class GuildMember extends PartialTextBasedChannel() { + constructor(guild: Guild, data: object); + public readonly bannable: boolean; + public readonly client: Client; + public readonly colorRole: Role; + public readonly deaf: boolean; + public deleted: boolean; + public readonly displayColor: number; + public readonly displayHexColor: string; + public readonly displayName: string; + public guild: Guild; + public readonly highestRole: Role; + public readonly hoistRole: Role; + public readonly id: Snowflake; + public readonly joinedAt: Date; + public joinedTimestamp: number; + public readonly kickable: boolean; + public lastMessageID: string; + public readonly mute: boolean; + public nickname: string | null; + public readonly manageable: boolean; + public readonly permissions: Permissions; + public readonly premiumSince: Date | null; + public premiumSinceTimestamp: number | null; + public readonly presence: Presence; + public readonly roles: Collection<Snowflake, Role>; + public selfDeaf: boolean; + public selfMute: boolean; + public selfStream: boolean; + public serverDeaf: boolean; + public serverMute: boolean; + public speaking: boolean; + public user: User; + public readonly voiceChannel: VoiceChannel; + public voiceChannelID: string; + public voiceSessionID: string; + public addRole(role: Role | Snowflake, reason?: string): Promise<GuildMember>; + public addRoles(roles: Collection<Snowflake, Role> | Role[] | Snowflake[], reason?: string): Promise<GuildMember>; + public ban(options?: BanOptions | number | string): Promise<GuildMember>; + public createDM(): Promise<DMChannel>; + public deleteDM(): Promise<DMChannel>; + public edit(data: GuildMemberEditData, reason?: string): Promise<GuildMember>; + public hasPermission(permission: PermissionResolvable, explicit?: boolean, checkAdmin?: boolean, checkOwner?: boolean): boolean; + public hasPermissions(permission: PermissionResolvable, explicit?: boolean): boolean; + public kick(reason?: string): Promise<GuildMember>; + public missingPermissions(permissions: PermissionResolvable, explicit?: boolean): PermissionResolvable; + public permissionsIn(channel: ChannelResolvable): Permissions; + public removeRole(role: Role | Snowflake, reason?: string): Promise<GuildMember>; + public removeRoles(roles: Collection<Snowflake, Role> | Role[] | Snowflake[], reason?: string): Promise<GuildMember>; + public setDeaf(deaf: boolean, reason?: string): Promise<GuildMember>; + public setMute(mute: boolean, reason?: string): Promise<GuildMember>; + public setNickname(nickname: string, reason?: string): Promise<GuildMember>; + public setRoles(roles: Collection<Snowflake, Role> | Role[] | Snowflake[], reason?: string): Promise<GuildMember>; + public setVoiceChannel(voiceChannel: ChannelResolvable | null): Promise<GuildMember>; + public toString(): string; + } + + export class Integration { + constructor(client: Client, data: object, guild: Guild); + public account: IntegrationAccount; + public enabled: boolean; + public expireBehavior: number; + public expireGracePeriod: number; + public guild: Guild; + public id: Snowflake; + public name: string; + public role: Role; + public syncedAt: number; + public syncing: boolean; + public type: number; + public user: User; + public delete(reason?: string): Promise<Integration>; + public edit(data: IntegrationEditData, reason?: string): Promise<Integration>; + public sync(): Promise<Integration>; + } + + export class Invite { + constructor(client: Client, data: object); + public channel: GuildChannel | PartialGuildChannel; + public readonly client: Client; + public code: string; + public readonly createdAt: Date; + public createdTimestamp: number; + public readonly expiresAt: Date; + public readonly expiresTimestamp: number; + public guild: Guild | PartialGuild; + public inviter: User; + public maxAge: number; + public maxUses: number; + public memberCount: number; + public presenceCount: number; + public temporary: boolean; + public textChannelCount: number; + public readonly url: string; + public uses: number; + public voiceChannelCount: number; + public delete(reason?: string): Promise<Invite>; + public toString(): string; + } + + export class Message { + constructor(channel: TextChannel | DMChannel | GroupDMChannel, data: object, client: Client); + private _edits: Message[]; + private patch(data: object): void; + + public attachments: Collection<Snowflake, MessageAttachment>; + public author: User; + public channel: TextChannel | DMChannel | GroupDMChannel; + public readonly cleanContent: string; + public readonly client: Client; + public content: string; + public readonly createdAt: Date; + public createdTimestamp: number; + public readonly deletable: boolean; + public deleted: boolean; + public readonly editable: boolean; + public readonly editedAt: Date; + public editedTimestamp: number; + public readonly edits: Message[]; + public embeds: MessageEmbed[]; + public flags: Readonly<MessageFlags>; + public readonly guild: Guild; + public hit: boolean; + public id: Snowflake; + public member: GuildMember; + public mentions: MessageMentions; + public nonce: string; + public readonly pinnable: boolean; + public pinned: boolean; + public reactions: Collection<Snowflake, MessageReaction>; + public reference: MessageReference | null; + public system: boolean; + public tts: boolean; + public type: string; + public readonly url: string; + public webhookID: Snowflake; + public acknowledge(): Promise<Message>; + public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise<Collection<Snowflake, MessageReaction>>; + public clearReactions(): Promise<Message>; + public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; + public delete(timeout?: number): Promise<Message>; + public edit(content: StringResolvable, options?: MessageEditOptions | RichEmbed): Promise<Message>; + public editCode(lang: string, content: StringResolvable): Promise<Message>; + public equals(message: Message, rawData: object): boolean; + public fetchWebhook(): Promise<Webhook>; + public isMemberMentioned(member: GuildMember | User): boolean; + public isMentioned(data: GuildChannel | User | Role | Snowflake): boolean; + public pin(): Promise<Message>; + public react(emoji: string | Emoji | ReactionEmoji): Promise<MessageReaction>; + public reply(content?: StringResolvable, options?: MessageOptions & { split: false }): Promise<Message>; + public reply(content?: StringResolvable, options?: MessageOptions): Promise<Message | Message[]>; + public reply(options?: MessageOptions): Promise<Message | Message[]>; + public suppressEmbeds(suppress?: boolean): Promise<Message>; + public toString(): string; + public unpin(): Promise<Message>; + } + + export class MessageAttachment { + constructor(message: Message, data: object); + public readonly client: Client; + public filename: string; + public filesize: number; + public height: number; + public id: Snowflake; + public message: Message; + public proxyURL: string; + public readonly spoiler: boolean; + public url: string; + public width: number; + } + + export class MessageCollector extends Collector<Snowflake, Message> { + constructor(channel: TextChannel | DMChannel | GroupDMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); + public channel: Channel; + public options: MessageCollectorOptions; + public received: number; + + public cleanup(): void; + public handle(message: Message): CollectorHandler<Snowflake, Message>; + public postCheck(): string; + } + + export class MessageEmbed { + constructor(message: Message, data: object); + public author: MessageEmbedAuthor; + public readonly client: Client; + public color: number; + public readonly createdAt: Date; + public timestamp: number; + public description: string; + public fields: MessageEmbedField[]; + public footer: MessageEmbedFooter; + public readonly hexColor: string; + public image: MessageEmbedImage; + public message: Message; + public provider: MessageEmbedProvider; + public thumbnail: MessageEmbedThumbnail; + public title: string; + public type: string; + public url: string; + public video: MessageEmbedVideo; + } + + export class MessageEmbedAuthor { + constructor(embed: MessageEmbed, data: object); + public embed: MessageEmbed; + public iconURL: string; + public name: string; + public url: string; + } + + export class MessageEmbedField { + constructor(embed: MessageEmbed, data: object); + public embed: MessageEmbed; + public inline: boolean; + public name: string; + public value: string; + } + + export class MessageEmbedFooter { + constructor(embed: MessageEmbed, data: object); + public embed: MessageEmbed; + public iconURL: string; + public proxyIconURL: string; + public text: string; + } + + export class MessageEmbedImage { + constructor(embed: MessageEmbed, data: object); + public embed: MessageEmbed; + public height: number; + public proxyURL: string; + public url: string; + public width: number; + } + + export class MessageEmbedProvider { + constructor(embed: MessageEmbed, data: object); + public embed: MessageEmbed; + public name: string; + public url: string; + } + + export class MessageEmbedThumbnail { + constructor(embed: MessageEmbed, data: object); + public embed: MessageEmbed; + public height: number; + public proxyURL: string; + public url: string; + public width: number; + } + + export class MessageEmbedVideo { + constructor(embed: MessageEmbed, data: object); + public embed: MessageEmbed; + public height: number; + public url: string; + public width: number; + } + + export class MessageFlags extends BitField<MessageFlagsString> { + public static FLAGS: Record<MessageFlagsString, number>; + public static resolve(bit?: BitFieldResolvable<MessageFlagsString>): number; + } + + export class MessageMentions { + private _channels: Collection<Snowflake, GuildChannel>; + private _client: Client; + private _content: Message; + private _guild: Guild; + private _members: Collection<Snowflake, GuildMember>; + + public readonly channels: Collection<Snowflake, TextChannel>; + public crosspostedChannels: Collection<Snowflake, CrosspostedChannel>; + public everyone: boolean; + public readonly members: Collection<Snowflake, GuildMember>; + public roles: Collection<Snowflake, Role>; + public users: Collection<Snowflake, User>; + + public static CHANNELS_PATTERN: RegExp; + public static EVERYONE_PATTERN: RegExp; + public static ROLES_PATTERN: RegExp; + public static USERS_PATTERN: RegExp; + } + + export class MessageReaction { + constructor(message: Message, emoji: object, count: number, me: boolean); + public count: number; + public readonly emoji: Emoji | ReactionEmoji; + public me: boolean; + public message: Message; + public users: Collection<string, User>; + public fetchUsers(limit?: number, options?: { after?: number; before?: number }): Promise<Collection<Snowflake, User>>; + public remove(user?: UserResolvable): Promise<MessageReaction>; + public removeAll(): Promise<MessageReaction>; + } + + export class NewsChannel extends TextChannel { + constructor(guild: Guild, data: object); + public rateLimitPerUser: 0; + } + + export class OAuth2Application { + constructor(client: Client, data: object); + public bot: object; + public botPublic: boolean; + public botRequireCodeGrant: boolean; + public readonly client: Client; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public description: string; + public flags: number; + public icon: string; + public iconURL: string; + public id: Snowflake; + public name: string; + public owner: User; + public redirectURIs: string[]; + public rpcApplicationState: boolean; + public rpcOrigins: string[]; + public secret: string; + public team: Team | null; + public reset(): OAuth2Application; + public toString(): string; + } + + export class PartialGuild { + constructor(client: Client, data: object); + public readonly client: Client; + public icon: string; + public id: Snowflake; + public name: string; + public splash: string; + } + + export class PartialGuildChannel { + constructor(client: Client, data: object); + public readonly client: Client; + public id: Snowflake; + public name: string; + public type: string; + } + + export class PermissionOverwrites { + constructor(guildChannel: GuildChannel, data: object); + public allow: number; + public allowed: Permissions; + public channel: GuildChannel; + public denied: Permissions; + public deny: number; + public id: Snowflake; + public type: string; + public delete(reason?: string): Promise<PermissionOverwrites>; + } + + export class Permissions extends BitField<PermissionString> { + constructor(permissions: PermissionResolvable); + constructor(member: GuildMember, permissions: PermissionResolvable); + + public bitfield: number; + public member: GuildMember; + public readonly raw: number; + public any(permissions: PermissionResolvable, checkAdmin?: boolean): boolean; + public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; + public hasPermission(permission: PermissionResolvable, explicit?: boolean): boolean; + public hasPermissions(permissions: PermissionResolvable, explicit?: boolean): boolean; + public missing(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionString[]; + public missingPermissions(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionResolvable; + public serialize(checkAdmin?: boolean): Required<PermissionObject>; + public toArray(checkAdmin?: boolean): PermissionString[]; + public valueOf(): number; + + public static ALL: number; + public static DEFAULT: number; + public static FLAGS: PermissionFlags; + public static resolve(permission?: PermissionResolvable): number; + } + + export class Presence { + constructor(data: object, client: Client); + public activities: Game[]; + public readonly client: Client; + public game: Game; + public status: PresenceStatusData; + public clientStatus: ClientPresenceStatusData; + public equals(presence: Presence): boolean; + } + + export class ReactionCollector extends Collector<Snowflake, MessageReaction> { + constructor(message: Message, filter: CollectorFilter, options?: ReactionCollectorOptions); + public message: Message; + public options: ReactionCollectorOptions; + public total: number; + public users: Collection<Snowflake, User>; + + public cleanup(): void; + public handle(reaction: MessageReaction): CollectorHandler<Snowflake, MessageReaction>; + public postCheck(reaction: MessageReaction, user: User): string; + } + + export class ReactionEmoji { + constructor(reaction: MessageReaction, emoji: object); + public animated: boolean; + public readonly client: Client; + public readonly createdAt: number | null; + public readonly createdTimestamp: number | null; + public id: Snowflake; + public readonly identifier: string; + public name: string; + public reaction: MessageReaction; + public readonly url: string | null; + public toString(): string; + } + + class RequestHandler { + constructor(restManager: object); + public readonly globalLimit: boolean; + public queue: object[]; + public restManager: object; + public handle(): void; + public push(request: object): void; + } + + interface EmbedField { + name: string; + value: string; + inline: boolean; + } + + export class RichEmbed { + constructor(data?: RichEmbedOptions | MessageEmbed); + public author?: { name: string; url?: string; icon_url?: string; }; + public color?: number; + public description?: string; + public fields?: { name: string; value: string; inline?: boolean; }[]; + public file?: Attachment | string | FileOptions; + public files?: Array<Attachment | string | FileOptions>; + public footer?: { text?: string; icon_url?: string; }; + public image?: { url: string; proxy_url?: string; height?: number; width?: number; }; + public readonly length: number; + public thumbnail?: { url: string; height?: number; width?: number; }; + public timestamp?: Date; + public title?: string; + public url?: string; + public addBlankField(inline?: boolean): this; + public spliceFields(index: number, deleteCount: number, ...fields: EmbedFieldData[]): this; + public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; + public attachFile(file: Attachment | FileOptions | string): this; + public attachFiles(file: Array<Attachment | FileOptions | string>): this; + public setAuthor(name: StringResolvable, icon?: string, url?: string): this; + public setColor(color: ColorResolvable): this; + public setDescription(description: StringResolvable): this; + public setFooter(text: StringResolvable, icon?: string): this; + public setImage(url: string): this; + public setThumbnail(url: string): this; + public setTimestamp(timestamp?: Date | number): this; + public setTitle(title: StringResolvable): this; + public setURL(url: string): this; + public static normalizeField(name: StringResolvable, value: StringResolvable, inline?: boolean): EmbedField; + } + + export class RichPresenceAssets { + constructor(game: Game, assets: object); + public largeImage: Snowflake; + public largeText: string; + public smallImage: Snowflake; + public smallText: string; + public readonly smallImageURL: string; + public readonly largeImageURL: string; + } + + export class Role { + constructor(guild: Guild, data: object); + public readonly calculatedPosition: number; + public readonly client: Client; + public color: number; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public deleted: boolean; + public readonly editable: boolean; + public guild: Guild; + public readonly hexColor: string; + public hoist: boolean; + public id: Snowflake; + public managed: boolean; + public readonly members: Collection<Snowflake, GuildMember>; + public mentionable: boolean; + public name: string; + public permissions: number; + public position: number; + public comparePositionTo(role: Role): number; + public delete(reason?: string): Promise<Role>; + public edit(data: RoleData, reason?: string): Promise<Role>; + public equals(role: Role): boolean; + public hasPermission(permission: PermissionResolvable, explicit?: boolean, checkAdmin?: boolean): boolean; + public hasPermissions(permissions: PermissionResolvable, explicit?: boolean): boolean; + public serialize(): PermissionObject; + public setColor(color: string | number, reason?: string): Promise<Role>; + public setHoist(hoist: boolean, reason?: string): Promise<Role>; + public setMentionable(mentionable: boolean, reason?: string): Promise<Role>; + public setName(name: string, reason?: string): Promise<Role>; + public setPermissions(permissions: PermissionResolvable, reason?: string): Promise<Role>; + public setPosition(position: number, relative?: boolean): Promise<Role>; + public toString(): string; + + public static comparePositions(role1: Role, role2: Role): number; + } + + class SecretKey { + constructor(key: Uint8Array); + public key: Uint8Array; + } + + class SequentialRequestHandler extends RequestHandler { + constructor(restManager: object, endpoint: string); + public busy: boolean; + public endpoint: string; + public readonly globalLimit: boolean; + public queue: object[]; + public restManager: object; + public timeDifference: number; + public execute(item: object): Promise<object | Error>; + public handle(): void; + public push(request: object): void; + } + + export class Shard extends EventEmitter { + constructor(manager: ShardingManager, id: number, args?: string[]); + private _exitListener: Function; + private _handleExit(respawn?: boolean): void; + private _handleMessage(message: any): void; + + public env: object; + public id: string; + public manager: ShardingManager; + public process: ChildProcess; + public readonly: boolean; + public eval(script: string): Promise<any>; + public fetchClientValue(prop: string): Promise<any>; + public kill(): void; + public respawn(delay?: number): Promise<ChildProcess>; + public spawn(args?: string[], execArgv?: string[]): Promise<ChildProcess>; + public send(message: any): Promise<Shard>; + + public on(event: 'death', listener: () => void): this; + public on(event: 'disconnect', listener: () => void): this; + public on(event: 'message', listener: (message: any) => void): this; + public on(event: 'ready', listener: () => void): this; + public on(event: 'reconnecting', listener: () => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'death', listener: () => void): this; + public once(event: 'disconnect', listener: () => void): this; + public once(event: 'message', listener: (message: any) => void): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'reconnecting', listener: () => void): this; + public once(event: string, listener: Function): this; + } + + export class ShardClientUtil { + constructor(client: Client); + private _handleMessage(message: any): void; + private _respond(type: string, message: any): void; + + public readonly count: number; + public readonly id: number; + public broadcastEval(script: string): Promise<any[]>; + public fetchClientValues(prop: string): Promise<any[]>; + public send(message: any): Promise<void>; + + public static singleton(client: Client): ShardClientUtil; + } + + export class ShardingManager extends EventEmitter { + constructor(file: string, options?: { + totalShards?: number | 'auto'; + respawn?: boolean; + shardArgs?: string[]; + token?: string; + }); + private _spawn(amount: number, delay: number): Promise<Collection<number, Shard>>; + + public execArgv: string[]; + public file: string; + public respawn: boolean; + public shardArgs: string[]; + public shards: Collection<number, Shard>; + public token: string; + public totalShards: number | string; + public broadcast(message: any): Promise<Shard[]>; + public broadcastEval(script: string): Promise<any[]>; + public createShard(id: number): Promise<Shard>; + public fetchClientValues(prop: string): Promise<any[]>; + public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: true, currentShardIndex?: number): Promise<Collection<number, Shard>>; + public spawn(amount?: number, delay?: number): Promise<Collection<number, Shard>>; + + public on(event: 'launch', listener: (shard: Shard) => void): this; + public on(event: 'message', listener: (shard: Shard, message: any) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'launch', listener: (shard: Shard) => void): this; + public once(event: 'message', listener: (shard: Shard, message: any) => void): this; + public once(event: string, listener: Function): this; + } + + export class SnowflakeUtil { + public static deconstruct(snowflake: Snowflake): DeconstructedSnowflake; + public static generate(timestamp?: number | Date): Snowflake; + } + + export class StoreChannel extends GuildChannel { + constructor(guild: Guild, data: object); + public nsfw: boolean; + } + + export class StreamDispatcher extends VolumeInterface { + constructor(player: AudioPlayer, stream: NodeJS.ReadableStream, streamOptions: StreamOptions); + public destroyed: boolean; + public readonly passes: number; + public paused: boolean; + public player: AudioPlayer; + public stream: ReadableStream | VoiceBroadcast; + public readonly time: number; + public readonly totalStreamTime: number; + public end(reason?: string): void; + public pause(): void; + public resume(): void; + public setBitrate(bitrate: number | 'auto'): void; + } + + export class SystemChannelFlags extends BitField<SystemChannelFlagsString> { + public static FLAGS: Record<SystemChannelFlagsString, number>; + public static resolve(bit?: BitFieldResolvable<SystemChannelFlagsString>): number; + } + + export class Team { + constructor(client: Client, data: object); + public readonly client: Client; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public icon: string | null; + public readonly iconURL: string; + public id: Snowflake; + public members: Collection<Snowflake, TeamMember>; + public name: string; + public readonly owner: TeamMember; + public ownerID: Snowflake | null; + + public toString(): string; + } + + export class TeamMember { + constructor(client: Client, team: Team, data: object); + public readonly client: Client; + public id: Snowflake; + public membershipState: MembershipStates; + public permissions: string[]; + public team: Team; + public user: User; + + public toString(): string; + } + + export class TextChannel extends TextBasedChannel(GuildChannel) { + constructor(guild: Guild, data: object); + public lastMessageID: string; + public readonly members: Collection<Snowflake, GuildMember>; + public messages: Collection<Snowflake, Message>; + public nsfw: boolean; + public topic: string | null; + public rateLimitPerUser: number; + public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise<TextChannel>; + public createWebhook(name: string, avatar: BufferResolvable, reason?: string): Promise<Webhook>; + public fetchWebhooks(): Promise<Collection<Snowflake, Webhook>>; + public setNSFW(nsfw: boolean, reason?: string): Promise<this>; + } + + export class User extends PartialTextBasedChannel() { + constructor(client: Client, data: object); + public avatar: string; + public readonly avatarURL: string; + public bot: boolean; + public readonly client: Client; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public readonly defaultAvatarURL: string; + public discriminator: string; + public readonly displayAvatarURL: string; + public readonly dmChannel: DMChannel; + public id: Snowflake; + public lastMessageID: string; + public readonly note: string; + public readonly presence: Presence; + public system?: boolean; + public readonly tag: string; + public username: string; + public addFriend(): Promise<User>; + public block(): Promise<User>; + public createDM(): Promise<DMChannel>; + public deleteDM(): Promise<DMChannel>; + public equals(user: User): boolean; + public fetchProfile(): Promise<UserProfile>; + public removeFriend(): Promise<User>; + public setNote(note: string): Promise<User>; + public toString(): string; + public typingDurationIn(channel: ChannelResolvable): number; + public typingIn(channel: ChannelResolvable): boolean; + public typingSinceIn(channel: ChannelResolvable): Date; + public unblock(): Promise<User>; + } + + export class UserConnection { + constructor(user: User, data: object); + public id: string; + public integrations: object[]; + public name: string; + public revoked: boolean; + public type: string; + public user: User; + } + + export class UserProfile { + constructor(user: User, data: object); + public readonly client: Client; + public connections: Collection<string, UserConnection>; + public mutualGuilds: Collection<Snowflake, Guild>; + public premium: boolean; + public premiumSince: Date; + public user: User; + } + + export class Util { + public static arraysEqual(a: any[], b: any[]): boolean; + public static cloneObject(obj: object): object; + public static convertToBuffer(ab: ArrayBuffer | string): Buffer; + public static delayFor(ms: number): Promise<void>; + public static escapeMarkdown(text: string, onlyCodeBlock?: boolean, onlyInlineCode?: boolean): string; + public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise<number>; + public static makeError(obj: { name: string, message: string, stack: string }): Error; + public static makePlainError(err: Error): object; + public static mergeDefault(def: object, given: object): object; + public static moveElementInArray(array: any[], element: any, newIndex: number, offset?: boolean): number; + public static parseEmoji(text: string): { animated: boolean; name: string; id: string; }; + public static splitMessage(text: string, options?: SplitOptions): string | string[]; + public static str2ab(str: string): ArrayBuffer; + } + + export class VoiceBroadcast extends EventEmitter { + constructor(client: Client); + public readonly client: Client; + public currentTranscoder: object; + public readonly dispatchers: StreamDispatcher[]; + public prism: object; + public destroy(): void; + public end(): void; + public pause(): void; + public playArbitraryInput(input: string, options?: StreamOptions): VoiceBroadcast; + public playConvertedStream(stream: ReadableStream, options?: StreamOptions): VoiceBroadcast; + public playFile(file: string, options?: StreamOptions): StreamDispatcher; + public playOpusStream(stream: ReadableStream, options?: StreamOptions): StreamDispatcher; + public playStream(stream: ReadableStream, options?: StreamOptions): VoiceBroadcast; + public resume(): void; + + public on(event: string, listener: Function): this; + public on(event: 'end', listener: () => void): this; + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public on(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public on(event: 'warn', listener: (warning: string | Error) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'end', listener: () => void): this; + public once(event: 'error', listener: (error: Error) => void): this; + public once(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public once(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public once(event: 'warn', listener: (warning: string | Error) => void): this; + public once(event: string, listener: Function): this; + } + + export class VoiceChannel extends GuildChannel { + constructor(guild: Guild, data: object); + public bitrate: number; + public readonly connection: VoiceConnection; + public readonly full: boolean; + public readonly joinable: boolean; + public members: Collection<Snowflake, GuildMember>; + public readonly speakable: boolean; + public userLimit: number; + public join(): Promise<VoiceConnection>; + public leave(): void; + public setBitrate(bitrate: number, reason?: string): Promise<VoiceChannel>; + public setUserLimit(userLimit: number, reason?: string): Promise<VoiceChannel>; + } + + export class VoiceConnection extends EventEmitter { + constructor(voiceManager: ClientVoiceManager, channel: VoiceChannel); + private authentication: object; + private sockets: object; + private ssrcMap: Map<number, Snowflake>; + private speakingTimeouts: Map<number, NodeJS.Timer>; + private authenticate(): void; + private authenticateFailed(reason: string): void; + private checkAuthenticated(): void; + private cleanup(): void; + private connect(): void; + private onReady(data: object): void; + private onSessionDescription(mode: string, secret: string): void; + private onSpeaking(data: object): void; + private reconnect(token: string, endpoint: string): void; + private setSpeaking(value: boolean): void; + private updateChannel(channel: VoiceChannel): void; + + public channel: VoiceChannel; + public readonly client: Client; + public readonly dispatcher: StreamDispatcher; + public player: AudioPlayer; + public prism: object; + public receivers: VoiceReceiver[]; + public speaking: boolean; + public status: number; + public voiceManager: ClientVoiceManager; + public createReceiver(): VoiceReceiver; + public disconnect(): void; + public playArbitraryInput(input: string, options?: StreamOptions): StreamDispatcher; + public playBroadcast(broadcast: VoiceBroadcast, options?: StreamOptions): StreamDispatcher; + public playConvertedStream(stream: ReadableStream, options?: StreamOptions): StreamDispatcher; + public playFile(file: string, options?: StreamOptions): StreamDispatcher; + public playOpusStream(steam: ReadableStream, options?: StreamOptions): StreamDispatcher; + public playStream(stream: ReadableStream, options?: StreamOptions): StreamDispatcher; + public sendVoiceStateUpdate(options: object): void; + public setSessionID(sessionID: string): void; + public setTokenAndEndpoint(token: string, endpoint: string): void; + + public on(event: 'authenticated', listener: () => void): this; + public on(event: 'debug', listener: (message: string) => void): this; + public on(event: 'disconnect', listener: (error: Error) => void): this; + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'failed', listener: (error: Error) => void): this; + public on(event: 'newSession', listener: () => void): this; + public on(event: 'ready', listener: () => void): this; + public on(event: 'reconnecting', listener: () => void): this; + public on(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public on(event: 'warn', listener: (warning: string | Error) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'authenticated', listener: () => void): this; + public once(event: 'debug', listener: (message: string) => void): this; + public once(event: 'disconnect', listener: (error: Error) => void): this; + public once(event: 'error', listener: (error: Error) => void): this; + public once(event: 'failed', listener: (error: Error) => void): this; + public once(event: 'newSession', listener: () => void): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'reconnecting', listener: () => void): this; + public once(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public once(event: 'warn', listener: (warning: string | Error) => void): this; + public once(event: string, listener: Function): this; + } + + class VoiceConnectionUDPClient extends EventEmitter { + constructor(voiceConnection: VoiceConnection); + public discordAddress: string; + public readonly discordPort: number; + public localAddress: string; + public localPort: string; + public socket: any; + public voiceConnection: VoiceConnection; + public findEndpointAddress(): Promise<string>; + public send(packet: object): Promise<object>; + } + + export class VoiceReceiver extends EventEmitter { + constructor(connection: VoiceConnection); + private stoppedSpeaking(user: User): void; + + public destroyed: boolean; + public voiceConnection: VoiceConnection; + public createOpusStream(user: UserResolvable): ReadableStream; + public createPCMStream(user: UserResolvable): ReadableStream; + public destroy(): void; + public recreate(): void; + + public on(event: 'opus', listener: (user: User, buffer: Buffer) => void): this; + public on(event: 'pcm', listener: (user: User, buffer: Buffer) => void): this; + public on(event: 'warn', listener: (reason: string, message: string) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'opus', listener: (user: User, buffer: Buffer) => void): this; + public once(event: 'pcm', listener: (user: User, buffer: Buffer) => void): this; + public once(event: 'warn', listener: (reason: string, message: string) => void): this; + public once(event: string, listener: Function): this; + } + + export class VoiceRegion { + constructor(data: object); + public custom: boolean; + public deprecated: boolean; + public id: string; + public name: string; + public optimal: boolean; + public sampleHostname: string; + public vip: boolean; + } + + class VoiceWebsocket extends EventEmitter { + constructor(voiceConnection: VoiceConnection); + public attempts: number; + public readonly client: Client; + public voiceConnection: VoiceConnection; + public ws: any; + public clearHeartbeat(): void; + public connect(): void; + public onClose(): void; + public onError(error: Error): void; + public onMessage(event: any): void; + public onOpen(): void; + public onPacket(packet: object): void; + public reset(): void; + public send(data: string): Promise<string>; + public sendHeartbeat(): void; + public sendPacket(packet: object): Promise<string>; + public setHeartbeat(interval: number): void; + + public on(event: 'ready', listener: (packet: object) => void): this; + public on(event: 'sessionDescription', listener: (encryptionMode: string, secretKey: SecretKey) => void): this; + public on(event: 'startSpeaking', listener: (data: object) => void): this; + public on(event: 'unknownPacket', listener: (packet: object) => void): this; + public on(event: 'warn', listener: (warn: string) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'ready', listener: (packet: object) => void): this; + public once(event: 'sessionDescription', listener: (encryptionMode: string, secretKey: SecretKey) => void): this; + public once(event: 'startSpeaking', listener: (data: object) => void): this; + public once(event: 'unknownPacket', listener: (packet: object) => void): this; + public once(event: 'warn', listener: (warn: string) => void): this; + public once(event: string, listener: Function): this; + } + + export class VolumeInterface extends EventEmitter { + constructor(object?: { volume: number }) + public readonly volume: number; + public readonly volumeDecibels: number; + public readonly volumeLogarithmic: number; + public setVolume(volume: number): void; + public setVolumeDecibels(db: number): void; + public setVolumeLogarithmic(value: number): void; + + public on(event: 'debug', listener: (information: string) => void): this; + public on(event: 'end', listener: (reason: string) => void): this; + public on(event: 'error', listener: (err: Error) => void): this; + public on(event: 'speaking', listener: (value: boolean) => void): this; + public on(event: 'start', listener: () => void): this; + public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'debug', listener: (information: string) => void): this; + public once(event: 'end', listener: (reason: string) => void): this; + public once(event: 'error', listener: (err: Error) => void): this; + public once(event: 'speaking', listener: (value: boolean) => void): this; + public once(event: 'start', listener: () => void): this; + public once(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + public once(event: string, listener: Function): this; + } + + export class Webhook { + constructor(client: Client, dataOrID: object | string, token: string); + public avatar: string | null; + public readonly avatarURL: string | null; + public channelID: string; + public readonly client: Client; + public guildID: string; + public id: Snowflake; + public name: string; + public owner: User | object; + public token: string | null; + public type: WebhookTypes; + public readonly url: string; + public delete(reason?: string): Promise<void>; + public edit(name?: string, avatar?: BufferResolvable): Promise<Webhook>; + public edit(options?: WebhookEditOptions, reason?: string): Promise<Webhook>; + public send(content?: StringResolvable, options?: WebhookMessageOptions & { split: false } | RichEmbed | Attachment): Promise<Message>; + public send(content?: StringResolvable, options?: WebhookMessageOptions | RichEmbed | Attachment): Promise<Message | Message[]>; + public send(options?: WebhookMessageOptions | RichEmbed | Attachment): Promise<Message | Message[]>; + public sendCode(lang: string, content: StringResolvable, options?: WebhookMessageOptions): Promise<Message | Message[]>; + public sendFile(attachment: BufferResolvable, name?: string, content?: StringResolvable, options?: WebhookMessageOptions): Promise<Message>; + public sendMessage(content?: StringResolvable, options?: WebhookMessageOptions): Promise<Message | Message[]>; + public sendMessage(options?: WebhookMessageOptions): Promise<Message | Message[]>; + public sendSlackMessage(body: object): Promise<void>; + } + + export class WebhookClient extends Webhook { + constructor(id: string, token: string, options?: ClientOptions); + private _intervals: Set<NodeJS.Timer>; + private _timeouts: Set<NodeJS.Timer>; + private resolver: ClientDataResolver; + private rest: object; + public token: string; + + public options: ClientOptions; + public clearInterval(interval: NodeJS.Timer): void; + public clearTimeout(timeout: NodeJS.Timer): void; + public destroy(): void; + public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + } + +//#endregion + +//#region Mixins + + // Model the TextBasedChannel mixin system, allowing application of these fields + // to the classes that use these methods without having to manually add them + // to each of those classes + + type Constructable<T> = new (...args: any[]) => T; + const PartialTextBasedChannel: <T>(Base?: Constructable<T>) => Constructable<T & PartialTextBasedChannelFields>; + const TextBasedChannel: <T>(Base?: Constructable<T>) => Constructable<T & TextBasedChannelFields>; + + type PartialTextBasedChannelFields = { + lastMessage: Message; + acknowledge(): Promise<DMChannel | GroupDMChannel | TextChannel>; + send(content?: StringResolvable, options?: MessageOptions & { split: false } | RichEmbed | Attachment): Promise<Message>; + send(content?: StringResolvable, options?: MessageOptions | RichEmbed | Attachment): Promise<Message | Message[]>; + send(options?: MessageOptions | RichEmbed | Attachment): Promise<Message | Message[]>; + sendCode(lang: string, content: StringResolvable, options?: MessageOptions): Promise<Message | Message[]>; + sendEmbed(embed: RichEmbed | RichEmbedOptions, content?: string, options?: MessageOptions): Promise<Message>; + sendEmbed(embed: RichEmbed | RichEmbedOptions, options?: MessageOptions): Promise<Message>; + sendFile(attachment: BufferResolvable, name?: string, content?: StringResolvable, options?: MessageOptions): Promise<Message>; + sendMessage(content?: string, options?: MessageOptions): Promise<Message | Message[]>; + sendMessage(options?: MessageOptions): Promise<Message | Message[]>; + }; + + type TextBasedChannelFields = { + lastPinTimestamp: number; + readonly lastPinAt: Date; + typing: boolean; + typingCount: number; + awaitMessages(filter: CollectorFilter, options?: AwaitMessagesOptions): Promise<Collection<string, Message>>; + bulkDelete(messages: Collection<string, Message> | Message[] | Snowflake[] | number, filterOld?: boolean): Promise<Collection<string, Message>>; + createCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; + createMessageCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; + fetchMessage(messageID: string): Promise<Message>; + fetchMessages(options?: ChannelLogsQueryOptions): Promise<Collection<string, Message>>; + fetchPinnedMessages(): Promise<Collection<string, Message>>; + search(options?: MessageSearchOptions): Promise<MessageSearchResult>; + startTyping(count?: number): void; + stopTyping(force?: boolean): void; + } & PartialTextBasedChannelFields; + +//#endregion + +//#region Typedefs + + type ActivityType = 'PLAYING' + | 'STREAMING' + | 'LISTENING' + | 'WATCHING' + | 'CUSTOM_STATUS'; + + type AddGuildMemberOptions = { + accessToken: String; + nick?: string; + roles?: Collection<string, Role> | Role[] | string[]; + mute?: boolean; + deaf?: boolean; + }; + + type AuditLogChange = { + key: string; + old?: any; + new?: any; + }; + + type AwaitMessagesOptions = MessageCollectorOptions & { errors?: string[] }; + + type AwaitReactionsOptions = ReactionCollectorOptions & { errors?: string[] }; + + type BanInfo = { + user: User; + reason: string | null; + }; + + type BanOptions = { + days?: number; + reason?: string; + }; + + type Base64Resolvable = Buffer | Base64String; + + type Base64String = string; + + type BitFieldResolvable<T extends string> = RecursiveArray<T | number | Readonly<BitField<T>>> | T | number | Readonly<BitField<T>>; + + type BufferResolvable = Buffer | string; + + type ChannelCreationOverwrites = { + allow?: PermissionResolvable; + deny?: PermissionResolvable; + allowed?: PermissionResolvable; + denied?: PermissionResolvable; + id: RoleResolvable | UserResolvable; + }; + + type ChannelData = { + type?: GuildChannelType; + name?: string; + position?: number; + topic?: string; + nsfw?: boolean; + bitrate?: number; + userLimit?: number; + parent?: ChannelResolvable; + permissionOverwrites?: PermissionOverwrites[] | ChannelCreationOverwrites[]; + rateLimitPerUser?: number; + reason?: string; + }; + + type ChannelLogsQueryOptions = { + limit?: number + before?: Snowflake + after?: Snowflake + around?: Snowflake + }; + + type ChannelPosition = { + channel: ChannelResolvable; + position: number; + }; + + type ChannelResolvable = Channel | Guild | Message | Snowflake; + + type ClientOptions = { + apiRequestMethod?: string; + shardId?: number; + shardCount?: number; + messageCacheMaxSize?: number; + messageCacheLifetime?: number; + messageSweepInterval?: number; + fetchAllMembers?: boolean; + disableEveryone?: boolean; + sync?: boolean; + restWsBridgeTimeout?: number; + restTimeOffset?: number; + retryLimit?: number; + disabledEvents?: WSEventType[]; + ws?: WebSocketOptions; + http?: HTTPOptions; + }; + + type CollectorHandler<K, V> = { key: K, value: V }; + type CollectorFilter = (...args: any[]) => boolean; + type CollectorOptions = { + time?: number; + idle?: number; + }; + + type ColorResolvable = ('DEFAULT' + | 'WHITE' + | 'AQUA' + | 'GREEN' + | 'BLUE' + | 'PURPLE' + | 'LUMINOUS_VIVID_PINK' + | 'GOLD' + | 'ORANGE' + | 'RED' + | 'GREY' + | 'DARKER_GREY' + | 'NAVY' + | 'DARK_AQUA' + | 'DARK_GREEN' + | 'DARK_BLUE' + | 'DARK_PURPLE' + | ' DARK_VIVID_PINK' + | 'DARK_GOLD' + | 'DARK_ORANGE' + | 'DARK_RED' + | 'DARK_GREY' + | 'LIGHT_GREY' + | 'DARK_NAVY' + | 'RANDOM') + | [number, number, number] + | number + | string; + + type CrosspostedChannel = { + channelID: Snowflake; + guildID: Snowflake; + type: Channel["type"] | 'unknown'; + name: string; + }; + + type DeconstructedSnowflake = { + timestamp: number; + date: Date; + workerID: number; + processID: number; + increment: number; + binary: string; + }; + + type DefaultMessageNotifications = 'ALL' + | 'MENTIONS'; + + type EmojiEditData = { + name?: string; + roles?: Collection<Snowflake, Role> | Role[] | Snowflake[]; + }; + + type EmojiIdentifierResolvable = string | Emoji | ReactionEmoji; + + type FileOptions = { + attachment: BufferResolvable; + name?: string; + }; + + type GroupDMRecipientOptions = { + user?: UserResolvable | Snowflake; + accessToken?: string; + nick?: string; + }; + + type GuildAuditLogsAction = keyof GuildAuditLogsActions; + + type GuildAuditLogsActions = { + ALL?: null; + GUILD_UPDATE?: number; + CHANNEL_CREATE?: number; + CHANNEL_UPDATE?: number; + CHANNEL_DELETE?: number; + CHANNEL_OVERWRITE_CREATE?: number; + CHANNEL_OVERWRITE_UPDATE?: number; + CHANNEL_OVERWRITE_DELETE?: number; + MEMBER_KICK?: number; + MEMBER_PRUNE?: number; + MEMBER_BAN_ADD?: number; + MEMBER_BAN_REMOVE?: number; + MEMBER_UPDATE?: number; + MEMBER_ROLE_UPDATE?: number; + MEMBER_MOVE?: number; + MEMBER_DISCONNECT?: number; + BOT_ADD?: number; + ROLE_CREATE?: number; + ROLE_UPDATE?: number; + ROLE_DELETE?: number; + INVITE_CREATE?: number; + INVITE_UPDATE?: number; + INVITE_DELETE?: number; + WEBHOOK_CREATE?: number; + WEBHOOK_UPDATE?: number; + WEBHOOK_DELETE?: number; + EMOJI_CREATE?: number; + EMOJI_UPDATE?: number; + EMOJI_DELETE?: number; + MESSAGE_DELETE?: number; + MESSAGE_BULK_DELETE?: number; + MESSAGE_PIN?: number; + MESSAGE_UNPIN?: number; + INTEGRATION_CREATE?: number; + INTEGRATION_UPDATE?: number; + INTEGRATION_DELETE?: number; + }; + + type GuildAuditLogsActionType = 'CREATE' + | 'DELETE' + | 'UPDATE' + | 'ALL'; + + type GuildAuditLogsFetchOptions = { + before?: Snowflake | GuildAuditLogsEntry; + after?: Snowflake | GuildAuditLogsEntry; + limit?: number; + user?: UserResolvable; + type?: string | number; + }; + + type GuildAuditLogsTarget = keyof GuildAuditLogsTargets; + + type GuildAuditLogsTargets = { + ALL?: string; + GUILD?: string; + CHANNEL?: string; + USER?: string; + ROLE?: string; + INVITE?: string; + WEBHOOK?: string; + EMOJI?: string; + MESSAGE?: string; + INTEGRATION?: string; + UNKNOWN?: string; + }; + + type GuildChannelCloneOptions = { + name?: string; + permissionOverwrites?: ChannelCreationOverwrites[] | Collection<Snowflake, PermissionOverwrites>; + type?: GuildChannelType; + topic?: string; + nsfw?: boolean; + bitrate?: number; + userLimit?: number; + rateLimitPerUser?: number; + parent?: ChannelResolvable; + reason?: string; + } + + type GuildChannelMessageNotifications = MessageNotifications + & 'INHERIT'; + + type GuildChannelType = 'category' | 'text' | 'voice' | 'news' | 'store'; + + type GuildEditData = { + name?: string; + region?: string; + verificationLevel?: number; + explicitContentFilter?: number; + afkChannel?: ChannelResolvable; + systemChannel?: ChannelResolvable; + systemChannelFlags?: SystemChannelFlagsResolvable; + afkTimeout?: number; + banner?: Base64Resolvable; + icon?: Base64Resolvable; + owner?: GuildMemberResolvable; + splash?: Base64Resolvable; + }; + + type GuildEmbedData = { + enabled: boolean; + channel: ChannelResolvable; + }; + + type GuildMemberEditData = { + nick?: string; + roles?: Collection<Snowflake, Role> | Role[] | Snowflake[]; + mute?: boolean; + deaf?: boolean; + channel?: ChannelResolvable | null; + }; + + type GuildMemberResolvable = GuildMember | User; + + type GuildResolvable = Guild | Snowflake; + + type HTTPOptions = { + version?: number; + host?: string; + cdn?: string; + }; + + type IntegrationData = { + id: string; + type: string; + } + + type IntegrationEditData = { + expireBehavior?: number; + expireGracePeriod?: number; + } + + type IntegrationAccount = { + id: string; + number: string; + } + + type InviteOptions = { + temporary?: boolean; + maxAge?: number; + maxUses?: number; + unique?: boolean; + }; + + type InviteResolvable = string; + + type MembershipStates = 'INVITED' + | 'ACCEPTED'; + + type MessageCollectorOptions = CollectorOptions & { + max?: number; + maxMatches?: number; + }; + + type MessageEditOptions = { + embed?: RichEmbedOptions; + code?: string | boolean; + flags?: BitFieldResolvable<MessageFlagsString>; + }; + + type MessageFlagsString = 'CROSSPOSTED' + | 'IS_CROSSPOST' + | 'SUPRRESS_EMBEDS' + | 'SOURCE_MESSAGE_DELETED' + | 'URGENT'; + + type MessageNotifications = 'EVERYTHING' + | 'MENTIONS' + | 'NOTHING'; + + type MessageOptions = { + tts?: boolean; + nonce?: string; + embed?: RichEmbed | RichEmbedOptions, + disableEveryone?: boolean; + file?: FileOptions | string; + files?: (FileOptions | BufferResolvable | Attachment)[]; + code?: string | boolean; + split?: boolean | SplitOptions; + reply?: UserResolvable; + }; + + type MessageSearchOptions = { + content?: string; + maxID?: Snowflake; + minID?: Snowflake; + has?: 'link' + | 'embed' + | 'file' + | 'video' + | 'image' + | 'sound' + | '-link' + | '-embed' + | '-file' + | '-video' + | '-image' + | '-sound'; + channel?: ChannelResolvable; + author?: UserResolvable; + authorType?: 'user' + | 'bot' + | 'webhook' + | '-user' + | '-bot' + | '-webhook'; + sortBy?: 'relevant' | 'recent'; + sortOrder?: 'asc' | 'desc'; + contextSize?: number; + limit?: number; + offset?: number; + mentions?: UserResolvable; + mentionsEveryone?: boolean; + linkHostname?: string; + embedProvider?: string; + embedType?: 'image' | 'video' | 'url' | 'rich'; + attachmentFilename?: string; + attachmentExtension?: string; + before?: Date; + after?: Date; + during?: Date; + nsfw?: boolean; + }; + + type MessageReference = { + channelID: Snowflake; + guildID: Snowflake | null; + messageID: Snowflake | null; + }; + + type MessageSearchResult = { + totalResults: number; + messages: Message[][]; + }; + + type ActivityFlags = { + INSTANCE?: number; + JOIN?: number; + SPECTATE?: number; + JOIN_REQUEST?: number; + SYNC?: number; + PLAY?: number; + }; + + type PermissionFlags = { + ADMINISTRATOR?: number; + CREATE_INSTANT_INVITE?: number; + KICK_MEMBERS?: number; + BAN_MEMBERS?: number; + MANAGE_CHANNELS?: number; + MANAGE_GUILD?: number; + ADD_REACTIONS?: number; + VIEW_AUDIT_LOG?: number; + PRIORITY_SPEAKER?: number; + STREAM?: number; + VIEW_CHANNEL?: number; + READ_MESSAGES?: number; + SEND_MESSAGES?: number; + SEND_TTS_MESSAGES?: number; + MANAGE_MESSAGES?: number; + EMBED_LINKS?: number; + ATTACH_FILES?: number; + READ_MESSAGE_HISTORY?: number; + MENTION_EVERYONE?: number; + USE_EXTERNAL_EMOJIS?: number; + EXTERNAL_EMOJIS?: number; + CONNECT?: number; + SPEAK?: number; + MUTE_MEMBERS?: number; + DEAFEN_MEMBERS?: number; + MOVE_MEMBERS?: number; + USE_VAD?: number; + CHANGE_NICKNAME?: number; + MANAGE_NICKNAMES?: number; + MANAGE_ROLES?: number; + MANAGE_ROLES_OR_PERMISSIONS?: number; + MANAGE_WEBHOOKS?: number; + MANAGE_EMOJIS?: number; + }; + + type PermissionObject = { + ADMINISTRATOR?: boolean; + CREATE_INSTANT_INVITE?: boolean; + KICK_MEMBERS?: boolean; + BAN_MEMBERS?: boolean; + MANAGE_CHANNELS?: boolean; + MANAGE_GUILD?: boolean; + ADD_REACTIONS?: boolean; + VIEW_AUDIT_LOG?: boolean; + PRIORITY_SPEAKER?: boolean; + STREAM?: boolean; + VIEW_CHANNEL?: boolean; + READ_MESSAGES?: boolean; + SEND_MESSAGES?: boolean; + SEND_TTS_MESSAGES?: boolean; + MANAGE_MESSAGES?: boolean; + EMBED_LINKS?: boolean; + ATTACH_FILES?: boolean; + READ_MESSAGE_HISTORY?: boolean; + MENTION_EVERYONE?: boolean; + USE_EXTERNAL_EMOJIS?: boolean; + EXTERNAL_EMOJIS?: boolean; + CONNECT?: boolean; + SPEAK?: boolean; + MUTE_MEMBERS?: boolean; + DEAFEN_MEMBERS?: boolean; + MOVE_MEMBERS?: boolean; + USE_VAD?: boolean; + CHANGE_NICKNAME?: boolean; + MANAGE_NICKNAMES?: boolean; + MANAGE_ROLES?: boolean; + MANAGE_ROLES_OR_PERMISSIONS?: boolean; + MANAGE_WEBHOOKS?: boolean; + MANAGE_EMOJIS?: boolean; + }; + + type PermissionString = 'ADMINISTRATOR' + | 'CREATE_INSTANT_INVITE' + | 'KICK_MEMBERS' + | 'BAN_MEMBERS' + | 'MANAGE_CHANNELS' + | 'MANAGE_GUILD' + | 'ADD_REACTIONS' + | 'VIEW_AUDIT_LOG' + | 'PRIORITY_SPEAKER' + | 'STREAM' + | 'VIEW_CHANNEL' + | 'READ_MESSAGES' + | 'SEND_MESSAGES' + | 'SEND_TTS_MESSAGES' + | 'MANAGE_MESSAGES' + | 'EMBED_LINKS' + | 'ATTACH_FILES' + | 'READ_MESSAGE_HISTORY' + | 'MENTION_EVERYONE' + | 'USE_EXTERNAL_EMOJIS' + | 'EXTERNAL_EMOJIS' + | 'CONNECT' + | 'SPEAK' + | 'MUTE_MEMBERS' + | 'DEAFEN_MEMBERS' + | 'MOVE_MEMBERS' + | 'USE_VAD' + | 'CHANGE_NICKNAME' + | 'MANAGE_NICKNAMES' + | 'MANAGE_ROLES' + | 'MANAGE_ROLES_OR_PERMISSIONS' + | 'MANAGE_WEBHOOKS' + | 'MANAGE_EMOJIS'; + + type PermissionOverwriteOptions = PermissionObject; + + interface RecursiveArray<T> extends Array<T | RecursiveArray<T>> { } + + interface EmbedFieldData { + name: StringResolvable; + value: StringResolvable; + inline?: boolean; + } + + type PermissionResolvable = BitFieldResolvable<PermissionString> + + type PremiumTier = number; + + type PresenceData = { + status?: PresenceStatus; + afk?: boolean; + game?: { + name?: string; + url?: string; + type?: ActivityType; + } | null; + }; + + type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; + + type PresenceStatus = ClientPresenceStatus | 'invisible'; + type PresenceStatusData = ClientPresenceStatus | 'offline'; + + type ClientPresenceStatusData = { + web?: ClientPresenceStatus; + mobile?: ClientPresenceStatus; + desktop?: ClientPresenceStatus; + }; + + type RateLimitInfo = { + limit: number; + timeDifference: number; + method: string; + path: string; + }; + + type ReactionCollectorOptions = CollectorOptions & { + max?: number; + maxEmojis?: number; + maxUsers?: number; + }; + + type RichEmbedOptions = { + title?: string; + description?: string; + url?: string; + timestamp?: Date; + color?: number | string; + fields?: { name: string; value: string; inline?: boolean; }[]; + file?: Attachment | string | FileOptions; + author?: { name: string; url?: string; icon_url?: string; }; + thumbnail?: { url: string; height?: number; width?: number; }; + image?: { url: string; proxy_url?: string; height?: number; width?: number; }; + video?: { url: string; height: number; width: number; }; + footer?: { text?: string; icon_url?: string; }; + }; + + type RoleData = { + name?: string; + color?: ColorResolvable; + hoist?: boolean; + position?: number; + permissions?: PermissionResolvable; + mentionable?: boolean; + }; + + type RolePosition = { + role: RoleResolvable; + position: number; + }; + + type RoleResolvable = Role | string; + + type Snowflake = string; + + type SplitOptions = { + maxLength?: number; + char?: string; + prepend?: string; + append?: string; + }; + + type Status = number; + + type StreamOptions = { + seek?: number; + volume?: number; + passes?: number; + bitrate?: number | 'auto'; + }; + + type StringResolvable = string | string[] | any; + + type SystemChannelFlagsString = 'WELCOME_MESSAGE_DISABLED' | 'BOOST_MESSAGE_DISABLED'; + + type SystemChannelFlagsResolvable = BitFieldResolvable<SystemChannelFlagsString>; + + type UserResolvable = User | Snowflake | Message | Guild | GuildMember; + + type VoiceStatus = number; + + type WebhookEditOptions = { + name?: string; + avatar?: BufferResolvable; + channel?: ChannelResolvable; + }; + + type WebhookMessageOptions = { + username?: string; + avatarURL?: string; + tts?: boolean; + nonce?: string; + embeds?: (RichEmbed | object)[]; + disableEveryone?: boolean; + file?: FileOptions | BufferResolvable | Attachment; + files?: (FileOptions | BufferResolvable | Attachment)[]; + code?: string | boolean; + split?: boolean | SplitOptions; + }; + + type WebhookTypes = 'Incoming' | 'Channel Follower'; + + type WebSocketOptions = { + large_threshold?: number; + compress?: boolean; + }; + + type WSEventType = 'READY' + | 'RESUMED' + | 'GUILD_SYNC' + | 'GUILD_CREATE' + | 'GUILD_DELETE' + | 'GUILD_UPDATE' + | 'GUILD_MEMBER_ADD' + | 'GUILD_MEMBER_REMOVE' + | 'GUILD_MEMBER_UPDATE' + | 'GUILD_MEMBERS_CHUNK' + | 'GUILD_INTEGRATIONS_UPDATE' + | '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' + | 'USER_SETTINGS_UPDATE' + | 'USER_GUILD_SETTINGS_UPDATE' + | 'PRESENCE_UPDATE' + | 'VOICE_STATE_UPDATE' + | 'TYPING_START' + | 'VOICE_SERVER_UPDATE' + | 'WEBHOOKS_UPDATE' + | 'RELATIONSHIP_ADD' + | 'RELATIONSHIP_REMOVE'; + +//#endregion +} |