diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..b0f5c06b8
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,133 @@
+{
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": 6
+ },
+ "env": {
+ "es6": true,
+ "node": true
+ },
+ "rules": {
+ "no-extra-parens": ["warn", "all", {
+ "nestedBinaryExpressions": false
+ }],
+ "valid-jsdoc": ["error", {
+ "requireReturn": false,
+ "requireReturnDescription": false,
+ "preferType": {
+ "String": "string",
+ "Number": "number",
+ "Boolean": "boolean",
+ "Function": "function",
+ "object": "Object",
+ "date": "Date",
+ "error": "Error"
+ },
+ "prefer": {
+ "return": "returns"
+ }
+ }],
+
+ "accessor-pairs": "warn",
+ "array-callback-return": "error",
+ "complexity": "warn",
+ "consistent-return": "error",
+ "curly": ["error", "multi-line", "consistent"],
+ "dot-location": ["error", "property"],
+ "dot-notation": "error",
+ "eqeqeq": "error",
+ "no-empty-function": "error",
+ "no-floating-decimal": "error",
+ "no-implied-eval": "error",
+ "no-invalid-this": "error",
+ "no-lone-blocks": "error",
+ "no-multi-spaces": "error",
+ "no-new-func": "error",
+ "no-new-wrappers": "error",
+ "no-new": "error",
+ "no-octal-escape": "error",
+ "no-return-assign": "error",
+ "no-self-compare": "error",
+ "no-sequences": "error",
+ "no-throw-literal": "error",
+ "no-unmodified-loop-condition": "error",
+ "no-unused-expressions": "error",
+ "no-useless-call": "error",
+ "no-useless-concat": "error",
+ "no-useless-escape": "error",
+ "no-void": "error",
+ "no-warning-comments": "warn",
+ "wrap-iife": "error",
+ "yoda": "error",
+
+ "no-label-var": "error",
+ "no-shadow": "error",
+ "no-undef-init": "error",
+
+ "callback-return": "error",
+ "handle-callback-err": "error",
+ "no-mixed-requires": "error",
+ "no-new-require": "error",
+ "no-path-concat": "error",
+ "no-process-env": "error",
+
+ "array-bracket-spacing": "error",
+ "block-spacing": "error",
+ "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+ "comma-dangle": ["error", "always-multiline"],
+ "comma-spacing": "error",
+ "comma-style": "error",
+ "computed-property-spacing": "error",
+ "consistent-this": ["error", "$this"],
+ "eol-last": "error",
+ "func-names": "error",
+ "func-style": ["error", "declaration", { "allowArrowFunctions": true }],
+ "indent": ["error", 2, { "SwitchCase": 1 }],
+ "key-spacing": "error",
+ "keyword-spacing": "error",
+ "max-depth": "error",
+ "max-len": ["error", 120, 2],
+ "max-nested-callbacks": ["error", { "max": 4 }],
+ "max-statements-per-line": ["error", { "max": 2 }],
+ "new-cap": "error",
+ "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 3 }],
+ "no-array-constructor": "error",
+ "no-inline-comments": "error",
+ "no-lonely-if": "error",
+ "no-mixed-operators": "error",
+ "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
+ "no-new-object": "error",
+ "no-spaced-func": "error",
+ "no-trailing-spaces": "error",
+ "no-unneeded-ternary": "error",
+ "no-whitespace-before-property": "error",
+ "object-curly-spacing": ["error", "always"],
+ "operator-assignment": "error",
+ "operator-linebreak": ["error", "after"],
+ "padded-blocks": ["error", "never"],
+ "quote-props": ["error", "as-needed"],
+ "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
+ "semi-spacing": "error",
+ "semi": "error",
+ "space-before-blocks": "error",
+ "space-before-function-paren": ["error", "never"],
+ "space-in-parens": "error",
+ "space-infix-ops": "error",
+ "space-unary-ops": "error",
+ "spaced-comment": "error",
+ "unicode-bom": "error",
+
+ "arrow-body-style": "error",
+ "arrow-spacing": "error",
+ "no-duplicate-imports": "error",
+ "no-useless-computed-key": "error",
+ "no-useless-constructor": "error",
+ "prefer-arrow-callback": "error",
+ "prefer-rest-params": "error",
+ "prefer-spread": "error",
+ "prefer-template": "error",
+ "rest-spread-spacing": "error",
+ "template-curly-spacing": "error",
+ "yield-star-spacing": "error"
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..9167b884f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,38 @@
+# Created by https://www.gitignore.io
+
+.tmp/
+.vscode/
+
+### Node ###
+# Logs
+logs
+*.log
+
+test/auth.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
+node_modules
+test/auth.json
+examples/auth.json
+docs/_build
\ No newline at end of file
diff --git a/.jscsrc b/.jscsrc
new file mode 100644
index 000000000..d636656da
--- /dev/null
+++ b/.jscsrc
@@ -0,0 +1,10 @@
+{
+ "preset": "airbnb",
+ "validateIndentation": "\t",
+ "maximumLineLength": 140,
+ "maxErrors": 5000,
+ "disallowMultipleVarDecl": false,
+ "disallowSpacesInsideObjectBrackets": false,
+ "disallowMixedSpacesAndTabs": false,
+ "excludeFiles": []
+}
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..82e77e0af
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+ - "6"
+cache:
+ directories:
+ - node_modules
+install: npm install
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..8dada3eda
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ 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
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ 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/README.md b/README.md
new file mode 100644
index 000000000..4384e9814
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+
+
+ 
+
+
REWRITE
+
\ No newline at end of file
diff --git a/docs/custom/avatar.js b/docs/custom/avatar.js
new file mode 100644
index 000000000..b3a77ced7
--- /dev/null
+++ b/docs/custom/avatar.js
@@ -0,0 +1,10 @@
+const fs = require('fs');
+
+module.exports = {
+ category: 'Examples',
+ name: 'Avatars',
+ data:
+`\`\`\`js
+${fs.readFileSync('./docs/custom/examples/avatar.js').toString('utf-8')}
+\`\`\``,
+};
diff --git a/docs/custom/documents/updating.md b/docs/custom/documents/updating.md
new file mode 100644
index 000000000..f8c2abf11
--- /dev/null
+++ b/docs/custom/documents/updating.md
@@ -0,0 +1,11 @@
+# About the Rewrite
+The rewrite takes a much more OOP approach than previous versions, which allows code to be much more manageable.
+It's been rebuilt from the ground up and should be much more stable, fixing caching issues that affected
+older versions and it also has support for new Discord Features, such as emojis.
+
+## Upgrading your code
+The rewrite has a _lot_ of breaking changes. Major methods, e.g. `client.sendMessage(channel, message)` have been moved
+from the Client class towards their respective classes - `textChannel.sendMessage(message)`. You can find out the full
+extent of these changes by looking at the classes in the documentation.
+
+Additionally, some event names and parameters have changed - you should revisit these.
diff --git a/docs/custom/documents/welcome.md b/docs/custom/documents/welcome.md
new file mode 100644
index 000000000..c66cd635d
--- /dev/null
+++ b/docs/custom/documents/welcome.md
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+[](https://travis-ci.org/hydrabolt/discord.js) [](http://discordjs.readthedocs.org/en/latest/?badge=latest)
+
+[](https://nodei.co/npm/discord.js/)
+
+# Welcome!
+Welcome to the discord.js rewrite documentation. The rewrite has taken a lot of time, but it should be much more
+stable and performance-friendly than previous versions.
+
+## Installation
+`npm i --save hydrabolt/discord.js#indev-rewrite`
\ No newline at end of file
diff --git a/docs/custom/examples/avatar.js b/docs/custom/examples/avatar.js
new file mode 100644
index 000000000..796d942bd
--- /dev/null
+++ b/docs/custom/examples/avatar.js
@@ -0,0 +1,30 @@
+/*
+ Send a user a link to their avatar
+*/
+
+// import the discord.js module
+const Discord = require('discord.js');
+
+// create an instance of a Discord Client, and call it bot
+const bot = new Discord.Client();
+
+// the token of your bot - https://discordapp.com/developers/applications/me
+const token = 'your bot token here';
+
+// the ready event is vital, it means that your bot will only start reacting to information
+// from Discord _after_ ready is emitted.
+bot.on('ready', () => {
+ console.log('I am ready!');
+});
+
+// create an event listener for messages
+bot.on('message', message => {
+ // if the message is "what is my avatar",
+ if (message.content === 'what is my avatar') {
+ // send the user's avatar URL
+ message.reply(message.author.avatarURL);
+ }
+});
+
+// log our bot in
+bot.login(token);
diff --git a/docs/custom/examples/ping_pong.js b/docs/custom/examples/ping_pong.js
new file mode 100644
index 000000000..4cede6a80
--- /dev/null
+++ b/docs/custom/examples/ping_pong.js
@@ -0,0 +1,30 @@
+/*
+ A ping pong bot, whenever you send "ping", it replies "pong".
+*/
+
+// import the discord.js module
+const Discord = require('discord.js');
+
+// create an instance of a Discord Client, and call it bot
+const bot = new Discord.Client();
+
+// the token of your bot - https://discordapp.com/developers/applications/me
+const token = 'your bot token here';
+
+// the ready event is vital, it means that your bot will only start reacting to information
+// from Discord _after_ ready is emitted.
+bot.on('ready', () => {
+ console.log('I am ready!');
+});
+
+// create an event listener for messages
+bot.on('message', message => {
+ // if the message is "ping",
+ if (message.content === 'ping') {
+ // send "pong" to the same channel.
+ message.channel.sendMessage('pong');
+ }
+});
+
+// log our bot in
+bot.login(token);
diff --git a/docs/custom/index.js b/docs/custom/index.js
new file mode 100644
index 000000000..a75b308e7
--- /dev/null
+++ b/docs/custom/index.js
@@ -0,0 +1,17 @@
+const files = [
+ require('./welcome'),
+ require('./updating'),
+ require('./ping_pong'),
+ require('./avatar'),
+];
+
+const categories = {};
+for (const file of files) {
+ file.category = file.category.toLowerCase();
+ if (!categories[file.category]) {
+ categories[file.category] = [];
+ }
+ categories[file.category].push(file);
+}
+
+module.exports = categories;
diff --git a/docs/custom/ping_pong.js b/docs/custom/ping_pong.js
new file mode 100644
index 000000000..c6952ad6c
--- /dev/null
+++ b/docs/custom/ping_pong.js
@@ -0,0 +1,10 @@
+const fs = require('fs');
+
+module.exports = {
+ category: 'Examples',
+ name: 'Ping Pong',
+ data:
+`\`\`\`js
+${fs.readFileSync('./docs/custom/examples/ping_pong.js').toString('utf-8')}
+\`\`\``,
+};
diff --git a/docs/custom/updating.js b/docs/custom/updating.js
new file mode 100644
index 000000000..1615d95cd
--- /dev/null
+++ b/docs/custom/updating.js
@@ -0,0 +1,7 @@
+const fs = require('fs');
+
+module.exports = {
+ category: 'General',
+ name: 'Updating your code',
+ data: fs.readFileSync('./docs/custom/documents/updating.md').toString('utf-8'),
+};
diff --git a/docs/custom/welcome.js b/docs/custom/welcome.js
new file mode 100644
index 000000000..809093e7e
--- /dev/null
+++ b/docs/custom/welcome.js
@@ -0,0 +1,7 @@
+const fs = require('fs');
+
+module.exports = {
+ category: 'General',
+ name: 'Welcome',
+ data: fs.readFileSync('./docs/custom/documents/welcome.md').toString('utf-8'),
+};
diff --git a/docs/docs.json b/docs/docs.json
new file mode 100644
index 000000000..7d9465f5b
--- /dev/null
+++ b/docs/docs.json
@@ -0,0 +1 @@
+{"meta":{"version":12,"date":1472989062686},"classes":[{"id":"Client","name":"Client","description":"The starting point for making a Discord Bot.","meta":{"line":17,"file":"Client.js","path":"src/client"},"extends":["EventEmitter"],"classConstructor":{"id":"Client()","name":"Client","memberof":"Client","params":[{"name":"options","description":"Options for the client","optional":true,"type":{"types":[[["ClientOptions",""]]]}}]},"methods":[{"id":"Client#login","name":"login","description":"Logs the client in. If successful, resolves with the account's token. If you're making a bot, it's\nmuch better to use a bot account rather than a user account.\nBot accounts have higher rate limits and have access to some features user accounts don't have. User bots\nthat are making a lot of API requests can even be banned.","memberof":"Client","examples":["// log the client in using a token\nconst token = 'my token';\nclient.login(token);","// log the client in using email and password\nconst email = 'user@email.com';\nconst password = 'supersecret123';\nclient.login(email, password);"],"meta":{"line":129,"file":"Client.js","path":"src/client"},"returns":{"types":[[["Promise",".<"],["string",">"]]]},"params":[{"name":"emailOrToken","description":"The email or token used for the account. If it is an email, a password _must_ be\nprovided.","type":{"types":[[["string",""]]]}},{"name":"password","description":"The password for the account, only needed if an email was provided.","optional":true,"type":{"types":[[["string",""]]]}}]},{"id":"Client#destroy","name":"destroy","description":"Destroys the client and logs out.","memberof":"Client","meta":{"line":138,"file":"Client.js","path":"src/client"},"returns":{"types":[[["Promise",""]]]},"params":[]},{"id":"Client#syncGuilds","name":"syncGuilds","description":"This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however\nif you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.","memberof":"Client","meta":{"line":174,"file":"Client.js","path":"src/client"},"returns":{"types":[[["null",""]]]},"params":[{"name":"guilds","description":"An array of guilds to sync","optional":true,"type":{"types":[[["Array",".<"],["Guild",">"]]]}}]},{"id":"Client#fetchUser","name":"fetchUser","description":"Caches a user, or obtains it from the cache if it's already cached.\nIf the user isn't already cached, it will only be obtainable by OAuth bot accounts.","memberof":"Client","meta":{"line":189,"file":"Client.js","path":"src/client"},"returns":{"types":[[["Promise",".<"],["User",">"]]]},"params":[{"name":"id","description":"The ID of the user to obtain","type":{"types":[[["string",""]]]}}]}],"properties":[{"id":"Client#users","name":"users","description":"A Collection of the Client's stored users","memberof":"Client","type":{"types":[[["Collection",".<"],["string",", "],["User",">"]]]},"meta":{"line":70,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#guilds","name":"guilds","description":"A Collection of the Client's stored guilds","memberof":"Client","type":{"types":[[["Collection",".<"],["string",", "],["Guild",">"]]]},"meta":{"line":75,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#channels","name":"channels","description":"A Collection of the Client's stored channels","memberof":"Client","type":{"types":[[["Collection",".<"],["string",", "],["Channel",">"]]]},"meta":{"line":80,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#token","name":"token","description":"The authorization token for the logged in user/bot.","memberof":"Client","type":{"types":[[["string",""]]]},"meta":{"line":85,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#user","name":"user","description":"The ClientUser representing the logged in Client","memberof":"Client","type":{"types":[[["ClientUser",""]]]},"meta":{"line":90,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#email","name":"email","description":"The email, if there is one, for the logged in Client","memberof":"Client","type":{"types":[[["string",""]]]},"meta":{"line":95,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#password","name":"password","description":"The password, if there is one, for the logged in Client","memberof":"Client","type":{"types":[[["string",""]]]},"meta":{"line":100,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#readyTime","name":"readyTime","description":"The date at which the Client was regarded as being in the `READY` state.","memberof":"Client","type":{"types":[[["Date",""]]]},"meta":{"line":105,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#voiceConnections","name":"voiceConnections","description":"Returns a Collection, mapping Guild ID to Voice Connections.","memberof":"Client","type":{"types":[[["Collection",".<"],["string",", "],["VoiceConnection",">"]]]},"meta":{"line":199,"file":"Client.js","path":"src/client"},"props":[]},{"id":"Client#uptime","name":"uptime","description":"The uptime for the logged in Client.","memberof":"Client","type":{"types":[[["number",""]]]},"meta":{"line":208,"file":"Client.js","path":"src/client"},"props":[]}],"events":[{"id":"Client#event:channelUpdate","name":"channelUpdate","description":"Emitted whenever a channel is updated - e.g. name change, topic change.","memberof":"Client","meta":{"line":27,"file":"ChannelUpdate.js","path":"src/client/actions"},"params":[{"name":"oldChannel","description":"The channel before the update","type":{"types":[[["Channel",""]]]}},{"name":"newChannel","description":"The channel after the update","type":{"types":[[["Channel",""]]]}}]},{"id":"Client#event:guildUnavailable","name":"guildUnavailable","description":"Emitted whenever a guild becomes unavailable, likely due to a server outage.","memberof":"Client","meta":{"line":45,"file":"GuildDelete.js","path":"src/client/actions"},"params":[{"name":"guild","description":"The guild that has become unavailable.","type":{"types":[[["Guild",""]]]}}]},{"id":"Client#event:guildMemberRemove","name":"guildMemberRemove","description":"Emitted whenever a member leaves a guild, or is kicked.","memberof":"Client","meta":{"line":43,"file":"GuildMemberRemove.js","path":"src/client/actions"},"params":[{"name":"guild","description":"The guild that the member has left.","type":{"types":[[["Guild",""]]]}},{"name":"member","description":"The member that has left the guild.","type":{"types":[[["GuildMember",""]]]}}]},{"id":"Client#event:guildRoleCreate","name":"guildRoleCreate","description":"Emitted whenever a guild role is created.","memberof":"Client","meta":{"line":26,"file":"GuildRoleCreate.js","path":"src/client/actions"},"params":[{"name":"guild","description":"The guild that the role was created in.","type":{"types":[[["Guild",""]]]}},{"name":"role","description":"The role that was created.","type":{"types":[[["Role",""]]]}}]},{"id":"Client#event:guildRoleDelete","name":"guildRoleDelete","description":"Emitted whenever a guild role is deleted.","memberof":"Client","meta":{"line":40,"file":"GuildRoleDelete.js","path":"src/client/actions"},"params":[{"name":"guild","description":"The guild that the role was deleted in.","type":{"types":[[["Guild",""]]]}},{"name":"role","description":"The role that was deleted.","type":{"types":[[["Role",""]]]}}]},{"id":"Client#event:guildRoleUpdated","name":"guildRoleUpdated","description":"Emitted whenever a guild role is updated.","memberof":"Client","meta":{"line":34,"file":"GuildRoleUpdate.js","path":"src/client/actions"},"params":[{"name":"guild","description":"The guild that the role was updated in.","type":{"types":[[["Guild",""]]]}},{"name":"oldRole","description":"The role before the update.","type":{"types":[[["Role",""]]]}},{"name":"newRole","description":"The role after the update.","type":{"types":[[["Role",""]]]}}]},{"id":"Client#event:guildUpdate","name":"guildUpdate","description":"Emitted whenever a guild is updated - e.g. name change.","memberof":"Client","meta":{"line":27,"file":"GuildUpdate.js","path":"src/client/actions"},"params":[{"name":"oldGuild","description":"The guild before the update.","type":{"types":[[["Guild",""]]]}},{"name":"newGuild","description":"The guild after the update.","type":{"types":[[["Guild",""]]]}}]},{"id":"Client#event:messageUpdate","name":"messageUpdate","description":"Emitted whenever a message is updated - e.g. embed or content change.","memberof":"Client","meta":{"line":35,"file":"MessageUpdate.js","path":"src/client/actions"},"params":[{"name":"oldMessage","description":"The message before the update.","type":{"types":[[["Message",""]]]}},{"name":"newMessage","description":"The message after the update.","type":{"types":[[["Message",""]]]}}]},{"id":"Client#event:userUpdate","name":"userUpdate","description":"Emitted whenever a detail of the logged in User changes - e.g. username.","memberof":"Client","meta":{"line":33,"file":"UserUpdate.js","path":"src/client/actions"},"params":[{"name":"oldClientUser","description":"The client user before the update.","type":{"types":[[["ClientUser",""]]]}},{"name":"newClientUser","description":"The client user after the update.","type":{"types":[[["ClientUser",""]]]}}]},{"id":"Client#event:guildCreate","name":"guildCreate","description":"Emitted whenever the client joins a Guild.","memberof":"Client","meta":{"line":25,"file":"ClientDataManager.js","path":"src/client"},"params":[{"name":"guild","description":"The created guild","type":{"types":[[["Guild",""]]]}}]},{"id":"Client#event:channelCreate","name":"channelCreate","description":"Emitted whenever a Channel is created.","memberof":"Client","meta":{"line":14,"file":"ChannelCreate.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"channel","description":"The channel that was created","type":{"types":[[["Channel",""]]]}}]},{"id":"Client#event:channelDelete","name":"channelDelete","description":"Emitted whenever a Channel is deleted.","memberof":"Client","meta":{"line":14,"file":"ChannelDelete.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"channel","description":"The channel that was deleted","type":{"types":[[["Channel",""]]]}}]},{"id":"Client#event:channelPinsUpdate","name":"channelPinsUpdate","description":"Emitted whenever the pins of a Channel are updated. Due to the nature of the WebSocket event, not much information\ncan be provided easily here - you need to manually check the pins yourself.","memberof":"Client","meta":{"line":23,"file":"ChannelPinsUpdate.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"channel","description":"The channel that the pins update occured in","type":{"types":[[["Channel",""]]]}},{"name":"time","description":"The time of the pins update","type":{"types":[[["Date",""]]]}}]},{"id":"Client#event:guildBanAdd","name":"guildBanAdd","description":"Emitted whenever a member is banned from a guild.","memberof":"Client","meta":{"line":16,"file":"GuildBanAdd.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"guild","description":"The guild that the ban occurred in","type":{"types":[[["Guild",""]]]}},{"name":"user","description":"The user that was banned","type":{"types":[[["User",""]]]}}]},{"id":"Client#event:guildBanRemove","name":"guildBanRemove","description":"Emitted whenever a member is unbanned from a guild.","memberof":"Client","meta":{"line":13,"file":"GuildBanRemove.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"guild","description":"The guild that the unban occurred in","type":{"types":[[["Guild",""]]]}},{"name":"user","description":"The user that was unbanned","type":{"types":[[["User",""]]]}}]},{"id":"Client#event:guildDelete","name":"guildDelete","description":"Emitted whenever a Guild is deleted/left.","memberof":"Client","meta":{"line":13,"file":"GuildDelete.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"guild","description":"The guild that was deleted","type":{"types":[[["Guild",""]]]}}]},{"id":"Client#event:guildMembersChunk","name":"guildMembersChunk","description":"Emitted whenever a chunk of Guild members is received","memberof":"Client","meta":{"line":22,"file":"GuildMembersChunk.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"guild","description":"The guild that the chunks relate to","type":{"types":[[["Guild",""]]]}},{"name":"members","description":"The members in the chunk","type":{"types":[[["Array",".<"],["GuildMember",">"]]]}}]},{"id":"Client#event:message","name":"message","description":"Emitted whenever a message is created","memberof":"Client","meta":{"line":13,"file":"MessageCreate.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"message","description":"The created message","type":{"types":[[["Message",""]]]}}]},{"id":"Client#event:messageDelete","name":"messageDelete","description":"Emitted whenever a message is deleted","memberof":"Client","meta":{"line":13,"file":"MessageDelete.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"message","description":"The deleted message","type":{"types":[[["Message",""]]]}}]},{"id":"Client#event:messageDeleteBulk","name":"messageDeleteBulk","description":"Emitted whenever messages are deleted in bulk","memberof":"Client","meta":{"line":11,"file":"MessageDeleteBulk.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"messages","description":"The deleted messages, mapped by their ID","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]}}]},{"id":"Client#event:presenceUpdate","name":"presenceUpdate","description":"Emitted whenever a user changes one of their details or starts/stop playing a game","memberof":"Client","meta":{"line":57,"file":"PresenceUpdate.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"oldUser","description":"The user before the presence update","type":{"types":[[["User",""]]]}},{"name":"newUser","description":"The user after the presence update","type":{"types":[[["User",""]]]}}]},{"id":"Client#event:guildMemberAvailable","name":"guildMemberAvailable","description":"Emitted whenever a member becomes available in a large Guild","memberof":"Client","meta":{"line":64,"file":"PresenceUpdate.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"guild","description":"The guild that the member became available in","type":{"types":[[["Guild",""]]]}},{"name":"member","description":"The member that became available","type":{"types":[[["GuildMember",""]]]}}]},{"id":"Client#event:typingStart","name":"typingStart","description":"Emitted whenever a user starts typing in a channel","memberof":"Client","meta":{"line":50,"file":"TypingStart.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"channel","description":"The channel the user started typing in","type":{"types":[[["Channel",""]]]}},{"name":"user","description":"The user that started typing","type":{"types":[[["User",""]]]}}]},{"id":"Client#event:typingStop","name":"typingStop","description":"Emitted whenever a user stops typing in a channel","memberof":"Client","meta":{"line":57,"file":"TypingStart.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"channel","description":"The channel the user stopped typing in","type":{"types":[[["Channel",""]]]}},{"name":"user","description":"The user that stopped typing","type":{"types":[[["User",""]]]}}]},{"id":"Client#event:voiceStateUpdate","name":"voiceStateUpdate","description":"Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes.","memberof":"Client","meta":{"line":42,"file":"VoiceStateUpdate.js","path":"src/client/websocket/packets/handlers"},"params":[{"name":"oldMember","description":"The member before the voice state update","type":{"types":[[["GuildMember",""]]]}},{"name":"newMember","description":"The member after the voice state update","type":{"types":[[["GuildMember",""]]]}}]},{"id":"Client#event:error","name":"error","description":"Emitted whenever the Client encounters a serious connection error","memberof":"Client","meta":{"line":184,"file":"WebSocketManager.js","path":"src/client/websocket"},"params":[{"name":"error","description":"The encountered error","type":{"types":[[["Error",""]]]}}]},{"id":"Client#event:ready","name":"ready","description":"Emitted when the Client becomes ready to start working","memberof":"Client","meta":{"line":194,"file":"WebSocketManager.js","path":"src/client/websocket"},"params":[]},{"id":"Client#event:reconnecting","name":"reconnecting","description":"Emitted when the Client tries to reconnect after being disconnected","memberof":"Client","meta":{"line":236,"file":"WebSocketManager.js","path":"src/client/websocket"},"params":[]},{"id":"Client#event:guildMemberAdd","name":"guildMemberAdd","description":"Emitted whenever a user joins a guild.","memberof":"Client","meta":{"line":88,"file":"Guild.js","path":"src/structures"},"params":[{"name":"guild","description":"The guild that the user has joined","type":{"types":[[["Guild",""]]]}},{"name":"member","description":"The member that has joined","type":{"types":[[["GuildMember",""]]]}}]},{"id":"Client#event:guildMemberUpdate","name":"guildMemberUpdate","description":"Emitted whenever a Guild Member changes - i.e. new role, removed role, nickname","memberof":"Client","meta":{"line":111,"file":"Guild.js","path":"src/structures"},"params":[{"name":"guild","description":"The guild that the update affects","type":{"types":[[["Guild",""]]]}},{"name":"oldMember","description":"The member before the update","type":{"types":[[["GuildMember",""]]]}},{"name":"newMember","description":"The member after the update","type":{"types":[[["GuildMember",""]]]}}]},{"id":"Client#event:guildMemberSpeaking","name":"guildMemberSpeaking","description":"Emitted once a Guild Member starts/stops speaking","memberof":"Client","meta":{"line":196,"file":"Guild.js","path":"src/structures"},"params":[{"name":"member","description":"The member that started/stopped speaking","type":{"types":[[["GuildMember",""]]]}},{"name":"speaking","description":"Whether or not the member is speaking","type":{"types":[[["boolean",""]]]}}]}]},{"id":"StreamDispatcher","name":"StreamDispatcher","description":"The class that sends voice packet data to the voice connection.\n```js\n// obtained using:\nvoiceChannel.join().then(connection => {\n // you can play a file or a stream here:\n connection.playFile('./file.mp3').then(dispatcher => {\n\n });\n});\n```","meta":{"line":20,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"extends":["EventEmitter"],"methods":[{"id":"StreamDispatcher#end","name":"end","description":"Stops the current stream permanently and emits an `end` event.","memberof":"StreamDispatcher","meta":{"line":208,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"returns":{"types":[[["null",""]]]},"params":[]},{"id":"StreamDispatcher#setVolume","name":"setVolume","description":"Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.","memberof":"StreamDispatcher","meta":{"line":225,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"returns":{"types":[[["null",""]]]},"params":[{"name":"volume","description":"The volume that you want to set","type":{"types":[[["number",""]]]}}]},{"id":"StreamDispatcher#setVolumeDecibels","name":"setVolumeDecibels","description":"Set the volume in decibels","memberof":"StreamDispatcher","meta":{"line":233,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"returns":{"types":[[["null",""]]]},"params":[{"name":"db","description":"The decibels","type":{"types":[[["number",""]]]}}]},{"id":"StreamDispatcher#setVolumeLogarithmic","name":"setVolumeLogarithmic","description":"Set the volume so that a perceived value of 0.5 is half the perceived volume etc.","memberof":"StreamDispatcher","meta":{"line":241,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"returns":{"types":[[["null",""]]]},"params":[{"name":"value","description":"The value for the volume","type":{"types":[[["number",""]]]}}]},{"id":"StreamDispatcher#pause","name":"pause","description":"Stops sending voice packets to the voice connection (stream may still progress however)","memberof":"StreamDispatcher","meta":{"line":248,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"returns":{"types":[[["null",""]]]},"params":[]},{"id":"StreamDispatcher#resume","name":"resume","description":"Resumes sending voice packets to the voice connection (may be further on in the stream than when paused)","memberof":"StreamDispatcher","meta":{"line":255,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"returns":{"types":[[["null",""]]]},"params":[]}],"properties":[{"id":"StreamDispatcher#volume","name":"volume","description":"The volume of the stream, relative to the stream's input volume","memberof":"StreamDispatcher","type":{"types":[[["number",""]]]},"meta":{"line":217,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"props":[]}],"events":[{"id":"StreamDispatcher#event:speaking","name":"speaking","description":"Emitted when the dispatcher starts/stops speaking","memberof":"StreamDispatcher","meta":{"line":36,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"params":[{"name":"value","description":"Whether or not the dispatcher is speaking","type":{"types":[[["boolean",""]]]}}]},{"id":"StreamDispatcher#event:end","name":"end","description":"Emitted once the stream has ended. Attach a `once` listener to this.","memberof":"StreamDispatcher","meta":{"line":136,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"params":[]},{"id":"StreamDispatcher#event:error","name":"error","description":"Emitted once the stream has encountered an error. Attach a `once` listener to this. Also emits `end`.","memberof":"StreamDispatcher","meta":{"line":144,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"params":[{"name":"err","description":"The encountered error","type":{"types":[[["Error",""]]]}}]},{"id":"StreamDispatcher#event:debug","name":"debug","description":"Emitted when the stream wants to give debug information.","memberof":"StreamDispatcher","meta":{"line":157,"file":"StreamDispatcher.js","path":"src/client/voice/dispatcher"},"params":[{"name":"information","description":"The debug information","type":{"types":[[["string",""]]]}}]}]},{"id":"VoiceReceiver","name":"VoiceReceiver","description":"Receives voice data from a voice connection.\n```js\n// obtained using:\nvoiceChannel.join().then(connection => {\n const receiver = connection.createReceiver();\n});\n```","meta":{"line":18,"file":"VoiceReceiver.js","path":"src/client/voice/receiver"},"extends":["EventEmitter"],"methods":[{"id":"VoiceReceiver#createOpusStream","name":"createOpusStream","description":"Creates a readable stream for a user that provides opus data while the user is speaking. When the user\nstops speaking, the stream is destroyed.","memberof":"VoiceReceiver","meta":{"line":59,"file":"VoiceReceiver.js","path":"src/client/voice/receiver"},"returns":{"types":[[["ReadableStream",""]]]},"params":[{"name":"user","description":"The user to create the stream for","type":{"types":[[["UserResolvable",""]]]}}]},{"id":"VoiceReceiver#createPCMStream","name":"createPCMStream","description":"Creates a readable stream for a user that provides PCM data while the user is speaking. When the user\nstops speaking, the stream is destroyed. The stream is 16-bit signed stereo PCM at 48KHz.","memberof":"VoiceReceiver","meta":{"line":78,"file":"VoiceReceiver.js","path":"src/client/voice/receiver"},"returns":{"types":[[["ReadableStream",""]]]},"params":[{"name":"user","description":"The user to create the stream for","type":{"types":[[["UserResolvable",""]]]}}]}],"properties":[{"id":"VoiceReceiver#connection","name":"connection","description":"The VoiceConnection that instantiated this","memberof":"VoiceReceiver","type":{"types":[[["VoiceConnection",""]]]},"meta":{"line":32,"file":"VoiceReceiver.js","path":"src/client/voice/receiver"},"props":[]}],"events":[{"id":"VoiceReceiver#event:warn","name":"warn","description":"Emitted whenever a voice packet cannot be decrypted","memberof":"VoiceReceiver","meta":{"line":91,"file":"VoiceReceiver.js","path":"src/client/voice/receiver"},"params":[{"name":"message","description":"The warning message","type":{"types":[[["string",""]]]}}]},{"id":"VoiceReceiver#event:opus","name":"opus","description":"Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM).","memberof":"VoiceReceiver","meta":{"line":101,"file":"VoiceReceiver.js","path":"src/client/voice/receiver"},"params":[{"name":"user","description":"The user that is sending the buffer (is speaking)","type":{"types":[[["User",""]]]}},{"name":"buffer","description":"The opus buffer","type":{"types":[[["Buffer",""]]]}}]},{"id":"VoiceReceiver#event:pcm","name":"pcm","description":"Emits decoded voice data when it's received. For performance reasons, the decoding will only\nhappen if there is at least one `pcm` listener on this receiver.","memberof":"VoiceReceiver","meta":{"line":109,"file":"VoiceReceiver.js","path":"src/client/voice/receiver"},"params":[{"name":"user","description":"The user that is sending the buffer (is speaking)","type":{"types":[[["User",""]]]}},{"name":"buffer","description":"The decoded buffer","type":{"types":[[["Buffer",""]]]}}]}]},{"id":"VoiceConnection","name":"VoiceConnection","description":"Represents a connection to a Voice Channel in Discord.\n```js\n// obtained using:\nvoiceChannel.join().then(connection => {\n\n});\n```","meta":{"line":18,"file":"VoiceConnection.js","path":"src/client/voice"},"extends":["EventEmitter"],"methods":[{"id":"VoiceConnection#disconnect","name":"disconnect","description":"Disconnects the Client from the Voice Channel","memberof":"VoiceConnection","meta":{"line":91,"file":"VoiceConnection.js","path":"src/client/voice"},"returns":{"types":[[["null",""]]]},"params":[{"name":"reason","description":"The reason of the disconnection","optional":true,"type":{"types":[[["string",""]]]}}]},{"id":"VoiceConnection#playFile","name":"playFile","description":"Play the given file in the voice connection","memberof":"VoiceConnection","examples":["// play files natively\nvoiceChannel.join()\n .then(connection => {\n const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');\n })\n .catch(console.log);"],"meta":{"line":215,"file":"VoiceConnection.js","path":"src/client/voice"},"returns":{"types":[[["StreamDispatcher",""]]]},"params":[{"name":"file","description":"The path to the file","type":{"types":[[["string",""]]]}}]},{"id":"VoiceConnection#playStream","name":"playStream","description":"Plays and converts an audio stream in the voice connection","memberof":"VoiceConnection","examples":["// play streams using ytdl-core\nconst ytdl = require('ytdl-core');\nvoiceChannel.join()\n .then(connection => {\n const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', {filter : 'audioonly'});\n const dispatcher = connection.playStream(stream);\n })\n .catch(console.log);"],"meta":{"line":233,"file":"VoiceConnection.js","path":"src/client/voice"},"returns":{"types":[[["StreamDispatcher",""]]]},"params":[{"name":"stream","description":"The audio stream to play","type":{"types":[[["ReadableStream",""]]]}}]},{"id":"VoiceConnection#playConvertedStream","name":"playConvertedStream","description":"Plays a stream of 16-bit signed stereo PCM at 48KHz.","memberof":"VoiceConnection","meta":{"line":242,"file":"VoiceConnection.js","path":"src/client/voice"},"returns":{"types":[[["StreamDispatcher",""]]]},"params":[{"name":"stream","description":"The audio stream to play.","type":{"types":[[["ReadableStream",""]]]}}]},{"id":"VoiceConnection#createReceiver","name":"createReceiver","description":"Creates a VoiceReceiver so you can start listening to voice data. It's recommended to only create one of these.","memberof":"VoiceConnection","meta":{"line":252,"file":"VoiceConnection.js","path":"src/client/voice"},"returns":{"types":[[["VoiceReceiver",""]]]},"params":[]}],"properties":[{"id":"VoiceConnection#player","name":"player","description":"The player","memberof":"VoiceConnection","type":{"types":[[["BasePlayer",""]]]},"meta":{"line":31,"file":"VoiceConnection.js","path":"src/client/voice"},"props":[]},{"id":"VoiceConnection#endpoint","name":"endpoint","description":"The endpoint of the connection","memberof":"VoiceConnection","type":{"types":[[["string",""]]]},"meta":{"line":36,"file":"VoiceConnection.js","path":"src/client/voice"},"props":[]},{"id":"VoiceConnection#channel","name":"channel","description":"The VoiceChannel for this connection","memberof":"VoiceConnection","type":{"types":[[["VoiceChannel",""]]]},"meta":{"line":41,"file":"VoiceConnection.js","path":"src/client/voice"},"props":[]},{"id":"VoiceConnection#ready","name":"ready","description":"Whether or not the connection is ready","memberof":"VoiceConnection","type":{"types":[[["boolean",""]]]},"meta":{"line":52,"file":"VoiceConnection.js","path":"src/client/voice"},"props":[]}],"events":[{"id":"VoiceConnection#event:error","name":"error","description":"Emitted whenever the connection encounters a fatal error.","memberof":"VoiceConnection","meta":{"line":78,"file":"VoiceConnection.js","path":"src/client/voice"},"params":[{"name":"error","description":"The encountered error","type":{"types":[[["Error",""]]]}}]},{"id":"VoiceConnection#event:disconnected","name":"disconnected","description":"Emit once the voice connection has disconnected.","memberof":"VoiceConnection","meta":{"line":115,"file":"VoiceConnection.js","path":"src/client/voice"},"params":[{"name":"error","description":"The encountered error, if any","type":{"types":[[["Error",""]]]}}]},{"id":"VoiceConnection#event:ready","name":"ready","description":"Emitted once the connection is ready (joining voice channels resolves when the connection is ready anyway)","memberof":"VoiceConnection","meta":{"line":139,"file":"VoiceConnection.js","path":"src/client/voice"},"params":[]},{"id":"VoiceConnection#event:speaking","name":"speaking","description":"Emitted whenever a user starts/stops speaking","memberof":"VoiceConnection","meta":{"line":191,"file":"VoiceConnection.js","path":"src/client/voice"},"params":[{"name":"user","description":"The user that has started/stopped speaking","type":{"types":[[["User",""]]]}},{"name":"speaking","description":"Whether or not the user is speaking","type":{"types":[[["boolean",""]]]}}]}]},{"id":"Shard","name":"Shard","description":"Represents a Shard spawned by the ShardingManager.","meta":{"line":7,"file":"Shard.js","path":"src/sharding"},"methods":[],"properties":[{"id":"Shard#manager","name":"manager","description":"The manager of the spawned shard","memberof":"Shard","type":{"types":[[["ShardingManager",""]]]},"meta":{"line":13,"file":"Shard.js","path":"src/sharding"},"props":[]},{"id":"Shard#id","name":"id","description":"The shard id","memberof":"Shard","type":{"types":[[["number",""]]]},"meta":{"line":18,"file":"Shard.js","path":"src/sharding"},"props":[]},{"id":"Shard#process","name":"process","description":"The process of the shard","memberof":"Shard","type":{"types":[[["process",""]]]},"meta":{"line":23,"file":"Shard.js","path":"src/sharding"},"props":[]}],"events":[]},{"id":"ShardingManager","name":"ShardingManager","description":"This is a utility class that can be used to help you spawn shards of your Client. Each shard is completely separate\nfrom the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.\nThe Sharding Manager is still experimental","meta":{"line":12,"file":"ShardingManager.js","path":"src/sharding"},"extends":["EventEmitter"],"classConstructor":{"id":"ShardingManager()","name":"ShardingManager","description":"Creates an instance of ShardingManager.","memberof":"ShardingManager","params":[{"name":"file","description":"the path to your file","type":{"types":[[["string",""]]]}},{"name":"totalShards","description":"the number of shards you would like to spawn","type":{"types":[[["number",""]]]}}]},"methods":[],"properties":[{"id":"ShardingManager#totalShards","name":"totalShards","description":"The amount of shards that this manager is going to spawn","memberof":"ShardingManager","type":{"types":[[["number",""]]]},"meta":{"line":28,"file":"ShardingManager.js","path":"src/sharding"},"props":[]},{"id":"ShardingManager#shards","name":"shards","description":"A collection of shards that this manager has spawned.","memberof":"ShardingManager","type":{"types":[[["Collection",".<"],["number",", "],["Shard",">"]]]},"meta":{"line":33,"file":"ShardingManager.js","path":"src/sharding"},"props":[]}],"events":[]},{"id":"Channel","name":"Channel","description":"Represents any Channel on Discord","meta":{"line":4,"file":"Channel.js","path":"src/structures"},"methods":[{"id":"Channel#delete","name":"delete","description":"Deletes the channel","memberof":"Channel","examples":["// delete the channel\nchannel.delete()\n .then() // success\n .catch(console.log); // log error"],"meta":{"line":43,"file":"Channel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Channel",">"]]]},"params":[]}],"properties":[{"id":"Channel#client","name":"client","description":"The client that instantiated the Channel","memberof":"Channel","type":{"types":[[["Client",""]]]},"meta":{"line":10,"file":"Channel.js","path":"src/structures"},"props":[]},{"id":"Channel#type","name":"type","description":"The type of the channel, either:\n* `dm` - a DM channel\n* `group` - a Group DM channel\n* `text` - a guild text channel\n* `voice` - a guild voice channel","memberof":"Channel","type":{"types":[[["string",""]]]},"meta":{"line":22,"file":"Channel.js","path":"src/structures"},"props":[]},{"id":"Channel#id","name":"id","description":"The unique ID of the channel","memberof":"Channel","type":{"types":[[["string",""]]]},"meta":{"line":31,"file":"Channel.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"ClientUser","name":"ClientUser","description":"Represents the logged in client's Discord User","meta":{"line":7,"file":"ClientUser.js","path":"src/structures"},"extends":["User"],"methods":[{"id":"ClientUser#setUsername","name":"setUsername","description":"Set the username of the logged in Client.\nChanging usernames in Discord is heavily rate limited, with only 2 requests\nevery hour. Use this sparingly!","memberof":"ClientUser","examples":["// set username\nclient.user.setUsername('discordjs')\n .then(user => console.log(`My new username is ${user.username}`))\n .catch(console.log);"],"meta":{"line":36,"file":"ClientUser.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["ClientUser",">"]]]},"params":[{"name":"username","description":"The new username","type":{"types":[[["string",""]]]}}]},{"id":"ClientUser#setEmail","name":"setEmail","description":"If this user is a \"self bot\" or logged in using a normal user's details (which should be avoided), you can set the\nemail here.","memberof":"ClientUser","examples":["// set email\nclient.user.setEmail('bob@gmail.com')\n .then(user => console.log(`My new email is ${user.email}`))\n .catch(console.log);"],"meta":{"line":51,"file":"ClientUser.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["ClientUser",">"]]]},"params":[{"name":"email","description":"The new email","type":{"types":[[["string",""]]]}}]},{"id":"ClientUser#setPassword","name":"setPassword","description":"If this user is a \"self bot\" or logged in using a normal user's details (which should be avoided), you can set the\npassword here.","memberof":"ClientUser","examples":["// set password\nclient.user.setPassword('password')\n .then(user => console.log('New password set!'))\n .catch(console.log);"],"meta":{"line":66,"file":"ClientUser.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["ClientUser",">"]]]},"params":[{"name":"password","description":"The new password","type":{"types":[[["string",""]]]}}]},{"id":"ClientUser#setAvatar","name":"setAvatar","description":"Set the avatar of the logged in Client.","memberof":"ClientUser","examples":["// set avatar\nclient.user.setAvatar(fs.readFileSync('./avatar.png'))\n .then(user => console.log(`New avatar set!`))\n .catch(console.log);"],"meta":{"line":80,"file":"ClientUser.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["ClientUser",">"]]]},"params":[{"name":"avatar","description":"The new avatar","type":{"types":[[["Base64Resolvable",""]]]}}]},{"id":"ClientUser#setStatus","name":"setStatus","description":"Set the status and playing game of the logged in client.","memberof":"ClientUser","examples":["// set status\nclient.user.setStatus('status', 'game')\n .then(user => console.log('Changed status!'))\n .catch(console.log);"],"meta":{"line":95,"file":"ClientUser.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["ClientUser",">"]]]},"params":[{"name":"status","description":"The status, can be `online` or `idle`","optional":true,"type":{"types":[[["string",""]]]}},{"name":"game","description":"The game that is being played","optional":true,"type":{"types":[[["string",""]],[["Object",""]]]}}]},{"id":"ClientUser#toString","name":"toString","description":"When concatenated with a string, this automatically concatenates the User's mention instead of the User object.","memberof":"ClientUser","examples":["// logs: Hello from <@123456789>!\nconsole.log(`Hello from ${user}!`);"],"inherits":"User#toString","inherited":true,"meta":{"line":63,"file":"User.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]},{"id":"ClientUser#deleteDM","name":"deleteDM","description":"Deletes a DM Channel (if one exists) between the Client and the User. Resolves with the Channel if successful.","memberof":"ClientUser","inherits":"User#deleteDM","inherited":true,"meta":{"line":81,"file":"User.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["DMChannel",">"]]]},"params":[]},{"id":"ClientUser#equals","name":"equals","description":"Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.\nIt is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.","memberof":"ClientUser","inherits":"User#equals","inherited":true,"meta":{"line":91,"file":"User.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"user","description":"The user to compare","type":{"types":[[["User",""]]]}}]},{"id":"ClientUser#sendMessage","name":"sendMessage","description":"Send a message to this channel","memberof":"ClientUser","examples":["// send a message\nchannel.sendMessage('hello!')\n .then(message => console.log(`Sent message: ${message.content}`))\n .catch(console.log);"],"inherits":"User#sendMessage","inherited":true,"meta":{"line":53,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendMessage"]},{"id":"ClientUser#sendTTSMessage","name":"sendTTSMessage","description":"Send a text-to-speech message to this channel","memberof":"ClientUser","examples":["// send a TTS message\nchannel.sendTTSMessage('hello!')\n .then(message => console.log(`Sent tts message: ${message.content}`))\n .catch(console.log);"],"inherits":"User#sendTTSMessage","inherited":true,"meta":{"line":68,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendTTSMessage"]},{"id":"ClientUser#sendFile","name":"sendFile","description":"Send a file to this channel","memberof":"ClientUser","inherits":"User#sendFile","inherited":true,"meta":{"line":78,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"attachment","description":"The file to send","type":{"types":[[["FileResolvable",""]]]}},{"name":"fileName","description":"The name and extension of the file","optional":true,"type":{"types":[[["string",""]]]}}],"implements":["TextBasedChannel#sendFile"]}],"properties":[{"id":"ClientUser#verified","name":"verified","description":"Whether or not this account has been verified","memberof":"ClientUser","type":{"types":[[["boolean",""]]]},"meta":{"line":14,"file":"ClientUser.js","path":"src/structures"},"props":[]},{"id":"ClientUser#email","name":"email","description":"The email of this account","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":19,"file":"ClientUser.js","path":"src/structures"},"props":[]},{"id":"ClientUser#username","name":"username","description":"The username of the User","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":19,"file":"User.js","path":"src/structures"},"props":[]},{"id":"ClientUser#id","name":"id","description":"The ID of the User","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"User.js","path":"src/structures"},"props":[]},{"id":"ClientUser#discriminator","name":"discriminator","description":"A discriminator based on username for the User","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":29,"file":"User.js","path":"src/structures"},"props":[]},{"id":"ClientUser#avatar","name":"avatar","description":"The ID of the user's avatar","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":34,"file":"User.js","path":"src/structures"},"props":[]},{"id":"ClientUser#bot","name":"bot","description":"Whether or not the User is a Bot.","memberof":"ClientUser","type":{"types":[[["boolean",""]]]},"meta":{"line":39,"file":"User.js","path":"src/structures"},"props":[]},{"id":"ClientUser#status","name":"status","description":"The status of the user:\n\n* **`online`** - user is online\n* **`offline`** - user is offline\n* **`idle`** - user is AFK","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":48,"file":"User.js","path":"src/structures"},"props":[]},{"id":"ClientUser#game","name":"game","description":"The game that the user is playing, `null` if they aren't playing a game.","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":53,"file":"User.js","path":"src/structures"},"props":[]},{"id":"ClientUser#avatarURL","name":"avatarURL","description":"A link to the user's avatar (if they have one, otherwise null)","memberof":"ClientUser","type":{"types":[[["string",""]]]},"meta":{"line":72,"file":"User.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"DMChannel","name":"DMChannel","description":"Represents a Direct Message Channel between two users.","meta":{"line":11,"file":"DMChannel.js","path":"src/structures"},"extends":["Channel"],"methods":[{"id":"DMChannel#toString","name":"toString","description":"When concatenated with a string, this automatically concatenates the recipient's mention instead of the\nDM channel object.","memberof":"DMChannel","meta":{"line":38,"file":"DMChannel.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]},{"id":"DMChannel#bulkDelete","name":"bulkDelete","description":"Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after.","memberof":"DMChannel","meta":{"line":24,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"params":[{"name":"messages","description":"The messages to delete","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]],[["Array",".<"],["Message",">"]]]}}],"implements":["TextBasedChannel#bulkDelete"]},{"id":"DMChannel#sendMessage","name":"sendMessage","description":"Send a message to this channel","memberof":"DMChannel","examples":["// send a message\nchannel.sendMessage('hello!')\n .then(message => console.log(`Sent message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":53,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendMessage"]},{"id":"DMChannel#sendTTSMessage","name":"sendTTSMessage","description":"Send a text-to-speech message to this channel","memberof":"DMChannel","examples":["// send a TTS message\nchannel.sendTTSMessage('hello!')\n .then(message => console.log(`Sent tts message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":68,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendTTSMessage"]},{"id":"DMChannel#sendFile","name":"sendFile","description":"Send a file to this channel","memberof":"DMChannel","meta":{"line":78,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"attachment","description":"The file to send","type":{"types":[[["FileResolvable",""]]]}},{"name":"fileName","description":"The name and extension of the file","optional":true,"type":{"types":[[["string",""]]]}}],"implements":["TextBasedChannel#sendFile"]},{"id":"DMChannel#fetchMessages","name":"fetchMessages","description":"Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects.","memberof":"DMChannel","examples":["// get messages\nchannel.fetchMessages({limit: 10})\n .then(messages => console.log(`Received ${messages.size} messages`))\n .catch(console.log);"],"meta":{"line":122,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"options","description":"The query parameters to pass in","optional":true,"type":{"types":[[["ChannelLogsQueryOptions",""]]]}}],"implements":["TextBasedChannel#fetchMessages"]},{"id":"DMChannel#startTyping","name":"startTyping","description":"Starts a typing indicator in the channel.","memberof":"DMChannel","examples":["// start typing in a channel\nchannel.startTyping();"],"meta":{"line":143,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"count","description":"The number of times startTyping should be considered to have been called","optional":true,"type":{"types":[[["number",""]]]}}],"implements":["TextBasedChannel#startTyping"]},{"id":"DMChannel#stopTyping","name":"stopTyping","description":"Stops the typing indicator in the channel.\nThe indicator will only stop if this is called as many times as startTyping().\nIt can take a few seconds for the Client User to stop typing.","memberof":"DMChannel","examples":["// stop typing in a channel\nchannel.stopTyping();","// force typing to fully stop in a channel\nchannel.stopTyping(true);"],"meta":{"line":171,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"force","description":"Whether or not to reset the call count and force the indicator to stop","optional":true,"type":{"types":[[["boolean",""]]]}}],"implements":["TextBasedChannel#stopTyping"]},{"id":"DMChannel#createCollector","name":"createCollector","description":"Creates a Message Collector","memberof":"DMChannel","examples":["// create a message collector\nconst collector = channel.createCollector(\n m => m.content.includes('discord'),\n { time: 15000 }\n);\ncollector.on('message', m => console.log(`Collected ${m.content}`));\ncollector.on('end', collected => console.log(`Collected ${collected.size} items`));"],"meta":{"line":213,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["MessageCollector",""]]]},"params":[{"name":"filter","description":"The filter to create the collector with","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"The options to pass to the collector","optional":true,"type":{"types":[[["CollectorOptions",""]]]}}],"implements":["TextBasedChannel#createCollector"]},{"id":"DMChannel#awaitMessages","name":"awaitMessages","description":"Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified\nfilter.","memberof":"DMChannel","examples":["// await !vote messages\nconst filter = m => m.content.startsWith('!vote');\n// errors: ['time'] treats ending because of the time limit as an error\nchannel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] })\n .then(collected => console.log(collected.size))\n .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));"],"meta":{"line":241,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"filter","description":"The filter function to use","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"Optional options to pass to the internal collector","optional":true,"type":{"types":[[["AwaitMessagesOptions",""]]]}}],"implements":["TextBasedChannel#awaitMessages"]},{"id":"DMChannel#fetchPinnedMessages","name":"fetchPinnedMessages","description":"Fetches the pinned messages of this Channel and returns a Collection of them.","memberof":"DMChannel","meta":{"line":267,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[],"implements":["TextBasedChannel#fetchPinnedMessages"]},{"id":"DMChannel#delete","name":"delete","description":"Deletes the channel","memberof":"DMChannel","examples":["// delete the channel\nchannel.delete()\n .then() // success\n .catch(console.log); // log error"],"inherits":"Channel#delete","inherited":true,"meta":{"line":43,"file":"Channel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Channel",">"]]]},"params":[]}],"properties":[{"id":"DMChannel#recipient","name":"recipient","description":"The recipient on the other end of the DM","memberof":"DMChannel","type":{"types":[[["User",""]]]},"meta":{"line":24,"file":"DMChannel.js","path":"src/structures"},"props":[]},{"id":"DMChannel#lastMessageID","name":"lastMessageID","description":"The ID of the last sent message, if available","memberof":"DMChannel","type":{"types":[[["string",""]]]},"meta":{"line":29,"file":"DMChannel.js","path":"src/structures"},"props":[]},{"id":"DMChannel#messages","name":"messages","description":"A Collection containing the messages sent to this channel.","memberof":"DMChannel","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"meta":{"line":16,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"DMChannel#typing","name":"typing","description":"Whether or not the typing indicator is being shown in the channel.","memberof":"DMChannel","type":{"types":[[["boolean",""]]]},"meta":{"line":186,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"DMChannel#typingCount","name":"typingCount","description":"Number of times `startTyping` has been called.","memberof":"DMChannel","type":{"types":[[["number",""]]]},"meta":{"line":194,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"DMChannel#client","name":"client","description":"The client that instantiated the Channel","memberof":"DMChannel","type":{"types":[[["Client",""]]]},"meta":{"line":10,"file":"Channel.js","path":"src/structures"},"props":[]},{"id":"DMChannel#type","name":"type","description":"The type of the channel, either:\n* `dm` - a DM channel\n* `group` - a Group DM channel\n* `text` - a guild text channel\n* `voice` - a guild voice channel","memberof":"DMChannel","type":{"types":[[["string",""]]]},"meta":{"line":22,"file":"Channel.js","path":"src/structures"},"props":[]},{"id":"DMChannel#id","name":"id","description":"The unique ID of the channel","memberof":"DMChannel","type":{"types":[[["string",""]]]},"meta":{"line":31,"file":"Channel.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"Emoji","name":"Emoji","description":"Represents a Custom Emoji","meta":{"line":7,"file":"Emoji.js","path":"src/structures"},"methods":[{"id":"Emoji#toString","name":"toString","description":"When concatenated with a string, this automatically returns the emoji mention rather than the object.","memberof":"Emoji","examples":["// send an emoji:\nconst emoji = guild.emojis.array()[0];\nmsg.reply(`Hello! ${emoji}`);"],"meta":{"line":76,"file":"Emoji.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]}],"properties":[{"id":"Emoji#client","name":"client","description":"The Client that instantiated this object","memberof":"Emoji","type":{"types":[[["Client",""]]]},"meta":{"line":13,"file":"Emoji.js","path":"src/structures"},"props":[]},{"id":"Emoji#guild","name":"guild","description":"The Guild this emoji is part of","memberof":"Emoji","type":{"types":[[["Guild",""]]]},"meta":{"line":18,"file":"Emoji.js","path":"src/structures"},"props":[]},{"id":"Emoji#id","name":"id","description":"The ID of the Emoji","memberof":"Emoji","type":{"types":[[["string",""]]]},"meta":{"line":27,"file":"Emoji.js","path":"src/structures"},"props":[]},{"id":"Emoji#name","name":"name","description":"The name of the Emoji","memberof":"Emoji","type":{"types":[[["string",""]]]},"meta":{"line":32,"file":"Emoji.js","path":"src/structures"},"props":[]},{"id":"Emoji#requiresColons","name":"requiresColons","description":"Whether or not this emoji requires colons surrounding it","memberof":"Emoji","type":{"types":[[["boolean",""]]]},"meta":{"line":38,"file":"Emoji.js","path":"src/structures"},"props":[]},{"id":"Emoji#managed","name":"managed","description":"Whether this emoji is managed by an external service","memberof":"Emoji","type":{"types":[[["boolean",""]]]},"meta":{"line":43,"file":"Emoji.js","path":"src/structures"},"props":[]},{"id":"Emoji#roles","name":"roles","description":"A collection of roles this emoji is active for (empty if all), mapped by role ID.","memberof":"Emoji","type":{"types":[[["Collection",".<"],["string",", "],["Role",">"]]]},"meta":{"line":51,"file":"Emoji.js","path":"src/structures"},"props":[]},{"id":"Emoji#url","name":"url","description":"The URL to the emoji file","memberof":"Emoji","type":{"types":[[["string",""]]]},"meta":{"line":64,"file":"Emoji.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"EvaluatedPermissions","name":"EvaluatedPermissions","description":"The final evaluated permissions for a member in a channel","meta":{"line":6,"file":"EvaluatedPermissions.js","path":"src/structures"},"methods":[{"id":"EvaluatedPermissions#serialize","name":"serialize","description":"Get an object mapping permission name, e.g. `READ_MESSAGES` to a boolean - whether the user\ncan perform this or not.","memberof":"EvaluatedPermissions","meta":{"line":26,"file":"EvaluatedPermissions.js","path":"src/structures"},"returns":{"types":[[["Object",".<"],["string",", "],["boolean",">"]]]},"params":[]},{"id":"EvaluatedPermissions#hasPermission","name":"hasPermission","description":"Checks whether the user has a certain permission, e.g. `READ_MESSAGES`.","memberof":"EvaluatedPermissions","meta":{"line":40,"file":"EvaluatedPermissions.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"permission","description":"The permission to check for","type":{"types":[[["PermissionResolvable",""]]]}},{"name":"explicit","description":"Whether to require the user to explicitly have the exact permission","optional":true,"type":{"types":[[["boolean",""]]]}}]}],"properties":[{"id":"EvaluatedPermissions#member","name":"member","description":"The member this permissions refer to","memberof":"EvaluatedPermissions","type":{"types":[[["GuildMember",""]]]},"meta":{"line":12,"file":"EvaluatedPermissions.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"GroupDMChannel","name":"GroupDMChannel","description":"Represents a Group DM on Discord","meta":{"line":33,"file":"GroupDMChannel.js","path":"src/structures"},"extends":["Channel"],"methods":[{"id":"GroupDMChannel#bulkDelete","name":"bulkDelete","description":"Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after.","memberof":"GroupDMChannel","meta":{"line":24,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"params":[{"name":"messages","description":"The messages to delete","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]],[["Array",".<"],["Message",">"]]]}}],"implements":["TextBasedChannel#bulkDelete"]},{"id":"GroupDMChannel#sendMessage","name":"sendMessage","description":"Send a message to this channel","memberof":"GroupDMChannel","examples":["// send a message\nchannel.sendMessage('hello!')\n .then(message => console.log(`Sent message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":53,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendMessage"]},{"id":"GroupDMChannel#sendTTSMessage","name":"sendTTSMessage","description":"Send a text-to-speech message to this channel","memberof":"GroupDMChannel","examples":["// send a TTS message\nchannel.sendTTSMessage('hello!')\n .then(message => console.log(`Sent tts message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":68,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendTTSMessage"]},{"id":"GroupDMChannel#sendFile","name":"sendFile","description":"Send a file to this channel","memberof":"GroupDMChannel","meta":{"line":78,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"attachment","description":"The file to send","type":{"types":[[["FileResolvable",""]]]}},{"name":"fileName","description":"The name and extension of the file","optional":true,"type":{"types":[[["string",""]]]}}],"implements":["TextBasedChannel#sendFile"]},{"id":"GroupDMChannel#fetchMessages","name":"fetchMessages","description":"Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects.","memberof":"GroupDMChannel","examples":["// get messages\nchannel.fetchMessages({limit: 10})\n .then(messages => console.log(`Received ${messages.size} messages`))\n .catch(console.log);"],"meta":{"line":122,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"options","description":"The query parameters to pass in","optional":true,"type":{"types":[[["ChannelLogsQueryOptions",""]]]}}],"implements":["TextBasedChannel#fetchMessages"]},{"id":"GroupDMChannel#startTyping","name":"startTyping","description":"Starts a typing indicator in the channel.","memberof":"GroupDMChannel","examples":["// start typing in a channel\nchannel.startTyping();"],"meta":{"line":143,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"count","description":"The number of times startTyping should be considered to have been called","optional":true,"type":{"types":[[["number",""]]]}}],"implements":["TextBasedChannel#startTyping"]},{"id":"GroupDMChannel#stopTyping","name":"stopTyping","description":"Stops the typing indicator in the channel.\nThe indicator will only stop if this is called as many times as startTyping().\nIt can take a few seconds for the Client User to stop typing.","memberof":"GroupDMChannel","examples":["// stop typing in a channel\nchannel.stopTyping();","// force typing to fully stop in a channel\nchannel.stopTyping(true);"],"meta":{"line":171,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"force","description":"Whether or not to reset the call count and force the indicator to stop","optional":true,"type":{"types":[[["boolean",""]]]}}],"implements":["TextBasedChannel#stopTyping"]},{"id":"GroupDMChannel#createCollector","name":"createCollector","description":"Creates a Message Collector","memberof":"GroupDMChannel","examples":["// create a message collector\nconst collector = channel.createCollector(\n m => m.content.includes('discord'),\n { time: 15000 }\n);\ncollector.on('message', m => console.log(`Collected ${m.content}`));\ncollector.on('end', collected => console.log(`Collected ${collected.size} items`));"],"meta":{"line":213,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["MessageCollector",""]]]},"params":[{"name":"filter","description":"The filter to create the collector with","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"The options to pass to the collector","optional":true,"type":{"types":[[["CollectorOptions",""]]]}}],"implements":["TextBasedChannel#createCollector"]},{"id":"GroupDMChannel#awaitMessages","name":"awaitMessages","description":"Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified\nfilter.","memberof":"GroupDMChannel","examples":["// await !vote messages\nconst filter = m => m.content.startsWith('!vote');\n// errors: ['time'] treats ending because of the time limit as an error\nchannel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] })\n .then(collected => console.log(collected.size))\n .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));"],"meta":{"line":241,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"filter","description":"The filter function to use","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"Optional options to pass to the internal collector","optional":true,"type":{"types":[[["AwaitMessagesOptions",""]]]}}],"implements":["TextBasedChannel#awaitMessages"]},{"id":"GroupDMChannel#fetchPinnedMessages","name":"fetchPinnedMessages","description":"Fetches the pinned messages of this Channel and returns a Collection of them.","memberof":"GroupDMChannel","meta":{"line":267,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[],"implements":["TextBasedChannel#fetchPinnedMessages"]},{"id":"GroupDMChannel#delete","name":"delete","description":"Deletes the channel","memberof":"GroupDMChannel","examples":["// delete the channel\nchannel.delete()\n .then() // success\n .catch(console.log); // log error"],"inherits":"Channel#delete","inherited":true,"meta":{"line":43,"file":"Channel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Channel",">"]]]},"params":[]}],"properties":[{"id":"GroupDMChannel#recipients","name":"recipients","description":"A collection of the recipients of this DM, mapped by their ID.","memberof":"GroupDMChannel","type":{"types":[[["Collection",".<"],["string",", "],["User",">"]]]},"meta":{"line":63,"file":"GroupDMChannel.js","path":"src/structures"},"props":[]},{"id":"GroupDMChannel#name","name":"name","description":"The name of this Group DM, can be null if one isn't set.","memberof":"GroupDMChannel","type":{"types":[[["string",""]]]},"meta":{"line":77,"file":"GroupDMChannel.js","path":"src/structures"},"props":[]},{"id":"GroupDMChannel#id","name":"id","description":"The ID of this Group DM Channel.","memberof":"GroupDMChannel","type":{"types":[[["string",""]]]},"meta":{"line":82,"file":"GroupDMChannel.js","path":"src/structures"},"props":[]},{"id":"GroupDMChannel#icon","name":"icon","description":"A hash of the Group DM icon.","memberof":"GroupDMChannel","type":{"types":[[["string",""]]]},"meta":{"line":87,"file":"GroupDMChannel.js","path":"src/structures"},"props":[]},{"id":"GroupDMChannel#lastMessageID","name":"lastMessageID","description":"The ID of the last message in the channel, if one was sent.","memberof":"GroupDMChannel","type":{"types":[[["string",""]]]},"meta":{"line":92,"file":"GroupDMChannel.js","path":"src/structures"},"props":[]},{"id":"GroupDMChannel#owner","name":"owner","description":"The owner of this Group DM.","memberof":"GroupDMChannel","type":{"types":[[["User",""]]]},"meta":{"line":97,"file":"GroupDMChannel.js","path":"src/structures"},"props":[]},{"id":"GroupDMChannel#messages","name":"messages","description":"A Collection containing the messages sent to this channel.","memberof":"GroupDMChannel","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"meta":{"line":16,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"GroupDMChannel#typing","name":"typing","description":"Whether or not the typing indicator is being shown in the channel.","memberof":"GroupDMChannel","type":{"types":[[["boolean",""]]]},"meta":{"line":186,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"GroupDMChannel#typingCount","name":"typingCount","description":"Number of times `startTyping` has been called.","memberof":"GroupDMChannel","type":{"types":[[["number",""]]]},"meta":{"line":194,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"GroupDMChannel#client","name":"client","description":"The client that instantiated the Channel","memberof":"GroupDMChannel","type":{"types":[[["Client",""]]]},"meta":{"line":10,"file":"Channel.js","path":"src/structures"},"props":[]},{"id":"GroupDMChannel#type","name":"type","description":"The type of the channel, either:\n* `dm` - a DM channel\n* `group` - a Group DM channel\n* `text` - a guild text channel\n* `voice` - a guild voice channel","memberof":"GroupDMChannel","type":{"types":[[["string",""]]]},"meta":{"line":22,"file":"Channel.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"Guild","name":"Guild","description":"Represents a Guild (or a Server) on Discord.\nIt's recommended to see if a guild is available before performing operations or reading data from it. You can\ncheck this with `guild.available`.","meta":{"line":15,"file":"Guild.js","path":"src/structures"},"methods":[{"id":"Guild#toString","name":"toString","description":"When concatenated with a string, this automatically concatenates the Guild's name instead of the Guild object.","memberof":"Guild","examples":["// logs: Hello from My Guild!\nconsole.log(`Hello from ${guild}!`);","// logs: Hello from My Guild!\nconsole.log(`Hello from ' + guild + '!');"],"meta":{"line":142,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]},{"id":"Guild#member","name":"member","description":"Returns the GuildMember form of a User object, if the User is present in the guild.","memberof":"Guild","examples":["// get the guild member of a user\nconst member = guild.member(message.author);"],"meta":{"line":154,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["GuildMember",""]],[["null",""]]]},"params":[{"name":"user","description":"The user that you want to obtain the GuildMember of","type":{"types":[[["UserResolvable",""]]]}}]},{"id":"Guild#equals","name":"equals","description":"Whether this Guild equals another Guild. It compares all properties, so for most operations\nit is advisable to just compare `guild.id === guild2.id` as it is much faster and is often\nwhat most users need.","memberof":"Guild","meta":{"line":165,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"guild","description":"The guild to compare","type":{"types":[[["Guild",""]]]}}]},{"id":"Guild#createChannel","name":"createChannel","description":"Creates a new Channel in the Guild.","memberof":"Guild","examples":["// create a new text channel\nguild.createChannel('new general', 'text')\n .then(channel => console.log(`Created new channel ${channel}`))\n .catch(console.log);"],"meta":{"line":354,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<("],["TextChannel","|"],["VoiceChannel",")>"]]]},"params":[{"name":"name","description":"The name of the new channel","type":{"types":[[["string",""]]]}},{"name":"type","description":"The type of the new channel, either `text` or `voice`","type":{"types":[[["string",""]]]}}]},{"id":"Guild#createRole","name":"createRole","description":"Creates a new role in the guild, as of now this is just a blank role.","memberof":"Guild","examples":["// create a new role\nguild.createRole()\n .then(role => console.log(`Created role ${role}`))\n .catch(console.log);"],"meta":{"line":367,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[]},{"id":"Guild#leave","name":"leave","description":"Causes the Client to leave the guild.","memberof":"Guild","examples":["// leave a guild\nguild.leave()\n .then(g => console.log(`Left the guild ${g}`))\n .catch(console.log);"],"meta":{"line":380,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[]},{"id":"Guild#delete","name":"delete","description":"Causes the Client to delete the guild.","memberof":"Guild","examples":["// delete a guild\nguild.delete()\n .then(g => console.log(`Deleted the guild ${g}`))\n .catch(console.log);"],"meta":{"line":393,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[]},{"id":"Guild#edit","name":"edit","description":"Updates the Guild with new information - e.g. a new name.","memberof":"Guild","examples":["// set the guild name and region\nguild.edit({\n name: 'Discord Guild',\n region: 'london',\n})\n.then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`))\n.catch(console.log);"],"meta":{"line":410,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"data","description":"The data to update the guild with","type":{"types":[[["GuildEditData",""]]]}}]},{"id":"Guild#setName","name":"setName","description":"Edit the name of the Guild.","memberof":"Guild","examples":["// edit the guild name\nguild.setName('Discord Guild')\n .then(updated => console.log(`Updated guild name to ${guild.name}`))\n .catch(console.log);"],"meta":{"line":424,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"name","description":"The new name of the Guild","type":{"types":[[["string",""]]]}}]},{"id":"Guild#setRegion","name":"setRegion","description":"Edit the region of the Guild.","memberof":"Guild","examples":["// edit the guild region\nguild.setRegion('london')\n .then(updated => console.log(`Updated guild region to ${guild.region}`))\n .catch(console.log);"],"meta":{"line":438,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"region","description":"The new region of the guild.","type":{"types":[[["Region",""]]]}}]},{"id":"Guild#setVerificationLevel","name":"setVerificationLevel","description":"Edit the verification level of the Guild.","memberof":"Guild","examples":["// edit the guild verification level\nguild.setVerificationLevel(1)\n .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`))\n .catch(console.log);"],"meta":{"line":452,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"verificationLevel","description":"The new verification level of the guild","type":{"types":[[["VerificationLevel",""]]]}}]},{"id":"Guild#setAFKChannel","name":"setAFKChannel","description":"Edit the AFK channel of the Guild.","memberof":"Guild","examples":["// edit the guild AFK channel\nguild.setAFKChannel(channel)\n .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`))\n .catch(console.log);"],"meta":{"line":466,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"afkChannel","description":"The new AFK channel","type":{"types":[[["GuildChannelResolvable",""]]]}}]},{"id":"Guild#setAFKTimeout","name":"setAFKTimeout","description":"Edit the AFK timeout of the Guild.","memberof":"Guild","examples":["// edit the guild AFK channel\nguild.setAFKTimeout(60)\n .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`))\n .catch(console.log);"],"meta":{"line":480,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"afkTimeout","description":"The time in seconds that a user must be idle to be considered AFK","type":{"types":[[["number",""]]]}}]},{"id":"Guild#setIcon","name":"setIcon","description":"Set a new Guild Icon.","memberof":"Guild","examples":["// edit the guild icon\nguild.setIcon(fs.readFileSync('./icon.png'))\n .then(updated => console.log('Updated the guild icon'))\n .catch(console.log);"],"meta":{"line":494,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"icon","description":"The new icon of the guild","type":{"types":[[["Base64Resolvable",""]]]}}]},{"id":"Guild#setOwner","name":"setOwner","description":"Sets a new owner of the Guild.","memberof":"Guild","examples":["// edit the guild owner\nguild.setOwner(guilds.members[0])\n .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`))\n .catch(console.log);"],"meta":{"line":508,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"owner","description":"The new owner of the Guild","type":{"types":[[["GuildMemberResolvable",""]]]}}]},{"id":"Guild#setSplash","name":"setSplash","description":"Set a new Guild Splash Logo.","memberof":"Guild","examples":["// edit the guild splash\nguild.setIcon(fs.readFileSync('./splash.png'))\n .then(updated => console.log('Updated the guild splash'))\n .catch(console.log);"],"meta":{"line":522,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"splash","description":"The new splash screen of the guild","type":{"types":[[["Base64Resolvable",""]]]}}]},{"id":"Guild#unban","name":"unban","description":"Unbans a member from the Guild","memberof":"Guild","examples":["// unban a member\nguild.unban('123123123123')\n .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))\n .catch(reject);"],"meta":{"line":536,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["User",">"]]]},"params":[{"name":"member","description":"The member to unban","type":{"types":[[["UserResolvable",""]]]}}]},{"id":"Guild#fetchBans","name":"fetchBans","description":"Fetch a Collection of banned users in this Guild.","memberof":"Guild","meta":{"line":544,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["User",">>"]]]},"params":[]},{"id":"Guild#fetchInvites","name":"fetchInvites","description":"Fetch a Collection of invites to this Guild. Resolves with a Collection mapping invites by their codes.","memberof":"Guild","meta":{"line":552,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Invite",">>"]]]},"params":[]},{"id":"Guild#fetchMembers","name":"fetchMembers","description":"Fetches all the members in the Guild, even if they are offline. If the Guild has less than 250 members,\nthis should not be necessary.","memberof":"Guild","meta":{"line":562,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Guild",">"]]]},"params":[{"name":"query","description":"An optional query to provide when fetching members","optional":true,"type":{"types":[[["string",""]]]}}]},{"id":"Guild#sync","name":"sync","description":"Syncs this guild (already done automatically every 30 seconds). Only applicable to user accounts.","memberof":"Guild","meta":{"line":607,"file":"Guild.js","path":"src/structures"},"returns":{"types":[[["null",""]]]},"params":[]}],"properties":[{"id":"Guild#client","name":"client","description":"The Client that created the instance of the the Guild.","memberof":"Guild","type":{"types":[[["Client",""]]]},"meta":{"line":21,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#members","name":"members","description":"A Collection of members that are in this Guild. The key is the member's ID, the value is the member.","memberof":"Guild","type":{"types":[[["Collection",".<"],["string",", "],["GuildMember",">"]]]},"meta":{"line":27,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#channels","name":"channels","description":"A Collection of channels that are in this Guild. The key is the channel's ID, the value is the channel.","memberof":"Guild","type":{"types":[[["Collection",".<"],["string",", "],["GuildChannel",">"]]]},"meta":{"line":33,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#roles","name":"roles","description":"A Collection of roles that are in this Guild. The key is the role's ID, the value is the role.","memberof":"Guild","type":{"types":[[["Collection",".<"],["string",", "],["Role",">"]]]},"meta":{"line":39,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#available","name":"available","description":"Whether the Guild is available to access. If it is not available, it indicates a server outage.","memberof":"Guild","type":{"types":[[["boolean",""]]]},"meta":{"line":48,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#id","name":"id","description":"The Unique ID of the Guild, useful for comparisons.","memberof":"Guild","type":{"types":[[["string",""]]]},"meta":{"line":53,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#splash","name":"splash","description":"The hash of the guild splash image, or null if no splash (VIP only)","memberof":"Guild","type":{"types":[[["string",""]]]},"meta":{"line":218,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#region","name":"region","description":"The region the guild is located in","memberof":"Guild","type":{"types":[[["string",""]]]},"meta":{"line":223,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#name","name":"name","description":"The name of the guild","memberof":"Guild","type":{"types":[[["string",""]]]},"meta":{"line":228,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#memberCount","name":"memberCount","description":"The full amount of members in this Guild as of `READY`","memberof":"Guild","type":{"types":[[["number",""]]]},"meta":{"line":233,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#large","name":"large","description":"Whether the guild is \"large\" (has more than 250 members)","memberof":"Guild","type":{"types":[[["boolean",""]]]},"meta":{"line":238,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#icon","name":"icon","description":"The hash of the guild icon, or null if there is no icon.","memberof":"Guild","type":{"types":[[["string",""]]]},"meta":{"line":244,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#features","name":"features","description":"An array of guild features.","memberof":"Guild","type":{"types":[[["Array",".<"],["Object",">"]]]},"meta":{"line":249,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#emojis","name":"emojis","description":"An array of guild emojis.","memberof":"Guild","type":{"types":[[["Array",".<"],["Object",">"]]]},"meta":{"line":254,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#afkTimeout","name":"afkTimeout","description":"The time in seconds before a user is counted as \"away from keyboard\".","memberof":"Guild","type":{"types":[[["number",""]]]},"meta":{"line":262,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#afkChannelID","name":"afkChannelID","description":"The ID of the voice channel where AFK members are moved.","memberof":"Guild","type":{"types":[[["string",""]]]},"meta":{"line":267,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#embedEnabled","name":"embedEnabled","description":"Whether embedded images are enabled on this guild.","memberof":"Guild","type":{"types":[[["boolean",""]]]},"meta":{"line":272,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#verificationLevel","name":"verificationLevel","description":"The verification level of the guild.","memberof":"Guild","type":{"types":[[["number",""]]]},"meta":{"line":277,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#joinDate","name":"joinDate","description":"The date at which the logged-in client joined the guild.","memberof":"Guild","type":{"types":[[["Date",""]]]},"meta":{"line":339,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#iconURL","name":"iconURL","description":"Gets the URL to this guild's icon (if it has one, otherwise it returns null)","memberof":"Guild","type":{"types":[[["string",""]]]},"meta":{"line":590,"file":"Guild.js","path":"src/structures"},"props":[]},{"id":"Guild#owner","name":"owner","description":"The owner of the Guild","memberof":"Guild","type":{"types":[[["GuildMember",""]]]},"meta":{"line":600,"file":"Guild.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"GuildChannel","name":"GuildChannel","description":"Represents a Guild Channel (i.e. Text Channels and Voice Channels)","meta":{"line":13,"file":"GuildChannel.js","path":"src/structures"},"extends":["Channel"],"methods":[{"id":"GuildChannel#equals","name":"equals","description":"Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel.\nIn most cases, a simple `channel.id === channel2.id` will do, and is much faster too.","memberof":"GuildChannel","meta":{"line":54,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"channel","description":"The channel to compare this channel to","type":{"types":[[["GuildChannel",""]]]}}]},{"id":"GuildChannel#permissionsFor","name":"permissionsFor","description":"Gets the overall set of permissions for a user in this channel, taking into account roles and permission\noverwrites.","memberof":"GuildChannel","meta":{"line":81,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["EvaluatedPermissions",""]]]},"params":[{"name":"member","description":"The user that you want to obtain the overall permissions for","type":{"types":[[["GuildMemberResolvable",""]]]}}]},{"id":"GuildChannel#overwritePermissions","name":"overwritePermissions","description":"Overwrites the permissions for a user or role in this channel.","memberof":"GuildChannel","examples":["// overwrite permissions for a message author\nmessage.channel.overwritePermissions(message.author, {\n SEND_MESSAGES: false\n})\n.then(() => console.log('Done!'))\n.catch(console.log);"],"meta":{"line":155,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",""]]]},"params":[{"name":"userOrRole","description":"The user or role to update","type":{"types":[[["Role",""]],[["UserResolvable",""]]]}},{"name":"options","description":"The configuration for the update","type":{"types":[[["PermissionOverwriteOptions",""]]]}}]},{"id":"GuildChannel#setName","name":"setName","description":"Set a new name for the Guild Channel","memberof":"GuildChannel","examples":["// set a new channel name\nchannel.setName('not general')\n .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))\n .catch(console.log);"],"meta":{"line":205,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"name","description":"The new name for the guild channel","type":{"types":[[["string",""]]]}}]},{"id":"GuildChannel#setPosition","name":"setPosition","description":"Set a new position for the Guild Channel","memberof":"GuildChannel","examples":["// set a new channel position\nchannel.setPosition(2)\n .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))\n .catch(console.log);"],"meta":{"line":219,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"position","description":"The new position for the guild channel","type":{"types":[[["number",""]]]}}]},{"id":"GuildChannel#setTopic","name":"setTopic","description":"Set a new topic for the Guild Channel","memberof":"GuildChannel","examples":["// set a new channel topic\nchannel.setTopic('needs more rate limiting')\n .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))\n .catch(console.log);"],"meta":{"line":233,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"topic","description":"The new topic for the guild channel","type":{"types":[[["string",""]]]}}]},{"id":"GuildChannel#toString","name":"toString","description":"When concatenated with a string, this automatically returns the Channel's mention instead of the Channel object.","memberof":"GuildChannel","examples":["// Outputs: Hello from #general\nconsole.log(`Hello from ${channel}`);","// Outputs: Hello from #general\nconsole.log('Hello from ' + channel);"],"meta":{"line":247,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]},{"id":"GuildChannel#createInvite","name":"createInvite","description":"Create an invite to this Guild Channel","memberof":"GuildChannel","meta":{"line":268,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Invite",">"]]]},"params":[{"name":"options","description":"The options for the invite","optional":true,"type":{"types":[[["InviteOptions",""]]]}}]},{"id":"GuildChannel#delete","name":"delete","description":"Deletes the channel","memberof":"GuildChannel","examples":["// delete the channel\nchannel.delete()\n .then() // success\n .catch(console.log); // log error"],"inherits":"Channel#delete","inherited":true,"meta":{"line":43,"file":"Channel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Channel",">"]]]},"params":[]}],"properties":[{"id":"GuildChannel#topic","name":"topic","description":"The topic of the Guild Channel, if there is one.","memberof":"GuildChannel","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"GuildChannel#position","name":"position","description":"The position of the channel in the list.","memberof":"GuildChannel","type":{"types":[[["number",""]]]},"meta":{"line":29,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"GuildChannel#name","name":"name","description":"The name of the Guild Channel","memberof":"GuildChannel","type":{"types":[[["string",""]]]},"meta":{"line":34,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"GuildChannel#permissionOverwrites","name":"permissionOverwrites","description":"A map of permission overwrites in this channel for roles and users.","memberof":"GuildChannel","type":{"types":[[["Collection",".<"],["string",", "],["PermissionOverwrites",">"]]]},"meta":{"line":40,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"GuildChannel#client","name":"client","description":"The client that instantiated the Channel","memberof":"GuildChannel","type":{"types":[[["Client",""]]]},"meta":{"line":10,"file":"Channel.js","path":"src/structures"},"props":[]},{"id":"GuildChannel#type","name":"type","description":"The type of the channel, either:\n* `dm` - a DM channel\n* `group` - a Group DM channel\n* `text` - a guild text channel\n* `voice` - a guild voice channel","memberof":"GuildChannel","type":{"types":[[["string",""]]]},"meta":{"line":22,"file":"Channel.js","path":"src/structures"},"props":[]},{"id":"GuildChannel#id","name":"id","description":"The unique ID of the channel","memberof":"GuildChannel","type":{"types":[[["string",""]]]},"meta":{"line":31,"file":"Channel.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"GuildMember","name":"GuildMember","description":"Represents a Member of a Guild on Discord","meta":{"line":8,"file":"GuildMember.js","path":"src/structures"},"methods":[{"id":"GuildMember#setMute","name":"setMute","description":"Mute/unmute a user","memberof":"GuildMember","meta":{"line":143,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[{"name":"mute","description":"Whether or not the member should be muted","type":{"types":[[["boolean",""]]]}}]},{"id":"GuildMember#setDeaf","name":"setDeaf","description":"Deafen/undeafen a user","memberof":"GuildMember","meta":{"line":152,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[{"name":"deaf","description":"Whether or not the member should be deafened","type":{"types":[[["boolean",""]]]}}]},{"id":"GuildMember#setVoiceChannel","name":"setVoiceChannel","description":"Moves the Guild Member to the given channel.","memberof":"GuildMember","meta":{"line":161,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[{"name":"channel","description":"The channel to move the member to","type":{"types":[[["ChannelResolvable",""]]]}}]},{"id":"GuildMember#setRoles","name":"setRoles","description":"Sets the Roles applied to the member.","memberof":"GuildMember","meta":{"line":170,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[{"name":"roles","description":"The roles to apply","type":{"types":[[["Collection",".<"],["string",", "],["Role",">"]],[["Array",".<"],["Role",">"]]]}}]},{"id":"GuildMember#setNickname","name":"setNickname","description":"Set the nickname for the Guild Member","memberof":"GuildMember","meta":{"line":179,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[{"name":"nick","description":"The nickname for the Guild Member","type":{"types":[[["string",""]]]}}]},{"id":"GuildMember#edit","name":"edit","description":"Edit a Guild Member","memberof":"GuildMember","meta":{"line":188,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[{"name":"data","description":"The data to edit the member with","type":{"types":[[["GuildmemberEditData",""]]]}}]},{"id":"GuildMember#deleteDM","name":"deleteDM","description":"Deletes any DMs with this Guild Member","memberof":"GuildMember","meta":{"line":196,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["DMChannel",">"]]]},"params":[]},{"id":"GuildMember#kick","name":"kick","description":"Kick this member from the Guild","memberof":"GuildMember","meta":{"line":204,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[]},{"id":"GuildMember#ban","name":"ban","description":"Ban this Guild Member","memberof":"GuildMember","examples":["// ban a guild member\nguildMember.ban(7);"],"meta":{"line":217,"file":"GuildMember.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildMember",">"]]]},"params":[{"name":"deleteDays","description":"The amount of days worth of messages from this member that should\nalso be deleted. Between `0` and `7`.","optional":true,"type":{"types":[[["number",""]]]}}]},{"id":"GuildMember#sendMessage","name":"sendMessage","description":"Send a message to this channel","memberof":"GuildMember","examples":["// send a message\nchannel.sendMessage('hello!')\n .then(message => console.log(`Sent message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":53,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendMessage"]},{"id":"GuildMember#sendTTSMessage","name":"sendTTSMessage","description":"Send a text-to-speech message to this channel","memberof":"GuildMember","examples":["// send a TTS message\nchannel.sendTTSMessage('hello!')\n .then(message => console.log(`Sent tts message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":68,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendTTSMessage"]},{"id":"GuildMember#sendFile","name":"sendFile","description":"Send a file to this channel","memberof":"GuildMember","meta":{"line":78,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"attachment","description":"The file to send","type":{"types":[[["FileResolvable",""]]]}},{"name":"fileName","description":"The name and extension of the file","optional":true,"type":{"types":[[["string",""]]]}}],"implements":["TextBasedChannel#sendFile"]}],"properties":[{"id":"GuildMember#client","name":"client","description":"The client that instantiated this GuildMember","memberof":"GuildMember","type":{"types":[[["Client",""]]]},"meta":{"line":14,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#guild","name":"guild","description":"The guild that this member is part of","memberof":"GuildMember","type":{"types":[[["Guild",""]]]},"meta":{"line":19,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#user","name":"user","description":"The user that this guild member instance Represents","memberof":"GuildMember","type":{"types":[[["User",""]]]},"meta":{"line":24,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#serverDeaf","name":"serverDeaf","description":"Whether this member is deafened server-wide","memberof":"GuildMember","type":{"types":[[["boolean",""]]]},"meta":{"line":35,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#serverMute","name":"serverMute","description":"Whether this member is muted server-wide","memberof":"GuildMember","type":{"types":[[["boolean",""]]]},"meta":{"line":40,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#selfMute","name":"selfMute","description":"Whether this member is self-muted","memberof":"GuildMember","type":{"types":[[["boolean",""]]]},"meta":{"line":45,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#selfDeaf","name":"selfDeaf","description":"Whether this member is self-deafened","memberof":"GuildMember","type":{"types":[[["boolean",""]]]},"meta":{"line":50,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#voiceSessionID","name":"voiceSessionID","description":"The voice session ID of this member, if any","memberof":"GuildMember","type":{"types":[[["string",""]]]},"meta":{"line":55,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#voiceChannelID","name":"voiceChannelID","description":"The voice channel ID of this member, if any","memberof":"GuildMember","type":{"types":[[["string",""]]]},"meta":{"line":60,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#speaking","name":"speaking","description":"Whether this meember is speaking","memberof":"GuildMember","type":{"types":[[["boolean",""]]]},"meta":{"line":66,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#nickname","name":"nickname","description":"The nickname of this Guild Member, if they have one","memberof":"GuildMember","type":{"types":[[["string",""]]]},"meta":{"line":71,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#joinDate","name":"joinDate","description":"The date this member joined the guild","memberof":"GuildMember","type":{"types":[[["Date",""]]]},"meta":{"line":79,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#roles","name":"roles","description":"A list of roles that are applied to this GuildMember, mapped by the role ID.","memberof":"GuildMember","type":{"types":[[["Collection",".<"],["string",", "],["Role",">"]]]},"meta":{"line":88,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#mute","name":"mute","description":"Whether this member is muted in any way","memberof":"GuildMember","type":{"types":[[["boolean",""]]]},"meta":{"line":107,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#deaf","name":"deaf","description":"Whether this member is deafened in any way","memberof":"GuildMember","type":{"types":[[["boolean",""]]]},"meta":{"line":116,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#voiceChannel","name":"voiceChannel","description":"The voice channel this member is in, if any","memberof":"GuildMember","type":{"types":[[["VoiceChannel",""]]]},"meta":{"line":125,"file":"GuildMember.js","path":"src/structures"},"props":[]},{"id":"GuildMember#id","name":"id","description":"The ID of this User","memberof":"GuildMember","type":{"types":[[["string",""]]]},"meta":{"line":134,"file":"GuildMember.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"MessageCollector","name":"MessageCollector","description":"Collects messages based on a specified filter, then emits them.","meta":{"line":286,"file":"TextBasedChannel.js","path":"src/structures/interface"},"extends":["EventEmitter"],"classConstructor":{"id":"MessageCollector()","name":"MessageCollector","memberof":"MessageCollector","params":[{"name":"channel","description":"The channel to collect messages in","type":{"types":[[["Channel",""]]]}},{"name":"filter","description":"The filter function","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"Options for the collector","optional":true,"type":{"types":[[["CollectorOptions",""]]]}}]},"methods":[{"id":"MessageCollector#stop","name":"stop","description":"Stops the collector and emits `end`.","memberof":"MessageCollector","meta":{"line":375,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"reason","description":"An optional reason for stopping the collector","optional":true,"type":{"types":[[["string",""]]]}}]}],"properties":[{"id":"MessageCollector#channel","name":"channel","description":"The channel this collector is operating on","memberof":"MessageCollector","type":{"types":[[["Channel",""]]]},"meta":{"line":322,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"MessageCollector#filter","name":"filter","description":"A function used to filter messages that the collector collects.","memberof":"MessageCollector","type":{"types":[[["CollectorFilterFunction",""]]]},"meta":{"line":327,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"MessageCollector#options","name":"options","description":"Options for the collecor.","memberof":"MessageCollector","type":{"types":[[["CollectorOptions",""]]]},"meta":{"line":332,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"MessageCollector#ended","name":"ended","description":"Whether this collector has stopped collecting Messages.","memberof":"MessageCollector","type":{"types":[[["boolean",""]]]},"meta":{"line":337,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"MessageCollector#collected","name":"collected","description":"A collection of collected messages, mapped by message ID.","memberof":"MessageCollector","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"meta":{"line":344,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]}],"events":[{"id":"MessageCollector#event:message","name":"message","description":"Emitted whenever the Collector receives a Message that passes the filter test.","memberof":"MessageCollector","meta":{"line":358,"file":"TextBasedChannel.js","path":"src/structures/interface"},"params":[{"name":"message","description":"The received message","type":{"types":[[["Message",""]]]}},{"name":"collector","description":"The collector the message passed through","type":{"types":[[["MessageCollector",""]]]}}]},{"id":"MessageCollector#event:end","name":"end","description":"Emitted when the Collector stops collecting.","memberof":"MessageCollector","meta":{"line":379,"file":"TextBasedChannel.js","path":"src/structures/interface"},"params":[{"name":"collection","description":"A collection of messages collected\nduring the lifetime of the Collector, mapped by the ID of the Messages.","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]}},{"name":"reason","description":"The reason for the end of the collector. If it ended because it reached the specified time\nlimit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it\nended because it reached its message limit, it will be `limit`.","type":{"types":[[["string",""]]]}}]}]},{"id":"Invite","name":"Invite","description":"Represents an Invitation to a Guild Channel","meta":{"line":28,"file":"Invite.js","path":"src/structures"},"methods":[{"id":"Invite#delete","name":"delete","description":"Deletes this invite","memberof":"Invite","meta":{"line":103,"file":"Invite.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Invite",">"]]]},"params":[]}],"properties":[{"id":"Invite#client","name":"client","description":"The client that instantiated the invite","memberof":"Invite","type":{"types":[[["Client",""]]]},"meta":{"line":34,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#maxAge","name":"maxAge","description":"The maximum age of the invite, in seconds","memberof":"Invite","type":{"types":[[["number",""]]]},"meta":{"line":43,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#code","name":"code","description":"The code for this invite","memberof":"Invite","type":{"types":[[["string",""]]]},"meta":{"line":49,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#temporary","name":"temporary","description":"Whether or not this invite is temporary","memberof":"Invite","type":{"types":[[["boolean",""]]]},"meta":{"line":56,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#uses","name":"uses","description":"How many times this invite has been used","memberof":"Invite","type":{"types":[[["number",""]]]},"meta":{"line":62,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#maxUses","name":"maxUses","description":"The maximum uses of this invite","memberof":"Invite","type":{"types":[[["number",""]]]},"meta":{"line":68,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#inviter","name":"inviter","description":"The user who created this invite","memberof":"Invite","type":{"types":[[["User",""]]]},"meta":{"line":74,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#guild","name":"guild","description":"The Guild the invite is for. If this Guild is already known, this will be a Guild object. If the Guild is\nunknown, this will be a Partial Guild.","memberof":"Invite","type":{"types":[[["Guild",""]],[["PartialGuild",""]]]},"meta":{"line":81,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#channels","name":"channels","description":"The Channel the invite is for. If this Channel is already known, this will be a GuildChannel object.\nIf the Channel is unknown, this will be a Partial Guild Channel.","memberof":"Invite","type":{"types":[[["GuildChannel",""]],[["PartialGuildChannel",""]]]},"meta":{"line":88,"file":"Invite.js","path":"src/structures"},"props":[]},{"id":"Invite#creationDate","name":"creationDate","description":"The creation date of the invite","memberof":"Invite","type":{"types":[[["Date",""]]]},"meta":{"line":95,"file":"Invite.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"Message","name":"Message","description":"Represents a Message on Discord","meta":{"line":8,"file":"Message.js","path":"src/structures"},"methods":[{"id":"Message#equals","name":"equals","description":"Used mainly internally. Whether two messages are identical in properties. If you want to compare messages\nwithout checking all the properties, use `message.id === message2.id`, which is much more efficient. This\nmethod allows you to see if there are differences in content, embeds, attachments, nonce and tts properties.","memberof":"Message","meta":{"line":214,"file":"Message.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"message","description":"The message to compare it to","type":{"types":[[["Message",""]]]}},{"name":"rawData","description":"Raw data passed through the WebSocket about this message","type":{"types":[[["Object",""]]]}}]},{"id":"Message#delete","name":"delete","description":"Deletes the message","memberof":"Message","examples":["// delete a message\nmessage.delete()\n .then(msg => console.log(`Deleted message from ${msg.author}`))\n .catch(console.log);"],"meta":{"line":246,"file":"Message.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"timeout","description":"How long to wait to delete the message in milliseconds","optional":true,"type":{"types":[[["number",""]]]}}]},{"id":"Message#edit","name":"edit","description":"Edit the content of a message","memberof":"Message","examples":["// update the content of a message\nmessage.edit('This is my new content!')\n .then(msg => console.log(`Updated the content of a message from ${msg.author}`))\n .catch(console.log);"],"meta":{"line":266,"file":"Message.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The new content for the message","type":{"types":[[["string",""]]]}}]},{"id":"Message#reply","name":"reply","description":"Reply to a message","memberof":"Message","examples":["// reply to a message\nmessage.reply('Hey, I'm a reply!')\n .then(msg => console.log(`Sent a reply to ${msg.author}`))\n .catch(console.log);"],"meta":{"line":281,"file":"Message.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content for the message","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}]},{"id":"Message#pin","name":"pin","description":"Pins this message to the channel's pinned messages","memberof":"Message","meta":{"line":290,"file":"Message.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[]},{"id":"Message#unpin","name":"unpin","description":"Unpins this message from the channel's pinned messages","memberof":"Message","meta":{"line":298,"file":"Message.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[]}],"properties":[{"id":"Message#channel","name":"channel","description":"The channel that the message was sent in","memberof":"Message","type":{"types":[[["TextChannel",""]],[["DMChannel",""]],[["GroupDMChannel",""]]]},"meta":{"line":15,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#guild","name":"guild","description":"If the message was sent in a guild, this will be the guild the message was sent in","memberof":"Message","type":{"types":[[["Guild",""]]]},"meta":{"line":22,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#client","name":"client","description":"The client that instantiated the Message","memberof":"Message","type":{"types":[[["Client",""]]]},"meta":{"line":29,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#pinned","name":"pinned","description":"Whether or not this message is pinned","memberof":"Message","type":{"types":[[["boolean",""]]]},"meta":{"line":38,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#author","name":"author","description":"The author of the message","memberof":"Message","type":{"types":[[["User",""]]]},"meta":{"line":43,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#member","name":"member","description":"Represents the Author of the message as a Guild Member. Only available if the message comes from a Guild\nwhere the author is still a member.","memberof":"Message","type":{"types":[[["GuildMember",""]]]},"meta":{"line":50,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#content","name":"content","description":"The content of the message","memberof":"Message","type":{"types":[[["string",""]]]},"meta":{"line":56,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#tts","name":"tts","description":"Whether or not the message was Text-To-Speech","memberof":"Message","type":{"types":[[["boolean",""]]]},"meta":{"line":63,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#nonce","name":"nonce","description":"A random number used for checking message delivery","memberof":"Message","type":{"types":[[["string",""]]]},"meta":{"line":68,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#embeds","name":"embeds","description":"A list of embeds in the message - e.g. YouTube Player","memberof":"Message","type":{"types":[[["Array",".<"],["Embed",">"]]]},"meta":{"line":73,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#attachments","name":"attachments","description":"A collection of attachments in the message - e.g. Pictures - mapped by their ID.","memberof":"Message","type":{"types":[[["Collection",".<"],["string",", "],["MessageAttachment",">"]]]},"meta":{"line":78,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#mentions","name":"mentions","description":"An object containing a further users, roles or channels collections","memberof":"Message","type":{"types":[[["Object",""]]]},"meta":{"line":89,"file":"Message.js","path":"src/structures"},"props":[{"name":"mentions.users","description":"Mentioned users, maps their ID to the user object.","type":{"types":[[["Collection",".<"],["string",", "],["User",">"]]]}},{"name":"mentions.roles","description":"Mentioned roles, maps their ID to the role object.","type":{"types":[[["Collection",".<"],["string",", "],["Role",">"]]]}},{"name":"mentions.channels","description":"Mentioned channels,\nmaps their ID to the channel object.","type":{"types":[[["Collection",".<"],["string",", "],["GuildChannel",">"]]]}},{"name":"mentions.everyone","description":"Whether or not @everyone was mentioned.","type":{"types":[[["boolean",""]]]}}]},{"id":"Message#id","name":"id","description":"The ID of the message (unique in the channel it was sent)","memberof":"Message","type":{"types":[[["string",""]]]},"meta":{"line":99,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#system","name":"system","description":"Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)","memberof":"Message","type":{"types":[[["boolean",""]]]},"meta":{"line":130,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#timestamp","name":"timestamp","description":"When the message was sent","memberof":"Message","type":{"types":[[["Date",""]]]},"meta":{"line":137,"file":"Message.js","path":"src/structures"},"props":[]},{"id":"Message#editedTimestamp","name":"editedTimestamp","description":"If the message was edited, the timestamp at which it was last edited","memberof":"Message","type":{"types":[[["Date",""]]]},"meta":{"line":145,"file":"Message.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"MessageAttachment","name":"MessageAttachment","description":"Represents an Attachment in a Message","meta":{"line":4,"file":"MessageAttachment.js","path":"src/structures"},"methods":[],"properties":[{"id":"MessageAttachment#client","name":"client","description":"The Client that instantiated this Message.","memberof":"MessageAttachment","type":{"types":[[["Client",""]]]},"meta":{"line":10,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#message","name":"message","description":"The message this attachment is part of.","memberof":"MessageAttachment","type":{"types":[[["Message",""]]]},"meta":{"line":15,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#id","name":"id","description":"The ID of this attachment","memberof":"MessageAttachment","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#filename","name":"filename","description":"The file name of this attachment","memberof":"MessageAttachment","type":{"types":[[["string",""]]]},"meta":{"line":29,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#filesize","name":"filesize","description":"The size of this attachment in bytes","memberof":"MessageAttachment","type":{"types":[[["number",""]]]},"meta":{"line":34,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#url","name":"url","description":"The URL to this attachment","memberof":"MessageAttachment","type":{"types":[[["string",""]]]},"meta":{"line":39,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#proxyURL","name":"proxyURL","description":"The Proxy URL to this attachment","memberof":"MessageAttachment","type":{"types":[[["string",""]]]},"meta":{"line":44,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#height","name":"height","description":"The height of this attachment (if an image)","memberof":"MessageAttachment","type":{"types":[[["number",""]]]},"meta":{"line":49,"file":"MessageAttachment.js","path":"src/structures"},"props":[]},{"id":"MessageAttachment#width","name":"width","description":"The width of this attachment (if an image)","memberof":"MessageAttachment","type":{"types":[[["number",""]]]},"meta":{"line":54,"file":"MessageAttachment.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"MessageEmbed","name":"MessageEmbed","description":"Represents an embed in an image - e.g. preview of image","meta":{"line":4,"file":"MessageEmbed.js","path":"src/structures"},"methods":[],"properties":[{"id":"MessageEmbed#message","name":"message","description":"The message this embed is part of","memberof":"MessageEmbed","type":{"types":[[["Message",""]]]},"meta":{"line":10,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbed#client","name":"client","description":"The client that instantiated this embed","memberof":"MessageEmbed","type":{"types":[[["Client",""]]]},"meta":{"line":15,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbed#title","name":"title","description":"The title of this embed, if there is one","memberof":"MessageEmbed","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbed#type","name":"type","description":"The type of this embed","memberof":"MessageEmbed","type":{"types":[[["string",""]]]},"meta":{"line":29,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbed#description","name":"description","description":"The description of this embed, if there is one","memberof":"MessageEmbed","type":{"types":[[["string",""]]]},"meta":{"line":34,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbed#url","name":"url","description":"The URL of this embed","memberof":"MessageEmbed","type":{"types":[[["string",""]]]},"meta":{"line":39,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbed#thumbnail","name":"thumbnail","description":"The thumbnail of this embed, if there is one","memberof":"MessageEmbed","type":{"types":[[["MessageEmbedThumbnail",""]]]},"meta":{"line":45,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbed#provider","name":"provider","description":"The provider of this embed, if there is one","memberof":"MessageEmbed","type":{"types":[[["MessageEmbedProvider",""]]]},"meta":{"line":52,"file":"MessageEmbed.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"MessageEmbedThumbnail","name":"MessageEmbedThumbnail","description":"Represents a thumbnail for a Message embed","meta":{"line":60,"file":"MessageEmbed.js","path":"src/structures"},"methods":[],"properties":[{"id":"MessageEmbedThumbnail#embed","name":"embed","description":"The embed this thumbnail is part of","memberof":"MessageEmbedThumbnail","type":{"types":[[["MessageEmbed",""]]]},"meta":{"line":66,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbedThumbnail#url","name":"url","description":"The URL for this thumbnail","memberof":"MessageEmbedThumbnail","type":{"types":[[["string",""]]]},"meta":{"line":75,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbedThumbnail#proxyURL","name":"proxyURL","description":"The Proxy URL for this thumbnail","memberof":"MessageEmbedThumbnail","type":{"types":[[["string",""]]]},"meta":{"line":80,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbedThumbnail#height","name":"height","description":"The height of the thumbnail","memberof":"MessageEmbedThumbnail","type":{"types":[[["number",""]]]},"meta":{"line":85,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbedThumbnail#width","name":"width","description":"The width of the thumbnail","memberof":"MessageEmbedThumbnail","type":{"types":[[["number",""]]]},"meta":{"line":90,"file":"MessageEmbed.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"MessageEmbedProvider","name":"MessageEmbedProvider","description":"Represents a Provider for a Message embed","meta":{"line":97,"file":"MessageEmbed.js","path":"src/structures"},"methods":[],"properties":[{"id":"MessageEmbedProvider#embed","name":"embed","description":"The embed this provider is part of","memberof":"MessageEmbedProvider","type":{"types":[[["MessageEmbed",""]]]},"meta":{"line":103,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbedProvider#name","name":"name","description":"The name of this provider","memberof":"MessageEmbedProvider","type":{"types":[[["string",""]]]},"meta":{"line":112,"file":"MessageEmbed.js","path":"src/structures"},"props":[]},{"id":"MessageEmbedProvider#url","name":"url","description":"The URL of this provider","memberof":"MessageEmbedProvider","type":{"types":[[["string",""]]]},"meta":{"line":117,"file":"MessageEmbed.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"PartialGuild","name":"PartialGuild","description":"Represents a Guild that the client only has limited information for - e.g. from invites.","meta":{"line":11,"file":"PartialGuild.js","path":"src/structures"},"methods":[],"properties":[{"id":"PartialGuild#client","name":"client","description":"The client that instantiated this PartialGuild","memberof":"PartialGuild","type":{"types":[[["Client",""]]]},"meta":{"line":17,"file":"PartialGuild.js","path":"src/structures"},"props":[]},{"id":"PartialGuild#splash","name":"splash","description":"The hash of the guild splash image, or null if no splash (VIP only)","memberof":"PartialGuild","type":{"types":[[["string",""]]]},"meta":{"line":26,"file":"PartialGuild.js","path":"src/structures"},"props":[]},{"id":"PartialGuild#id","name":"id","description":"The ID of this guild","memberof":"PartialGuild","type":{"types":[[["string",""]]]},"meta":{"line":31,"file":"PartialGuild.js","path":"src/structures"},"props":[]},{"id":"PartialGuild#icon","name":"icon","description":"The hash of this guild's icon, or null if there is none.","memberof":"PartialGuild","type":{"types":[[["string",""]]]},"meta":{"line":36,"file":"PartialGuild.js","path":"src/structures"},"props":[]},{"id":"PartialGuild#name","name":"name","description":"The name of this guild","memberof":"PartialGuild","type":{"types":[[["string",""]]]},"meta":{"line":41,"file":"PartialGuild.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"PartialGuildChannel","name":"PartialGuildChannel","description":"Represents a Guild Channel that the client only has limited information for - e.g. from invites.","meta":{"line":10,"file":"PartialGuildChannel.js","path":"src/structures"},"methods":[],"properties":[{"id":"PartialGuildChannel#client","name":"client","description":"The client that instantiated this PartialGuildChannel","memberof":"PartialGuildChannel","type":{"types":[[["Client",""]]]},"meta":{"line":16,"file":"PartialGuildChannel.js","path":"src/structures"},"props":[]},{"id":"PartialGuildChannel#id","name":"id","description":"The ID of this Guild Channel","memberof":"PartialGuildChannel","type":{"types":[[["string",""]]]},"meta":{"line":25,"file":"PartialGuildChannel.js","path":"src/structures"},"props":[]},{"id":"PartialGuildChannel#name","name":"name","description":"The name of this Guild Channel","memberof":"PartialGuildChannel","type":{"types":[[["string",""]]]},"meta":{"line":30,"file":"PartialGuildChannel.js","path":"src/structures"},"props":[]},{"id":"PartialGuildChannel#type","name":"type","description":"The type of this Guild Channel - `text` or `voice`","memberof":"PartialGuildChannel","type":{"types":[[["string",""]]]},"meta":{"line":35,"file":"PartialGuildChannel.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"PermissionOverwrites","name":"PermissionOverwrites","description":"Represents a permission overwrite for a Role or Member in a Guild Channel.","meta":{"line":4,"file":"PermissionOverwrites.js","path":"src/structures"},"methods":[{"id":"PermissionOverwrites#delete","name":"delete","description":"Delete this Permission Overwrite.","memberof":"PermissionOverwrites","meta":{"line":33,"file":"PermissionOverwrites.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["PermissionOverwrites",">"]]]},"params":[]}],"properties":[{"id":"PermissionOverwrites#channel","name":"channel","description":"The GuildChannel this overwrite is for","memberof":"PermissionOverwrites","type":{"types":[[["GuildChannel",""]]]},"meta":{"line":10,"file":"PermissionOverwrites.js","path":"src/structures"},"props":[]},{"id":"PermissionOverwrites#type","name":"type","description":"The type of this overwrite","memberof":"PermissionOverwrites","type":{"types":[[["string",""]]]},"meta":{"line":19,"file":"PermissionOverwrites.js","path":"src/structures"},"props":[]},{"id":"PermissionOverwrites#id","name":"id","description":"The ID of this overwrite, either a User ID or a Role ID","memberof":"PermissionOverwrites","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"PermissionOverwrites.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"Role","name":"Role","description":"Represents a Role on Discord","meta":{"line":6,"file":"Role.js","path":"src/structures"},"methods":[{"id":"Role#delete","name":"delete","description":"Deletes the role","memberof":"Role","examples":["// delete a role\nrole.delete()\n .then(r => console.log(`Deleted role ${r}`))\n .catch(console.log);"],"meta":{"line":79,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[]},{"id":"Role#edit","name":"edit","description":"Edits the role","memberof":"Role","examples":["// edit a role\nrole.edit({name: 'new role'})\n .then(r => console.log(`Edited role ${r}`))\n .catch(console.log);"],"meta":{"line":93,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[{"name":"data","description":"The new data for the role","type":{"types":[[["RoleData",""]]]}}]},{"id":"Role#setName","name":"setName","description":"Set a new name for the role","memberof":"Role","examples":["// set the name of the role\nrole.setName('new role')\n .then(r => console.log(`Edited name of role ${r}`))\n .catch(console.log);"],"meta":{"line":107,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[{"name":"name","description":"The new name of the role","type":{"types":[[["string",""]]]}}]},{"id":"Role#setColor","name":"setColor","description":"Set a new color for the role","memberof":"Role","examples":["// set the color of a role\nrole.setColor('#FF0000')\n .then(r => console.log(`Set color of role ${r}`))\n .catch(console.log);"],"meta":{"line":121,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[{"name":"color","description":"The new color for the role, either a hex string or a base 10 number","type":{"types":[[["number",""]],[["string",""]]]}}]},{"id":"Role#setHoist","name":"setHoist","description":"Set whether or not the role should be hoisted","memberof":"Role","examples":["// set the hoist of the role\nrole.setHoist(true)\n .then(r => console.log(`Role hoisted: ${r.hoist}`))\n .catch(console.log);"],"meta":{"line":135,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[{"name":"hoist","description":"Whether or not to hoist the role","type":{"types":[[["boolean",""]]]}}]},{"id":"Role#setPosition","name":"setPosition","description":"Set the position of the role","memberof":"Role","examples":["// set the position of the role\nrole.setPosition(1)\n .then(r => console.log(`Role position: ${r.position}`))\n .catch(console.log);"],"meta":{"line":149,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[{"name":"position","description":"The position of the role","type":{"types":[[["number",""]]]}}]},{"id":"Role#setPermissions","name":"setPermissions","description":"Set the permissions of the role","memberof":"Role","examples":["// set the permissions of the role\nrole.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS'])\n .then(r => console.log(`Role updated ${r}`))\n .catch(console.log);"],"meta":{"line":163,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Role",">"]]]},"params":[{"name":"permissions","description":"The permissions of the role","type":{"types":[[["Array",".<"],["string",">"]]]}}]},{"id":"Role#serialize","name":"serialize","description":"Get an object mapping permission names to whether or not the role enables that permission","memberof":"Role","examples":["// print the serialized role\nconsole.log(role.serialize());"],"meta":{"line":174,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["Object",".<"],["string",", "],["boolean",">"]]]},"params":[]},{"id":"Role#hasPermission","name":"hasPermission","description":"Whether or not the role includes the given permission","memberof":"Role","examples":["// see if a role can ban a member\nif (role.hasPermission('BAN_MEMBERS')) {\n console.log('This role can ban members');\n} else {\n console.log('This role can\\'t ban members');\n}"],"meta":{"line":195,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"permission","description":"The name of the permission to test","type":{"types":[[["PermissionResolvable",""]]]}},{"name":"explicit","description":"Whether to require the role to explicitly have the exact permission","optional":true,"type":{"types":[[["boolean",""]]]}}]},{"id":"Role#toString","name":"toString","description":"When concatenated with a string, this automatically concatenates the Role mention rather than the Role object.","memberof":"Role","meta":{"line":205,"file":"Role.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]}],"properties":[{"id":"Role#guild","name":"guild","description":"The guild that the role belongs to","memberof":"Role","type":{"types":[[["Guild",""]]]},"meta":{"line":12,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#client","name":"client","description":"The client that instantiated the role","memberof":"Role","type":{"types":[[["Client",""]]]},"meta":{"line":17,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#id","name":"id","description":"The ID of the role (unique to the guild it is part of)","memberof":"Role","type":{"types":[[["string",""]]]},"meta":{"line":37,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#name","name":"name","description":"The name of the role","memberof":"Role","type":{"types":[[["string",""]]]},"meta":{"line":42,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#color","name":"color","description":"The base 10 color of the role","memberof":"Role","type":{"types":[[["number",""]]]},"meta":{"line":47,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#hoist","name":"hoist","description":"If true, users that are part of this role will appear in a separate category in the users list","memberof":"Role","type":{"types":[[["boolean",""]]]},"meta":{"line":52,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#position","name":"position","description":"The position of the role in the role manager","memberof":"Role","type":{"types":[[["number",""]]]},"meta":{"line":57,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#permissions","name":"permissions","description":"The evaluated permissions number","memberof":"Role","type":{"types":[[["number",""]]]},"meta":{"line":62,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#managed","name":"managed","description":"Whether or not the role is managed by an external service","memberof":"Role","type":{"types":[[["boolean",""]]]},"meta":{"line":67,"file":"Role.js","path":"src/structures"},"props":[]},{"id":"Role#hexColor","name":"hexColor","description":"The hexadecimal version of the role color, with a leading hashtag.","memberof":"Role","type":{"types":[[["string",""]]]},"meta":{"line":214,"file":"Role.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"TextChannel","name":"TextChannel","description":"Represents a Server Text Channel on Discord.","meta":{"line":10,"file":"TextChannel.js","path":"src/structures"},"extends":["GuildChannel"],"methods":[{"id":"TextChannel#bulkDelete","name":"bulkDelete","description":"Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after.","memberof":"TextChannel","meta":{"line":24,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"params":[{"name":"messages","description":"The messages to delete","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]],[["Array",".<"],["Message",">"]]]}}],"implements":["TextBasedChannel#bulkDelete"]},{"id":"TextChannel#sendMessage","name":"sendMessage","description":"Send a message to this channel","memberof":"TextChannel","examples":["// send a message\nchannel.sendMessage('hello!')\n .then(message => console.log(`Sent message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":53,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendMessage"]},{"id":"TextChannel#sendTTSMessage","name":"sendTTSMessage","description":"Send a text-to-speech message to this channel","memberof":"TextChannel","examples":["// send a TTS message\nchannel.sendTTSMessage('hello!')\n .then(message => console.log(`Sent tts message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":68,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendTTSMessage"]},{"id":"TextChannel#sendFile","name":"sendFile","description":"Send a file to this channel","memberof":"TextChannel","meta":{"line":78,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"attachment","description":"The file to send","type":{"types":[[["FileResolvable",""]]]}},{"name":"fileName","description":"The name and extension of the file","optional":true,"type":{"types":[[["string",""]]]}}],"implements":["TextBasedChannel#sendFile"]},{"id":"TextChannel#fetchMessages","name":"fetchMessages","description":"Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects.","memberof":"TextChannel","examples":["// get messages\nchannel.fetchMessages({limit: 10})\n .then(messages => console.log(`Received ${messages.size} messages`))\n .catch(console.log);"],"meta":{"line":122,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"options","description":"The query parameters to pass in","optional":true,"type":{"types":[[["ChannelLogsQueryOptions",""]]]}}],"implements":["TextBasedChannel#fetchMessages"]},{"id":"TextChannel#startTyping","name":"startTyping","description":"Starts a typing indicator in the channel.","memberof":"TextChannel","examples":["// start typing in a channel\nchannel.startTyping();"],"meta":{"line":143,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"count","description":"The number of times startTyping should be considered to have been called","optional":true,"type":{"types":[[["number",""]]]}}],"implements":["TextBasedChannel#startTyping"]},{"id":"TextChannel#stopTyping","name":"stopTyping","description":"Stops the typing indicator in the channel.\nThe indicator will only stop if this is called as many times as startTyping().\nIt can take a few seconds for the Client User to stop typing.","memberof":"TextChannel","examples":["// stop typing in a channel\nchannel.stopTyping();","// force typing to fully stop in a channel\nchannel.stopTyping(true);"],"meta":{"line":171,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"force","description":"Whether or not to reset the call count and force the indicator to stop","optional":true,"type":{"types":[[["boolean",""]]]}}],"implements":["TextBasedChannel#stopTyping"]},{"id":"TextChannel#createCollector","name":"createCollector","description":"Creates a Message Collector","memberof":"TextChannel","examples":["// create a message collector\nconst collector = channel.createCollector(\n m => m.content.includes('discord'),\n { time: 15000 }\n);\ncollector.on('message', m => console.log(`Collected ${m.content}`));\ncollector.on('end', collected => console.log(`Collected ${collected.size} items`));"],"meta":{"line":213,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["MessageCollector",""]]]},"params":[{"name":"filter","description":"The filter to create the collector with","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"The options to pass to the collector","optional":true,"type":{"types":[[["CollectorOptions",""]]]}}],"implements":["TextBasedChannel#createCollector"]},{"id":"TextChannel#awaitMessages","name":"awaitMessages","description":"Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified\nfilter.","memberof":"TextChannel","examples":["// await !vote messages\nconst filter = m => m.content.startsWith('!vote');\n// errors: ['time'] treats ending because of the time limit as an error\nchannel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] })\n .then(collected => console.log(collected.size))\n .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));"],"meta":{"line":241,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"filter","description":"The filter function to use","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"Optional options to pass to the internal collector","optional":true,"type":{"types":[[["AwaitMessagesOptions",""]]]}}],"implements":["TextBasedChannel#awaitMessages"]},{"id":"TextChannel#fetchPinnedMessages","name":"fetchPinnedMessages","description":"Fetches the pinned messages of this Channel and returns a Collection of them.","memberof":"TextChannel","meta":{"line":267,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[],"implements":["TextBasedChannel#fetchPinnedMessages"]},{"id":"TextChannel#equals","name":"equals","description":"Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel.\nIn most cases, a simple `channel.id === channel2.id` will do, and is much faster too.","memberof":"TextChannel","inherits":"GuildChannel#equals","inherited":true,"meta":{"line":54,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"channel","description":"The channel to compare this channel to","type":{"types":[[["GuildChannel",""]]]}}]},{"id":"TextChannel#permissionsFor","name":"permissionsFor","description":"Gets the overall set of permissions for a user in this channel, taking into account roles and permission\noverwrites.","memberof":"TextChannel","inherits":"GuildChannel#permissionsFor","inherited":true,"meta":{"line":81,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["EvaluatedPermissions",""]]]},"params":[{"name":"member","description":"The user that you want to obtain the overall permissions for","type":{"types":[[["GuildMemberResolvable",""]]]}}]},{"id":"TextChannel#overwritePermissions","name":"overwritePermissions","description":"Overwrites the permissions for a user or role in this channel.","memberof":"TextChannel","examples":["// overwrite permissions for a message author\nmessage.channel.overwritePermissions(message.author, {\n SEND_MESSAGES: false\n})\n.then(() => console.log('Done!'))\n.catch(console.log);"],"inherits":"GuildChannel#overwritePermissions","inherited":true,"meta":{"line":155,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",""]]]},"params":[{"name":"userOrRole","description":"The user or role to update","type":{"types":[[["Role",""]],[["UserResolvable",""]]]}},{"name":"options","description":"The configuration for the update","type":{"types":[[["PermissionOverwriteOptions",""]]]}}]},{"id":"TextChannel#setName","name":"setName","description":"Set a new name for the Guild Channel","memberof":"TextChannel","examples":["// set a new channel name\nchannel.setName('not general')\n .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))\n .catch(console.log);"],"inherits":"GuildChannel#setName","inherited":true,"meta":{"line":205,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"name","description":"The new name for the guild channel","type":{"types":[[["string",""]]]}}]},{"id":"TextChannel#setPosition","name":"setPosition","description":"Set a new position for the Guild Channel","memberof":"TextChannel","examples":["// set a new channel position\nchannel.setPosition(2)\n .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))\n .catch(console.log);"],"inherits":"GuildChannel#setPosition","inherited":true,"meta":{"line":219,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"position","description":"The new position for the guild channel","type":{"types":[[["number",""]]]}}]},{"id":"TextChannel#setTopic","name":"setTopic","description":"Set a new topic for the Guild Channel","memberof":"TextChannel","examples":["// set a new channel topic\nchannel.setTopic('needs more rate limiting')\n .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))\n .catch(console.log);"],"inherits":"GuildChannel#setTopic","inherited":true,"meta":{"line":233,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"topic","description":"The new topic for the guild channel","type":{"types":[[["string",""]]]}}]},{"id":"TextChannel#toString","name":"toString","description":"When concatenated with a string, this automatically returns the Channel's mention instead of the Channel object.","memberof":"TextChannel","examples":["// Outputs: Hello from #general\nconsole.log(`Hello from ${channel}`);","// Outputs: Hello from #general\nconsole.log('Hello from ' + channel);"],"inherits":"GuildChannel#toString","inherited":true,"meta":{"line":247,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]},{"id":"TextChannel#createInvite","name":"createInvite","description":"Create an invite to this Guild Channel","memberof":"TextChannel","inherits":"GuildChannel#createInvite","inherited":true,"meta":{"line":268,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Invite",">"]]]},"params":[{"name":"options","description":"The options for the invite","optional":true,"type":{"types":[[["InviteOptions",""]]]}}]}],"properties":[{"id":"TextChannel#lastMessageID","name":"lastMessageID","description":"The ID of the last message in the channel, if one was sent.","memberof":"TextChannel","type":{"types":[[["string",""]]]},"meta":{"line":22,"file":"TextChannel.js","path":"src/structures"},"props":[]},{"id":"TextChannel#messages","name":"messages","description":"A Collection containing the messages sent to this channel.","memberof":"TextChannel","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"meta":{"line":16,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"TextChannel#typing","name":"typing","description":"Whether or not the typing indicator is being shown in the channel.","memberof":"TextChannel","type":{"types":[[["boolean",""]]]},"meta":{"line":186,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"TextChannel#typingCount","name":"typingCount","description":"Number of times `startTyping` has been called.","memberof":"TextChannel","type":{"types":[[["number",""]]]},"meta":{"line":194,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"TextChannel#topic","name":"topic","description":"The topic of the Guild Channel, if there is one.","memberof":"TextChannel","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"TextChannel#position","name":"position","description":"The position of the channel in the list.","memberof":"TextChannel","type":{"types":[[["number",""]]]},"meta":{"line":29,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"TextChannel#name","name":"name","description":"The name of the Guild Channel","memberof":"TextChannel","type":{"types":[[["string",""]]]},"meta":{"line":34,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"TextChannel#permissionOverwrites","name":"permissionOverwrites","description":"A map of permission overwrites in this channel for roles and users.","memberof":"TextChannel","type":{"types":[[["Collection",".<"],["string",", "],["PermissionOverwrites",">"]]]},"meta":{"line":40,"file":"GuildChannel.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"User","name":"User","description":"Represents a User on Discord.","meta":{"line":8,"file":"User.js","path":"src/structures"},"methods":[{"id":"User#toString","name":"toString","description":"When concatenated with a string, this automatically concatenates the User's mention instead of the User object.","memberof":"User","examples":["// logs: Hello from <@123456789>!\nconsole.log(`Hello from ${user}!`);"],"meta":{"line":63,"file":"User.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]},{"id":"User#deleteDM","name":"deleteDM","description":"Deletes a DM Channel (if one exists) between the Client and the User. Resolves with the Channel if successful.","memberof":"User","meta":{"line":81,"file":"User.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["DMChannel",">"]]]},"params":[]},{"id":"User#equals","name":"equals","description":"Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.\nIt is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.","memberof":"User","meta":{"line":91,"file":"User.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"user","description":"The user to compare","type":{"types":[[["User",""]]]}}]},{"id":"User#sendMessage","name":"sendMessage","description":"Send a message to this channel","memberof":"User","examples":["// send a message\nchannel.sendMessage('hello!')\n .then(message => console.log(`Sent message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":53,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendMessage"]},{"id":"User#sendTTSMessage","name":"sendTTSMessage","description":"Send a text-to-speech message to this channel","memberof":"User","examples":["// send a TTS message\nchannel.sendTTSMessage('hello!')\n .then(message => console.log(`Sent tts message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":68,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}],"implements":["TextBasedChannel#sendTTSMessage"]},{"id":"User#sendFile","name":"sendFile","description":"Send a file to this channel","memberof":"User","meta":{"line":78,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"attachment","description":"The file to send","type":{"types":[[["FileResolvable",""]]]}},{"name":"fileName","description":"The name and extension of the file","optional":true,"type":{"types":[[["string",""]]]}}],"implements":["TextBasedChannel#sendFile"]}],"properties":[{"id":"User#username","name":"username","description":"The username of the User","memberof":"User","type":{"types":[[["string",""]]]},"meta":{"line":19,"file":"User.js","path":"src/structures"},"props":[]},{"id":"User#id","name":"id","description":"The ID of the User","memberof":"User","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"User.js","path":"src/structures"},"props":[]},{"id":"User#discriminator","name":"discriminator","description":"A discriminator based on username for the User","memberof":"User","type":{"types":[[["string",""]]]},"meta":{"line":29,"file":"User.js","path":"src/structures"},"props":[]},{"id":"User#avatar","name":"avatar","description":"The ID of the user's avatar","memberof":"User","type":{"types":[[["string",""]]]},"meta":{"line":34,"file":"User.js","path":"src/structures"},"props":[]},{"id":"User#bot","name":"bot","description":"Whether or not the User is a Bot.","memberof":"User","type":{"types":[[["boolean",""]]]},"meta":{"line":39,"file":"User.js","path":"src/structures"},"props":[]},{"id":"User#status","name":"status","description":"The status of the user:\n\n* **`online`** - user is online\n* **`offline`** - user is offline\n* **`idle`** - user is AFK","memberof":"User","type":{"types":[[["string",""]]]},"meta":{"line":48,"file":"User.js","path":"src/structures"},"props":[]},{"id":"User#game","name":"game","description":"The game that the user is playing, `null` if they aren't playing a game.","memberof":"User","type":{"types":[[["string",""]]]},"meta":{"line":53,"file":"User.js","path":"src/structures"},"props":[]},{"id":"User#avatarURL","name":"avatarURL","description":"A link to the user's avatar (if they have one, otherwise null)","memberof":"User","type":{"types":[[["string",""]]]},"meta":{"line":72,"file":"User.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"VoiceChannel","name":"VoiceChannel","description":"Represents a Server Voice Channel on Discord.","meta":{"line":8,"file":"VoiceChannel.js","path":"src/structures"},"extends":["GuildChannel"],"methods":[{"id":"VoiceChannel#setBitrate","name":"setBitrate","description":"Sets the bitrate of the channel","memberof":"VoiceChannel","examples":["// set the bitrate of a voice channel\nvoiceChannel.setBitrate(48000)\n .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`))\n .catch(console.log);"],"meta":{"line":43,"file":"VoiceChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["VoiceChannel",">"]]]},"params":[{"name":"bitrate","description":"The new bitrate","type":{"types":[[["number",""]]]}}]},{"id":"VoiceChannel#join","name":"join","description":"Attempts to join this Voice Channel","memberof":"VoiceChannel","examples":["// join a voice channel\nvoiceChannel.join()\n .then(connection => console.log('Connected!'))\n .catch(console.log);"],"meta":{"line":56,"file":"VoiceChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["VoiceConnection",">"]]]},"params":[]},{"id":"VoiceChannel#leave","name":"leave","description":"Leaves this voice channel","memberof":"VoiceChannel","examples":["// leave a voice channel\nvoiceChannel.leave();"],"meta":{"line":66,"file":"VoiceChannel.js","path":"src/structures"},"returns":{"types":[[["null",""]]]},"params":[]},{"id":"VoiceChannel#equals","name":"equals","description":"Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel.\nIn most cases, a simple `channel.id === channel2.id` will do, and is much faster too.","memberof":"VoiceChannel","inherits":"GuildChannel#equals","inherited":true,"meta":{"line":54,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"channel","description":"The channel to compare this channel to","type":{"types":[[["GuildChannel",""]]]}}]},{"id":"VoiceChannel#permissionsFor","name":"permissionsFor","description":"Gets the overall set of permissions for a user in this channel, taking into account roles and permission\noverwrites.","memberof":"VoiceChannel","inherits":"GuildChannel#permissionsFor","inherited":true,"meta":{"line":81,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["EvaluatedPermissions",""]]]},"params":[{"name":"member","description":"The user that you want to obtain the overall permissions for","type":{"types":[[["GuildMemberResolvable",""]]]}}]},{"id":"VoiceChannel#overwritePermissions","name":"overwritePermissions","description":"Overwrites the permissions for a user or role in this channel.","memberof":"VoiceChannel","examples":["// overwrite permissions for a message author\nmessage.channel.overwritePermissions(message.author, {\n SEND_MESSAGES: false\n})\n.then(() => console.log('Done!'))\n.catch(console.log);"],"inherits":"GuildChannel#overwritePermissions","inherited":true,"meta":{"line":155,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",""]]]},"params":[{"name":"userOrRole","description":"The user or role to update","type":{"types":[[["Role",""]],[["UserResolvable",""]]]}},{"name":"options","description":"The configuration for the update","type":{"types":[[["PermissionOverwriteOptions",""]]]}}]},{"id":"VoiceChannel#setName","name":"setName","description":"Set a new name for the Guild Channel","memberof":"VoiceChannel","examples":["// set a new channel name\nchannel.setName('not general')\n .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))\n .catch(console.log);"],"inherits":"GuildChannel#setName","inherited":true,"meta":{"line":205,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"name","description":"The new name for the guild channel","type":{"types":[[["string",""]]]}}]},{"id":"VoiceChannel#setPosition","name":"setPosition","description":"Set a new position for the Guild Channel","memberof":"VoiceChannel","examples":["// set a new channel position\nchannel.setPosition(2)\n .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))\n .catch(console.log);"],"inherits":"GuildChannel#setPosition","inherited":true,"meta":{"line":219,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"position","description":"The new position for the guild channel","type":{"types":[[["number",""]]]}}]},{"id":"VoiceChannel#setTopic","name":"setTopic","description":"Set a new topic for the Guild Channel","memberof":"VoiceChannel","examples":["// set a new channel topic\nchannel.setTopic('needs more rate limiting')\n .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))\n .catch(console.log);"],"inherits":"GuildChannel#setTopic","inherited":true,"meta":{"line":233,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["GuildChannel",">"]]]},"params":[{"name":"topic","description":"The new topic for the guild channel","type":{"types":[[["string",""]]]}}]},{"id":"VoiceChannel#toString","name":"toString","description":"When concatenated with a string, this automatically returns the Channel's mention instead of the Channel object.","memberof":"VoiceChannel","examples":["// Outputs: Hello from #general\nconsole.log(`Hello from ${channel}`);","// Outputs: Hello from #general\nconsole.log('Hello from ' + channel);"],"inherits":"GuildChannel#toString","inherited":true,"meta":{"line":247,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["string",""]]]},"params":[]},{"id":"VoiceChannel#createInvite","name":"createInvite","description":"Create an invite to this Guild Channel","memberof":"VoiceChannel","inherits":"GuildChannel#createInvite","inherited":true,"meta":{"line":268,"file":"GuildChannel.js","path":"src/structures"},"returns":{"types":[[["Promise",".<"],["Invite",">"]]]},"params":[{"name":"options","description":"The options for the invite","optional":true,"type":{"types":[[["InviteOptions",""]]]}}]}],"properties":[{"id":"VoiceChannel#members","name":"members","description":"The members in this Voice Channel.","memberof":"VoiceChannel","type":{"types":[[["Collection",".<"],["string",", "],["GuildMember",">"]]]},"meta":{"line":15,"file":"VoiceChannel.js","path":"src/structures"},"props":[]},{"id":"VoiceChannel#bitrate","name":"bitrate","description":"The bitrate of this voice channel","memberof":"VoiceChannel","type":{"types":[[["number",""]]]},"meta":{"line":24,"file":"VoiceChannel.js","path":"src/structures"},"props":[]},{"id":"VoiceChannel#userLimit","name":"userLimit","description":"The maximum amount of users allowed in this channel - 0 means unlimited.","memberof":"VoiceChannel","type":{"types":[[["number",""]]]},"meta":{"line":29,"file":"VoiceChannel.js","path":"src/structures"},"props":[]},{"id":"VoiceChannel#topic","name":"topic","description":"The topic of the Guild Channel, if there is one.","memberof":"VoiceChannel","type":{"types":[[["string",""]]]},"meta":{"line":24,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"VoiceChannel#position","name":"position","description":"The position of the channel in the list.","memberof":"VoiceChannel","type":{"types":[[["number",""]]]},"meta":{"line":29,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"VoiceChannel#name","name":"name","description":"The name of the Guild Channel","memberof":"VoiceChannel","type":{"types":[[["string",""]]]},"meta":{"line":34,"file":"GuildChannel.js","path":"src/structures"},"props":[]},{"id":"VoiceChannel#permissionOverwrites","name":"permissionOverwrites","description":"A map of permission overwrites in this channel for roles and users.","memberof":"VoiceChannel","type":{"types":[[["Collection",".<"],["string",", "],["PermissionOverwrites",">"]]]},"meta":{"line":40,"file":"GuildChannel.js","path":"src/structures"},"props":[]}],"events":[]},{"id":"Collection","name":"Collection","description":"A utility class to help make it easier to access the data stores","meta":{"line":5,"file":"Collection.js","path":"src/util"},"extends":["Map"],"methods":[{"id":"Collection#array","name":"array","description":"Returns an ordered array of the values of this collection.","memberof":"Collection","examples":["// identical to:\nArray.from(collection.values());"],"meta":{"line":13,"file":"Collection.js","path":"src/util"},"returns":{"types":[[["array",""]]]},"params":[]},{"id":"Collection#first","name":"first","description":"Returns the first item in this collection.","memberof":"Collection","examples":["// identical to:\nArray.from(collection.values())[0];"],"meta":{"line":24,"file":"Collection.js","path":"src/util"},"returns":{"types":[["*",""]]},"params":[]},{"id":"Collection#last","name":"last","description":"Returns the last item in this collection. This is a relatively slow operation,\nsince an array copy of the values must be made to find the last element.","memberof":"Collection","meta":{"line":33,"file":"Collection.js","path":"src/util"},"returns":{"types":[["*",""]]},"params":[]},{"id":"Collection#random","name":"random","description":"Returns a random item from this collection. This is a relatively slow operation,\nsince an array copy of the values must be made to find a random element.","memberof":"Collection","meta":{"line":43,"file":"Collection.js","path":"src/util"},"returns":{"types":[["*",""]]},"params":[]},{"id":"Collection#deleteAll","name":"deleteAll","description":"If the items in this collection have a delete method (e.g. messages), invoke\nthe delete method. Returns an array of promises","memberof":"Collection","meta":{"line":53,"file":"Collection.js","path":"src/util"},"returns":{"types":[[["Array",".<"],["Promise",">"]]]},"params":[]},{"id":"Collection#findAll","name":"findAll","description":"Returns an array of items where `item[key] === value` of the collection","memberof":"Collection","examples":["collection.getAll('username', 'Bob');"],"meta":{"line":69,"file":"Collection.js","path":"src/util"},"returns":{"types":[[["array",""]]]},"params":[{"name":"key","description":"The key to filter by","type":{"types":[[["string",""]]]}},{"name":"value","description":"The expected value","type":{"types":[["*",""]]}}]},{"id":"Collection#find","name":"find","description":"Returns a single item where `item[key] === value`","memberof":"Collection","examples":["collection.get('id', '123123...');"],"meta":{"line":87,"file":"Collection.js","path":"src/util"},"returns":{"types":[["*",""]]},"params":[{"name":"key","description":"The key to filter by","type":{"types":[[["string",""]]]}},{"name":"value","description":"The expected value","type":{"types":[["*",""]]}}]},{"id":"Collection#exists","name":"exists","description":"Returns true if the collection has an item where `item[key] === value`","memberof":"Collection","examples":["if (collection.exists('id', '123123...')) {\n console.log('user here!');\n}"],"meta":{"line":106,"file":"Collection.js","path":"src/util"},"returns":{"types":[[["boolean",""]]]},"params":[{"name":"key","description":"The key to filter by","type":{"types":[[["string",""]]]}},{"name":"value","description":"The expected value","type":{"types":[["*",""]]}}]},{"id":"Collection#filter","name":"filter","description":"Identical to\n[Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter),\nbut returns a Collection instead of an Array.","memberof":"Collection","meta":{"line":118,"file":"Collection.js","path":"src/util"},"returns":{"types":[[["Collection",""]]]},"params":[{"name":"callback","description":"Function used to filter (should return a boolean)","type":{"types":[[["function",""]]]}},{"name":"thisArg","description":"Value to set as this when filtering","optional":true,"type":{"types":[[["Object",""]]]}}]},{"id":"Collection#map","name":"map","description":"Functionally identical shortcut to `collection.array().map(...)`.","memberof":"Collection","meta":{"line":131,"file":"Collection.js","path":"src/util"},"returns":{"types":[[["array",""]]]},"params":[{"name":"callback","description":"Function that produces an element of the new Array, taking three arguments","type":{"types":[[["function",""]]]}},{"name":"thisArg","description":"Optional. Value to use as this when executing callback.","optional":true,"type":{"types":[["*",""]]}}]}],"properties":[],"events":[]}],"interfaces":[{"id":"TextBasedChannel","name":"TextBasedChannel","description":"Interface for classes that have text-channel-like features","meta":{"line":10,"file":"TextBasedChannel.js","path":"src/structures/interface"},"methods":[{"id":"TextBasedChannel#bulkDelete","name":"bulkDelete","description":"Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after.","memberof":"TextBasedChannel","meta":{"line":24,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"params":[{"name":"messages","description":"The messages to delete","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]],[["Array",".<"],["Message",">"]]]}}]},{"id":"TextBasedChannel#sendMessage","name":"sendMessage","description":"Send a message to this channel","memberof":"TextBasedChannel","examples":["// send a message\nchannel.sendMessage('hello!')\n .then(message => console.log(`Sent message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":53,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}]},{"id":"TextBasedChannel#sendTTSMessage","name":"sendTTSMessage","description":"Send a text-to-speech message to this channel","memberof":"TextBasedChannel","examples":["// send a TTS message\nchannel.sendTTSMessage('hello!')\n .then(message => console.log(`Sent tts message: ${message.content}`))\n .catch(console.log);"],"meta":{"line":68,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"content","description":"The content to send","type":{"types":[[["string",""]]]}},{"name":"options","description":"The options to provide","optional":true,"type":{"types":[[["MessageOptions",""]]]}}]},{"id":"TextBasedChannel#sendFile","name":"sendFile","description":"Send a file to this channel","memberof":"TextBasedChannel","meta":{"line":78,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Message",">"]]]},"params":[{"name":"attachment","description":"The file to send","type":{"types":[[["FileResolvable",""]]]}},{"name":"fileName","description":"The name and extension of the file","optional":true,"type":{"types":[[["string",""]]]}}]},{"id":"TextBasedChannel#fetchMessages","name":"fetchMessages","description":"Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects.","memberof":"TextBasedChannel","examples":["// get messages\nchannel.fetchMessages({limit: 10})\n .then(messages => console.log(`Received ${messages.size} messages`))\n .catch(console.log);"],"meta":{"line":122,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"options","description":"The query parameters to pass in","optional":true,"type":{"types":[[["ChannelLogsQueryOptions",""]]]}}]},{"id":"TextBasedChannel#startTyping","name":"startTyping","description":"Starts a typing indicator in the channel.","memberof":"TextBasedChannel","examples":["// start typing in a channel\nchannel.startTyping();"],"meta":{"line":143,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"count","description":"The number of times startTyping should be considered to have been called","optional":true,"type":{"types":[[["number",""]]]}}]},{"id":"TextBasedChannel#stopTyping","name":"stopTyping","description":"Stops the typing indicator in the channel.\nThe indicator will only stop if this is called as many times as startTyping().\nIt can take a few seconds for the Client User to stop typing.","memberof":"TextBasedChannel","examples":["// stop typing in a channel\nchannel.stopTyping();","// force typing to fully stop in a channel\nchannel.stopTyping(true);"],"meta":{"line":171,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["null",""]]]},"params":[{"name":"force","description":"Whether or not to reset the call count and force the indicator to stop","optional":true,"type":{"types":[[["boolean",""]]]}}]},{"id":"TextBasedChannel#createCollector","name":"createCollector","description":"Creates a Message Collector","memberof":"TextBasedChannel","examples":["// create a message collector\nconst collector = channel.createCollector(\n m => m.content.includes('discord'),\n { time: 15000 }\n);\ncollector.on('message', m => console.log(`Collected ${m.content}`));\ncollector.on('end', collected => console.log(`Collected ${collected.size} items`));"],"meta":{"line":213,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["MessageCollector",""]]]},"params":[{"name":"filter","description":"The filter to create the collector with","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"The options to pass to the collector","optional":true,"type":{"types":[[["CollectorOptions",""]]]}}]},{"id":"TextBasedChannel#awaitMessages","name":"awaitMessages","description":"Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified\nfilter.","memberof":"TextBasedChannel","examples":["// await !vote messages\nconst filter = m => m.content.startsWith('!vote');\n// errors: ['time'] treats ending because of the time limit as an error\nchannel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] })\n .then(collected => console.log(collected.size))\n .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));"],"meta":{"line":241,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[{"name":"filter","description":"The filter function to use","type":{"types":[[["CollectorFilterFunction",""]]]}},{"name":"options","description":"Optional options to pass to the internal collector","optional":true,"type":{"types":[[["AwaitMessagesOptions",""]]]}}]},{"id":"TextBasedChannel#fetchPinnedMessages","name":"fetchPinnedMessages","description":"Fetches the pinned messages of this Channel and returns a Collection of them.","memberof":"TextBasedChannel","meta":{"line":267,"file":"TextBasedChannel.js","path":"src/structures/interface"},"returns":{"types":[[["Promise",".<"],["Collection",".<"],["string",", "],["Message",">>"]]]},"params":[]}],"properties":[{"id":"TextBasedChannel#messages","name":"messages","description":"A Collection containing the messages sent to this channel.","memberof":"TextBasedChannel","type":{"types":[[["Collection",".<"],["string",", "],["Message",">"]]]},"meta":{"line":16,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"TextBasedChannel#typing","name":"typing","description":"Whether or not the typing indicator is being shown in the channel.","memberof":"TextBasedChannel","type":{"types":[[["boolean",""]]]},"meta":{"line":186,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]},{"id":"TextBasedChannel#typingCount","name":"typingCount","description":"Number of times `startTyping` has been called.","memberof":"TextBasedChannel","type":{"types":[[["number",""]]]},"meta":{"line":194,"file":"TextBasedChannel.js","path":"src/structures/interface"},"props":[]}],"events":[]}],"typedefs":[{"id":"UserResolvable","name":"UserResolvable","description":"Data that resolves to give a User object. This can be:\n* A User object\n* A User ID\n* A Message (resolves to the message author)\n* A Guild (owner of the guild)\n* A Guild Member","type":{"types":[[["User",""]],[["string",""]],[["Message",""]],[["Guild",""]],[["GuildMember",""]]]},"meta":{"line":25,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"GuildResolvable","name":"GuildResolvable","description":"Data that resolves to give a Guild object. This can be:\n* A Guild object","type":{"types":[[["Guild",""]]]},"meta":{"line":56,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"GuildMemberResolvable","name":"GuildMemberResolvable","description":"Data that resolves to give a GuildMember object. This can be:\n* A GuildMember object\n* A User object","type":{"types":[[["Guild",""]]]},"meta":{"line":73,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"Base64Resolvable","name":"Base64Resolvable","description":"Data that resolves to give a Base64 string, typically for image uploading. This can be:\n* A Buffer\n* A Base64 string","type":{"types":[[["Buffer",""]],[["string",""]]]},"meta":{"line":96,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"ChannelResolvable","name":"ChannelResolvable","description":"Data that can be resolved to give a Channel. This can be:\n* An instance of a Channel\n* An ID of a Channel","type":{"types":[[["Channel",""]],[["string",""]]]},"meta":{"line":113,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"PermissionResolvable","name":"PermissionResolvable","description":"Data that can be resolved to give a permission number. This can be:\n* A string\n* A permission number","type":{"types":[[["string",""]],[["number",""]]]},"meta":{"line":131,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"StringResolvable","name":"StringResolvable","description":"Data that can be resolved to give a string. This can be:\n* A string\n* An Array (joined with a new line delimiter to give a string)\n* Any value","type":{"types":[[["string",""]],[["Array",""]],["*",""]]},"meta":{"line":149,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"FileResolvable","name":"FileResolvable","description":"Data that can be resolved to give a Buffer. This can be:\n* A Buffer\n* The path to a local file\n* A URL","type":{"types":[[["string",""]],[["Buffer",""]]]},"meta":{"line":168,"file":"ClientDataResolver.js","path":"src/client"}},{"id":"PermissionOverwriteOptions","name":"PermissionOverwriteOptions","description":"An object mapping permission flags to `true` (enabled) or `false` (disabled)\n```js\n{\n 'SEND_MESSAGES': true,\n 'ATTACH_FILES': false,\n}\n```","type":{"types":[[["Object",""]]]},"meta":{"line":131,"file":"GuildChannel.js","path":"src/structures"}},{"id":"InviteOptions","name":"InviteOptions","description":"Options given when creating a Guild Channel Invite:\n```js\n{\n temporary: false, // whether the invite should kick users after 24hrs if they are not given a new role\n maxAge: 0, // the time in seconds the invite expires in\n maxUses: 0, // the maximum amount of uses for this invite\n}\n```","type":{"types":[[["Object",""]]]},"meta":{"line":251,"file":"GuildChannel.js","path":"src/structures"}},{"id":"MessageOptions","name":"MessageOptions","description":"Options that can be passed into sendMessage or sendTTSMessage:\n```js\n{\n tts: false,\n nonce: '',\n};\n```","type":{"types":[[["Object",""]]]},"meta":{"line":31,"file":"TextBasedChannel.js","path":"src/structures/interface"}},{"id":"ChannelLogsQueryOptions","name":"ChannelLogsQueryOptions","description":"The parameters to pass in when requesting previous messages from a channel. `around`, `before` and\n`after` are mutually exclusive. All the parameters are optional.\n```js\n{\n limit: 30, // the message limit, defaults to 50\n before: '123', // gets messages before the given message ID\n after: '123', // gets messages after the given message ID\n around: '123', // gets messages around the given message ID\n}\n```","type":{"types":[[["Object",""]]]},"meta":{"line":98,"file":"TextBasedChannel.js","path":"src/structures/interface"}},{"id":"AwaitMessagesOptions","name":"AwaitMessagesOptions","description":"An object containing the same properties as CollectorOptions, but a few more:\n```js\n{\n errors: [], // an array of stop/end reasons that cause the promise to reject.\n}\n```","type":{"types":[[["Object",""]]]},"meta":{"line":217,"file":"TextBasedChannel.js","path":"src/structures/interface"}},{"id":"CollectorFilterFunction","name":"CollectorFilterFunction","description":"A function that takes a Message object and a MessageCollector and returns a boolean.\n```js\nfunction(message, collector) {\n if (message.content.includes('discord')) {\n return true; // passed the filter test\n }\n return false; // failed the filter test\n}\n```","type":{"types":[[["function",""]]]},"meta":{"line":287,"file":"TextBasedChannel.js","path":"src/structures/interface"}},{"id":"CollectorOptions","name":"CollectorOptions","description":"An object containing options used to configure a MessageCollector. All properties are optional.\n```js\n{\n time: null, // time in milliseconds. If specified, the collector ends after this amount of time.\n max: null, // the maximum amount of messages to handle before ending.\n}\n```","type":{"types":[[["Object",""]]]},"meta":{"line":300,"file":"TextBasedChannel.js","path":"src/structures/interface"}},{"id":"ClientOptions","name":"ClientOptions","description":"Options that can be passed to a client:\n```js\n{\n ws: {\n large_threshold: 250,\n compress: true,\n properties: {\n $os: process ? process.platform : 'discord.js',\n $browser: 'discord.js',\n $device: 'discord.js',\n $referrer: '',\n $referring_domain: '',\n },\n },\n protocol_version: 6,\n max_message_cache: 200,\n rest_ws_bridge_timeout: 5000,\n api_request_method: 'sequential',\n shard_id: 0,\n shard_count: 0,\n fetch_all_members: false,\n};\n```","type":{"types":[[["Object",""]]]},"meta":{"line":1,"file":"Constants.js","path":"src/util"}}],"custom":{"general":[{"category":"general","name":"Welcome","data":"\r\n \r\n
\r\n \r\n
\r\n\r\n[](https://travis-ci.org/hydrabolt/discord.js) [](http://discordjs.readthedocs.org/en/latest/?badge=latest)\r\n\r\n[](https://nodei.co/npm/discord.js/)\r\n\r\n# Welcome!\r\nWelcome to the discord.js rewrite documentation. The rewrite has taken a lot of time, but it should be much more\r\nstable and performance-friendly than previous versions.\r\n\r\n## Installation\r\n`npm i --save hydrabolt/discord.js#indev-rewrite`"},{"category":"general","name":"Updating your code","data":"# About the Rewrite\r\nThe rewrite takes a much more OOP approach than previous versions, which allows code to be much more manageable.\r\nIt's been rebuilt from the ground up and should be much more stable, fixing caching issues that affected\r\nolder versions and it also has support for new Discord Features, such as emojis.\r\n\r\n## Upgrading your code\r\nThe rewrite has a _lot_ of breaking changes. Major methods, e.g. `client.sendMessage(channel, message)` have been moved\r\nfrom the Client class towards their respective classes - `textChannel.sendMessage(message)`. You can find out the full\r\nextent of these changes by looking at the classes in the documentation.\r\n\r\nAdditionally, some event names and parameters have changed - you should revisit these.\r\n"}],"examples":[{"category":"examples","name":"Ping Pong","data":"```js\n/*\n A ping pong bot, whenever you send \"ping\", it replies \"pong\".\n*/\n\n// import the discord.js module\nconst Discord = require('discord.js');\n\n// create an instance of a Discord Client, and call it bot\nconst bot = new Discord.Client();\n\n// the token of your bot - https://discordapp.com/developers/applications/me\nconst token = 'your bot token here';\n\n// the ready event is vital, it means that your bot will only start reacting to information\n// from Discord _after_ ready is emitted.\nbot.on('ready', () => {\n console.log('I am ready!');\n});\n\n// create an event listener for messages\nbot.on('message', message => {\n // if the message is \"ping\",\n if (message.content === 'ping') {\n // send \"pong\" to the same channel.\n message.channel.sendMessage('pong');\n }\n});\n\n// log our bot in\nbot.login(token);\n\n```"},{"category":"examples","name":"Avatars","data":"```js\n/*\n Send a user a link to their avatar\n*/\n\n// import the discord.js module\nconst Discord = require('discord.js');\n\n// create an instance of a Discord Client, and call it bot\nconst bot = new Discord.Client();\n\n// the token of your bot - https://discordapp.com/developers/applications/me\nconst token = 'your bot token here';\n\n// the ready event is vital, it means that your bot will only start reacting to information\n// from Discord _after_ ready is emitted.\nbot.on('ready', () => {\n console.log('I am ready!');\n});\n\n// create an event listener for messages\nbot.on('message', message => {\n // if the message is \"what is my avatar\",\n if (message.content === 'what is my avatar') {\n // send the user's avatar URL\n message.reply(message.author.avatarURL);\n }\n});\n\n// log our bot in\nbot.login(token);\n\n```"}]}}
\ No newline at end of file
diff --git a/docs/generator/config.json b/docs/generator/config.json
new file mode 100644
index 000000000..e8c278fd2
--- /dev/null
+++ b/docs/generator/config.json
@@ -0,0 +1,4 @@
+{
+ "GEN_VERSION": 12,
+ "COMPRESS": false
+}
\ No newline at end of file
diff --git a/docs/generator/doc-scanner.js b/docs/generator/doc-scanner.js
new file mode 100644
index 000000000..54fdaf51f
--- /dev/null
+++ b/docs/generator/doc-scanner.js
@@ -0,0 +1,24 @@
+/* eslint no-console:0 no-return-assign:0 */
+const parse = require('jsdoc-parse');
+
+module.exports = class DocumentationScanner {
+ constructor(generator) {
+ this.generator = generator;
+ }
+
+ scan(directory) {
+ return new Promise((resolve, reject) => {
+ const stream = parse({
+ src: [`${directory}*.js`, `${directory}**/*.js`],
+ });
+
+ let json = '';
+ stream.on('data', chunk => json += chunk.toString('utf-8'));
+ stream.on('error', reject);
+ stream.on('end', () => {
+ json = JSON.parse(json);
+ resolve(json);
+ });
+ });
+ }
+};
diff --git a/docs/generator/documentation.js b/docs/generator/documentation.js
new file mode 100644
index 000000000..7ecb4a738
--- /dev/null
+++ b/docs/generator/documentation.js
@@ -0,0 +1,114 @@
+const DocumentedClass = require('./types/DocumentedClass');
+const DocumentedInterface = require('./types/DocumentedInterface');
+const DocumentedTypeDef = require('./types/DocumentedTypeDef');
+const DocumentedConstructor = require('./types/DocumentedConstructor');
+const DocumentedMember = require('./types/DocumentedMember');
+const DocumentedFunction = require('./types/DocumentedFunction');
+const DocumentedEvent = require('./types/DocumentedEvent');
+const GEN_VERSION = require('./config.json').GEN_VERSION;
+
+class Documentation {
+ constructor(items, custom) {
+ this.classes = new Map();
+ this.interfaces = new Map();
+ this.typedefs = new Map();
+ this.custom = custom;
+ this.parse(items);
+ }
+
+ registerRoots(data) {
+ for (const item of data) {
+ switch (item.kind) {
+ case 'class':
+ this.classes.set(item.name, new DocumentedClass(this, item));
+ break;
+ case 'interface':
+ this.interfaces.set(item.name, new DocumentedInterface(this, item));
+ break;
+ case 'typedef':
+ this.typedefs.set(item.name, new DocumentedTypeDef(this, item));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ findParent(item) {
+ if (['constructor', 'member', 'function', 'event'].indexOf(item.kind) > -1) {
+ if (this.classes.get(item.memberof)) {
+ return this.classes.get(item.memberof);
+ }
+ if (this.interfaces.get(item.memberof)) {
+ return this.interfaces.get(item.memberof);
+ }
+ }
+ return;
+ }
+
+ parse(items) {
+ this.registerRoots(
+ items.filter(
+ item => ['class', 'interface', 'typedef'].indexOf(item.kind) > -1
+ )
+ );
+
+ const members = items.filter(
+ item => ['class', 'interface', 'typedef'].indexOf(item.kind) === -1
+ );
+
+ const unknowns = new Map();
+ for (const member of members) {
+ let item;
+ switch (member.kind) {
+ case 'constructor':
+ item = new DocumentedConstructor(this, member);
+ break;
+ case 'member':
+ item = new DocumentedMember(this, member);
+ break;
+ case 'function':
+ item = new DocumentedFunction(this, member);
+ break;
+ case 'event':
+ item = new DocumentedEvent(this, member);
+ break;
+ default:
+ unknowns.set(member.kind, member);
+ break;
+ }
+ if (!item) {
+ continue;
+ }
+ const parent = this.findParent(member);
+ if (!parent) {
+ console.log(new Error(`${member.name || member.directData.name} has no accessible parent`));
+ continue;
+ }
+ parent.add(item);
+ }
+ if (unknowns.size > 0) {
+ Array.from(unknowns.keys()).map(
+ k => console.log(`Unknown documentation kind ${k} - \n${JSON.stringify(unknowns.get(k))}\n`
+ ));
+ }
+ }
+
+ serialize() {
+ const meta = {
+ version: GEN_VERSION,
+ date: Date.now(),
+ };
+
+ const serialized = {
+ meta,
+ classes: Array.from(this.classes.values()).map(c => c.serialize()),
+ interfaces: Array.from(this.interfaces.values()).map(i => i.serialize()),
+ typedefs: Array.from(this.typedefs.values()).map(t => t.serialize()),
+ custom: this.custom,
+ };
+ return serialized;
+ }
+}
+
+module.exports = Documentation;
diff --git a/docs/generator/generator.js b/docs/generator/generator.js
new file mode 100644
index 000000000..d6ce73dfb
--- /dev/null
+++ b/docs/generator/generator.js
@@ -0,0 +1,30 @@
+/* eslint no-console:0 no-return-assign:0 */
+const GEN_VERSION = require('./config.json').GEN_VERSION;
+const compress = require('./config.json').COMPRESS;
+const DocumentationScanner = require('./doc-scanner');
+const Documentation = require('./documentation');
+const fs = require('fs-extra');
+const zlib = require('zlib');
+const custom = require('../custom/index');
+
+const docScanner = new DocumentationScanner(this);
+
+function parseDocs(json) {
+ console.log(`${json.length} items found`);
+ const documentation = new Documentation(json, custom);
+ console.log('serializing');
+ let output = JSON.stringify(documentation.serialize(), null, 0);
+ if (compress) {
+ console.log('compressing');
+ output = zlib.deflateSync(output).toString('utf8');
+ }
+ console.log('writing to docs.json');
+ fs.writeFileSync('./docs/docs.json', output);
+}
+
+console.log(`using format version ${GEN_VERSION}`);
+console.log('scanning for documentation');
+
+docScanner.scan('./src/')
+ .then(parseDocs)
+ .catch(console.error);
diff --git a/docs/generator/types/DocumentedClass.js b/docs/generator/types/DocumentedClass.js
new file mode 100644
index 000000000..323afafba
--- /dev/null
+++ b/docs/generator/types/DocumentedClass.js
@@ -0,0 +1,83 @@
+const DocumentedItem = require('./DocumentedItem');
+const DocumentedItemMeta = require('./DocumentedItemMeta');
+const DocumentedConstructor = require('./DocumentedConstructor');
+const DocumentedFunction = require('./DocumentedFunction');
+const DocumentedMember = require('./DocumentedMember');
+const DocumentedEvent = require('./DocumentedEvent');
+
+/*
+{ id: 'VoiceChannel',
+ longname: 'VoiceChannel',
+ name: 'VoiceChannel',
+ scope: 'global',
+ kind: 'class',
+ augments: [ 'GuildChannel' ],
+ description: 'Represents a Server Voice Channel on Discord.',
+ meta:
+ { lineno: 7,
+ filename: 'VoiceChannel.js',
+ path: 'src/structures' },
+ order: 232 }
+ */
+
+class DocumentedClass extends DocumentedItem {
+
+ constructor(docParent, data) {
+ super(docParent, data);
+ this.props = new Map();
+ this.methods = new Map();
+ this.events = new Map();
+ }
+
+ add(item) {
+ if (item instanceof DocumentedConstructor) {
+ if (this.classConstructor) {
+ throw new Error(`Doc ${this.directData.name} already has constructor - ${this.directData.classConstructor}`);
+ }
+ this.classConstructor = item;
+ } else if (item instanceof DocumentedFunction) {
+ if (this.methods.get(item.directData.name)) {
+ throw new Error(`Doc ${this.directData.name} already has method ${item.directData.name}`);
+ }
+ this.methods.set(item.directData.name, item);
+ } else if (item instanceof DocumentedMember) {
+ if (this.props.get(item.directData.name)) {
+ throw new Error(`Doc ${this.directData.name} already has prop ${item.directData.name}`);
+ }
+ this.props.set(item.directData.name, item);
+ } else if (item instanceof DocumentedEvent) {
+ if (this.events.get(item.directData.name)) {
+ throw new Error(`Doc ${this.directData.name} already has event ${item.directData.name}`);
+ }
+ this.events.set(item.directData.name, item);
+ }
+ }
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ this.directData.meta = new DocumentedItemMeta(this, data.meta);
+ }
+
+ serialize() {
+ super.serialize();
+ const { id, name, description, meta, augments, access } = this.directData;
+ const serialized = {
+ id,
+ name,
+ description,
+ meta: meta.serialize(),
+ extends: augments,
+ access,
+ };
+ if (this.classConstructor) {
+ serialized.classConstructor = this.classConstructor.serialize();
+ }
+ serialized.methods = Array.from(this.methods.values()).map(m => m.serialize());
+ serialized.properties = Array.from(this.props.values()).map(p => p.serialize());
+ serialized.events = Array.from(this.events.values()).map(e => e.serialize());
+ return serialized;
+ }
+}
+
+module.exports = DocumentedClass;
diff --git a/docs/generator/types/DocumentedConstructor.js b/docs/generator/types/DocumentedConstructor.js
new file mode 100644
index 000000000..e539bf642
--- /dev/null
+++ b/docs/generator/types/DocumentedConstructor.js
@@ -0,0 +1,46 @@
+const DocumentedItem = require('./DocumentedItem');
+const DocumentedParam = require('./DocumentedParam');
+
+/*
+{ id: 'Client()',
+ longname: 'Client',
+ name: 'Client',
+ kind: 'constructor',
+ description: 'Creates an instance of Client.',
+ memberof: 'Client',
+ params:
+ [ { type: [Object],
+ optional: true,
+ description: 'options to pass to the client',
+ name: 'options' } ],
+ order: 10 }
+*/
+
+class DocumentedConstructor extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ const newParams = [];
+ for (const param of data.params) {
+ newParams.push(new DocumentedParam(this, param));
+ }
+ this.directData.params = newParams;
+ }
+
+ serialize() {
+ super.serialize();
+ const { id, name, description, memberof, access, params } = this.directData;
+ return {
+ id,
+ name,
+ description,
+ memberof,
+ access,
+ params: params.map(p => p.serialize()),
+ };
+ }
+
+}
+
+module.exports = DocumentedConstructor;
diff --git a/docs/generator/types/DocumentedEvent.js b/docs/generator/types/DocumentedEvent.js
new file mode 100644
index 000000000..0641b23b8
--- /dev/null
+++ b/docs/generator/types/DocumentedEvent.js
@@ -0,0 +1,80 @@
+const DocumentedItem = require('./DocumentedItem');
+const DocumentedItemMeta = require('./DocumentedItemMeta');
+const DocumentedParam = require('./DocumentedParam');
+
+/*
+{
+ "id":"Client#event:guildMemberRolesUpdate",
+ "longname":"Client#event:guildMemberRolesUpdate",
+ "name":"guildMemberRolesUpdate",
+ "scope":"instance",
+ "kind":"event",
+ "description":"Emitted whenever a Guild Member's Roles change - i.e. new role or removed role",
+ "memberof":"Client",
+ "params":[
+ {
+ "type":{
+ "names":[
+ "Guild"
+ ]
+ },
+ "description":"the guild that the update affects",
+ "name":"guild"
+ },
+ {
+ "type":{
+ "names":[
+ "Array."
+ ]
+ },
+ "description":"the roles before the update",
+ "name":"oldRoles"
+ },
+ {
+ "type":{
+ "names":[
+ "Guild"
+ ]
+ },
+ "description":"the roles after the update",
+ "name":"newRoles"
+ }
+ ],
+ "meta":{
+ "lineno":91,
+ "filename":"Guild.js",
+ "path":"src/structures"
+ },
+ "order":110
+}
+*/
+
+class DocumentedEvent extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ this.directData = data;
+ this.directData.meta = new DocumentedItemMeta(this, data.meta);
+ const newParams = [];
+ data.params = data.params || [];
+ for (const param of data.params) {
+ newParams.push(new DocumentedParam(this, param));
+ }
+ this.directData.params = newParams;
+ }
+
+ serialize() {
+ super.serialize();
+ const { id, name, description, memberof, meta, params } = this.directData;
+ return {
+ id,
+ name,
+ description,
+ memberof,
+ meta: meta.serialize(),
+ params: params.map(p => p.serialize()),
+ };
+ }
+
+}
+
+module.exports = DocumentedEvent;
diff --git a/docs/generator/types/DocumentedFunction.js b/docs/generator/types/DocumentedFunction.js
new file mode 100644
index 000000000..698fb16b5
--- /dev/null
+++ b/docs/generator/types/DocumentedFunction.js
@@ -0,0 +1,91 @@
+const DocumentedItem = require('./DocumentedItem');
+const DocumentedItemMeta = require('./DocumentedItemMeta');
+const DocumentedVarType = require('./DocumentedVarType');
+const DocumentedParam = require('./DocumentedParam');
+
+/*
+{
+ "id":"ClientUser#sendTTSMessage",
+ "longname":"ClientUser#sendTTSMessage",
+ "name":"sendTTSMessage",
+ "scope":"instance",
+ "kind":"function",
+ "inherits":"User#sendTTSMessage",
+ "inherited":true,
+ "implements":[
+ "TextBasedChannel#sendTTSMessage"
+ ],
+ "description":"Send a text-to-speech message to this channel",
+ "memberof":"ClientUser",
+ "params":[
+ {
+ "type":{
+ "names":[
+ "String"
+ ]
+ },
+ "description":"the content to send",
+ "name":"content"
+ }
+ ],
+ "examples":[
+ "// send a TTS message..."
+ ],
+ "returns":[
+ {
+ "type":{
+ "names":[
+ "Promise."
+ ]
+ }
+ }
+ ],
+ "meta":{
+ "lineno":38,
+ "filename":"TextBasedChannel.js",
+ "path":src/structures/interface"
+ },
+ "order":293
+}
+ */
+
+class DocumentedFunction extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ this.directData.meta = new DocumentedItemMeta(this, data.meta);
+ this.directData.returns = new DocumentedVarType(this, data.returns ? data.returns[0].type : {
+ names: ['null'],
+ });
+ const newParams = [];
+ for (const param of data.params) {
+ newParams.push(new DocumentedParam(this, param));
+ }
+ this.directData.params = newParams;
+ }
+
+ serialize() {
+ super.serialize();
+ const {
+ id, name, description, memberof, examples, inherits, inherited, meta, returns, params, access,
+ } = this.directData;
+ const serialized = {
+ id,
+ access,
+ name,
+ description,
+ memberof,
+ examples,
+ inherits,
+ inherited,
+ meta: meta.serialize(),
+ returns: returns.serialize(),
+ params: params.map(p => p.serialize()),
+ };
+ serialized.implements = this.directData.implements;
+ return serialized;
+ }
+}
+
+module.exports = DocumentedFunction;
diff --git a/docs/generator/types/DocumentedInterface.js b/docs/generator/types/DocumentedInterface.js
new file mode 100644
index 000000000..ce2905785
--- /dev/null
+++ b/docs/generator/types/DocumentedInterface.js
@@ -0,0 +1,32 @@
+const DocumentedClass = require('./DocumentedClass');
+
+/*
+{ id: 'TextBasedChannel',
+ longname: 'TextBasedChannel',
+ name: 'TextBasedChannel',
+ scope: 'global',
+ kind: 'interface',
+ classdesc: 'Interface for classes that have text-channel-like features',
+ params: [],
+ meta:
+ { lineno: 5,
+ filename: 'TextBasedChannel.js',
+ path: 'src/structures/interface' },
+ order: 175 }
+ */
+
+class DocumentedInterface extends DocumentedClass {
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ // this.directData.meta = new DocumentedItemMeta(this, data.meta);
+ }
+
+ serialize() {
+ const serialized = super.serialize();
+ serialized.description = this.directData.classdesc;
+ return serialized;
+ }
+}
+
+module.exports = DocumentedInterface;
diff --git a/docs/generator/types/DocumentedItem.js b/docs/generator/types/DocumentedItem.js
new file mode 100644
index 000000000..562c706d3
--- /dev/null
+++ b/docs/generator/types/DocumentedItem.js
@@ -0,0 +1,17 @@
+class DocumentedItem {
+ constructor(parent, info) {
+ this.parent = parent;
+ this.directData = {};
+ this.registerMetaInfo(info);
+ }
+
+ registerMetaInfo() {
+ return;
+ }
+
+ serialize() {
+ return;
+ }
+}
+
+module.exports = DocumentedItem;
diff --git a/docs/generator/types/DocumentedItemMeta.js b/docs/generator/types/DocumentedItemMeta.js
new file mode 100644
index 000000000..7b44941b6
--- /dev/null
+++ b/docs/generator/types/DocumentedItemMeta.js
@@ -0,0 +1,29 @@
+const cwd = (`${process.cwd()}\\`).replace(/\\/g, '/');
+const backToForward = /\\/g;
+
+const DocumentedItem = require('./DocumentedItem');
+
+/*
+ { lineno: 7,
+ filename: 'VoiceChannel.js',
+ path: 'src/structures' },
+*/
+
+class DocumentedItemMeta extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData.line = data.lineno;
+ this.directData.file = data.filename;
+ this.directData.path = data.path.replace(backToForward, '/').replace(cwd, '');
+ }
+
+ serialize() {
+ super.serialize();
+ const { line, file, path } = this.directData;
+ return { line, file, path };
+ }
+
+}
+
+module.exports = DocumentedItemMeta;
diff --git a/docs/generator/types/DocumentedMember.js b/docs/generator/types/DocumentedMember.js
new file mode 100644
index 000000000..3eaddc24e
--- /dev/null
+++ b/docs/generator/types/DocumentedMember.js
@@ -0,0 +1,58 @@
+const DocumentedItem = require('./DocumentedItem');
+const DocumentedItemMeta = require('./DocumentedItemMeta');
+const DocumentedVarType = require('./DocumentedVarType');
+const DocumentedParam = require('./DocumentedParam');
+
+/*
+{ id: 'Client#rest',
+ longname: 'Client#rest',
+ name: 'rest',
+ scope: 'instance',
+ kind: 'member',
+ description: 'The REST manager of the client',
+ memberof: 'Client',
+ type: { names: [ 'RESTManager' ] },
+ access: 'private',
+ meta:
+ { lineno: 32,
+ filename: 'Client.js',
+ path: 'src/client' },
+ order: 11 }
+*/
+
+class DocumentedMember extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ this.directData.meta = new DocumentedItemMeta(this, data.meta);
+ this.directData.type = new DocumentedVarType(this, data.type);
+ if (data.properties) {
+ const newProps = [];
+ for (const param of data.properties) {
+ newProps.push(new DocumentedParam(this, param));
+ }
+ this.directData.properties = newProps;
+ } else {
+ data.properties = [];
+ }
+ }
+
+ serialize() {
+ super.serialize();
+ const { id, name, description, memberof, type, access, meta, properties } = this.directData;
+ return {
+ id,
+ name,
+ description,
+ memberof,
+ type: type.serialize(),
+ access,
+ meta: meta.serialize(),
+ props: properties.map(p => p.serialize()),
+ };
+ }
+
+}
+
+module.exports = DocumentedMember;
diff --git a/docs/generator/types/DocumentedParam.js b/docs/generator/types/DocumentedParam.js
new file mode 100644
index 000000000..bb94d7e37
--- /dev/null
+++ b/docs/generator/types/DocumentedParam.js
@@ -0,0 +1,36 @@
+const DocumentedItem = require('./DocumentedItem');
+const DocumentedVarType = require('./DocumentedVarType');
+
+/*
+{
+ "type":{
+ "names":[
+ "Guild"
+ ]
+ },
+ "description":"the roles after the update",
+ "name":"newRoles"
+ }
+*/
+
+class DocumentedParam extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ this.directData.type = new DocumentedVarType(this, data.type);
+ }
+
+ serialize() {
+ super.serialize();
+ const { name, description, type, optional } = this.directData;
+ return {
+ name,
+ description,
+ optional,
+ type: type.serialize(),
+ };
+ }
+}
+
+module.exports = DocumentedParam;
diff --git a/docs/generator/types/DocumentedTypeDef.js b/docs/generator/types/DocumentedTypeDef.js
new file mode 100644
index 000000000..a323b2ef3
--- /dev/null
+++ b/docs/generator/types/DocumentedTypeDef.js
@@ -0,0 +1,44 @@
+const DocumentedItem = require('./DocumentedItem');
+const DocumentedItemMeta = require('./DocumentedItemMeta');
+const DocumentedVarType = require('./DocumentedVarType');
+
+/*
+{ id: 'StringResolvable',
+ longname: 'StringResolvable',
+ name: 'StringResolvable',
+ scope: 'global',
+ kind: 'typedef',
+ description: 'Data that can be resolved to give a String...',
+ type: { names: [ 'String', 'Array', 'Object' ] },
+ meta:
+ { lineno: 142,
+ filename: 'ClientDataResolver.js',
+ path: 'src/client' },
+ order: 37 }
+*/
+
+class DocumentedTypeDef extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ this.directData.meta = new DocumentedItemMeta(this, data.meta);
+ this.directData.type = new DocumentedVarType(this, data.type);
+ }
+
+ serialize() {
+ super.serialize();
+ const { id, name, description, type, access, meta } = this.directData;
+ return {
+ id,
+ name,
+ description,
+ type: type.serialize(),
+ access,
+ meta: meta.serialize(),
+ };
+ }
+
+}
+
+module.exports = DocumentedTypeDef;
diff --git a/docs/generator/types/DocumentedVarType.js b/docs/generator/types/DocumentedVarType.js
new file mode 100644
index 000000000..209431daf
--- /dev/null
+++ b/docs/generator/types/DocumentedVarType.js
@@ -0,0 +1,50 @@
+const DocumentedItem = require('./DocumentedItem');
+
+/*
+{
+ "names":[
+ "String"
+ ]
+}
+*/
+
+const regex = /([\w]+)([^\w]+)/;
+const regexG = /([\w]+)([^\w]+)/g;
+
+function splitVarName(str) {
+ if (str === '*') {
+ return ['*', ''];
+ }
+ const matches = str.match(regexG);
+ const output = [];
+ if (matches) {
+ for (const match of matches) {
+ const groups = match.match(regex);
+ output.push([groups[1], groups[2]]);
+ }
+ } else {
+ output.push([str.match(/(\w+)/g)[0], '']);
+ }
+ return output;
+}
+
+class DocumentedVarType extends DocumentedItem {
+
+ registerMetaInfo(data) {
+ super.registerMetaInfo(data);
+ this.directData = data;
+ }
+
+ serialize() {
+ super.serialize();
+ const names = [];
+ for (const name of this.directData.names) {
+ names.push(splitVarName(name));
+ }
+ return {
+ types: names,
+ };
+ }
+}
+
+module.exports = DocumentedVarType;
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..aabb09cd0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "discord.js",
+ "version": "7.0.0",
+ "description": "A way to interface with the Discord API",
+ "main": "./src/index",
+ "scripts": {
+ "test": "eslint src/",
+ "docs": "node docs/generator/generator.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/hydrabolt/discord.js.git"
+ },
+ "keywords": [
+ "discord",
+ "api",
+ "bot",
+ "client",
+ "node",
+ "discordapp"
+ ],
+ "author": "Amish Shah ",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/hydrabolt/discord.js/issues"
+ },
+ "homepage": "https://github.com/hydrabolt/discord.js#readme",
+ "dependencies": {
+ "superagent": "^1.5.0",
+ "tweetnacl": "^0.14.3",
+ "ws": "^1.1.1",
+ "opusscript": "^0.0.1"
+ },
+ "devDependencies": {
+ "fs-extra": "^0.30.0",
+ "jsdoc-parse": "^1.2.0",
+ "eslint": "^3.4.0"
+ },
+ "optionalDependencies": {
+ "node-opus": "^0.1.13"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "browser": {
+ "./src/Util/TokenCacher.js": "./src/Util/TokenCacher-shim.js",
+ "./lib/Util/TokenCacher.js": "./lib/Util/TokenCacher-shim.js"
+ }
+}
diff --git a/src/client/Client.js b/src/client/Client.js
new file mode 100644
index 000000000..4177a3755
--- /dev/null
+++ b/src/client/Client.js
@@ -0,0 +1,213 @@
+const EventEmitter = require('events').EventEmitter;
+const mergeDefault = require('../util/MergeDefault');
+const Constants = require('../util/Constants');
+const RESTManager = require('./rest/RESTManager');
+const ClientDataManager = require('./ClientDataManager');
+const ClientManager = require('./ClientManager');
+const ClientDataResolver = require('./ClientDataResolver');
+const ClientVoiceManager = require('./voice/ClientVoiceManager');
+const WebSocketManager = require('./websocket/WebSocketManager');
+const ActionsManager = require('./actions/ActionsManager');
+const Collection = require('../util/Collection');
+
+/**
+ * The starting point for making a Discord Bot.
+ * @extends {EventEmitter}
+ */
+class Client extends EventEmitter {
+ /**
+ * @param {ClientOptions} [options] Options for the client
+ */
+ constructor(options) {
+ super();
+ this.options = mergeDefault(Constants.DefaultOptions, options);
+ /**
+ * 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
+ * @type {ClientVoiceManager}
+ * @private
+ */
+ this.voice = new ClientVoiceManager(this);
+ /**
+ * A Collection of the Client's stored users
+ * @type {Collection}
+ */
+ this.users = new Collection();
+ /**
+ * A Collection of the Client's stored guilds
+ * @type {Collection}
+ */
+ this.guilds = new Collection();
+ /**
+ * A Collection of the Client's stored channels
+ * @type {Collection}
+ */
+ this.channels = new Collection();
+ /**
+ * The authorization token for the logged in user/bot.
+ * @type {?string}
+ */
+ this.token = null;
+ /**
+ * The ClientUser representing the logged in Client
+ * @type {?ClientUser}
+ */
+ this.user = null;
+ /**
+ * The email, if there is one, for the logged in Client
+ * @type {?string}
+ */
+ this.email = null;
+ /**
+ * The password, if there is one, for the logged in Client
+ * @type {?string}
+ */
+ this.password = null;
+ /**
+ * The date at which the Client was regarded as being in the `READY` state.
+ * @type {?Date}
+ */
+ this.readyTime = null;
+ this._intervals = [];
+ this._timeouts = [];
+ }
+
+ /**
+ * Logs the client in. If successful, resolves with the account's token. If you're making a bot, it's
+ * much better to use a bot account rather than a user account.
+ * Bot accounts have higher rate limits and have access to some features user accounts don't have. User bots
+ * that are making a lot of API requests can even be banned.
+ * @param {string} emailOrToken The email or token used for the account. If it is an email, a password _must_ be
+ * provided.
+ * @param {string} [password] The password for the account, only needed if an email was provided.
+ * @returns {Promise}
+ * @example
+ * // log the client in using a token
+ * const token = 'my token';
+ * client.login(token);
+ * @example
+ * // log the client in using email and password
+ * const email = 'user@email.com';
+ * const password = 'supersecret123';
+ * client.login(email, password);
+ */
+ login(emailOrToken, password) {
+ if (password) return this.rest.methods.loginEmailPassword(emailOrToken, password);
+ return this.rest.methods.loginToken(emailOrToken);
+ }
+
+ /**
+ * Destroys the client and logs out.
+ * @returns {Promise}
+ */
+ destroy() {
+ return new Promise((resolve, reject) => {
+ this.manager.destroy().then(() => {
+ this._intervals.map(i => clearInterval(i));
+ this._timeouts.map(t => clearTimeout(t));
+ this.token = null;
+ this.email = null;
+ this.password = null;
+ this._timeouts = [];
+ this._intervals = [];
+ resolve();
+ }).catch(reject);
+ });
+ }
+
+ setInterval(...params) {
+ const interval = setInterval(...params);
+ this._intervals.push(interval);
+ return interval;
+ }
+
+ setTimeout(...params) {
+ const restParams = params.slice(1);
+ const timeout = setTimeout(() => {
+ this._timeouts.splice(this._timeouts.indexOf(params[0]), 1);
+ params[0]();
+ }, ...restParams);
+ this._timeouts.push(timeout);
+ return timeout;
+ }
+
+ /**
+ * This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
+ * if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
+ * @param {Guild[]} [guilds=this.guilds.array()] An array of guilds to sync
+ */
+ syncGuilds(guilds = this.guilds.array()) {
+ if (!this.user.bot) {
+ this.ws.send({
+ op: 12,
+ d: guilds.map(g => g.id),
+ });
+ }
+ }
+
+ /**
+ * Caches a user, or obtains it from the cache if it's already cached.
+ * If the user isn't already cached, it will only be obtainable by OAuth bot accounts.
+ * @param {string} id The ID of the user to obtain
+ * @returns {Promise}
+ */
+ fetchUser(id) {
+ if (this.users.has(id)) return Promise.resolve(this.users.get(id));
+ return this.rest.methods.getUser(id);
+ }
+
+ /**
+ * Returns a Collection, mapping Guild ID to Voice Connections.
+ * @readonly
+ * @type {Collection}
+ */
+ get voiceConnections() {
+ return this.voice.connections;
+ }
+
+ /**
+ * The uptime for the logged in Client.
+ * @readonly
+ * @type {?number}
+ */
+ get uptime() {
+ return this.readyTime ? Date.now() - this.readyTime : null;
+ }
+}
+
+module.exports = Client;
diff --git a/src/client/ClientDataManager.js b/src/client/ClientDataManager.js
new file mode 100644
index 000000000..5c2201810
--- /dev/null
+++ b/src/client/ClientDataManager.js
@@ -0,0 +1,98 @@
+const Constants = require('../util/Constants');
+const cloneObject = require('../util/CloneObject');
+const Guild = require('../structures/Guild');
+const User = require('../structures/User');
+const DMChannel = require('../structures/DMChannel');
+const TextChannel = require('../structures/TextChannel');
+const VoiceChannel = require('../structures/VoiceChannel');
+const GuildChannel = require('../structures/GuildChannel');
+const GroupDMChannel = require('../structures/GroupDMChannel');
+
+class ClientDataManager {
+ constructor(client) {
+ this.client = client;
+ }
+
+ get pastReady() {
+ return this.client.ws.status === Constants.Status.READY;
+ }
+
+ newGuild(data) {
+ const already = this.client.guilds.has(data.id);
+ const guild = new Guild(this.client, data);
+ this.client.guilds.set(guild.id, guild);
+ if (this.pastReady && !already) {
+ /**
+ * Emitted whenever the client joins a Guild.
+ * @event Client#guildCreate
+ * @param {Guild} guild The created guild
+ */
+ this.client.emit(Constants.Events.GUILD_CREATE, guild);
+ }
+
+ return guild;
+ }
+
+ newUser(data) {
+ if (this.client.users.has(data.id)) return this.client.users.get(data.id);
+ const user = new User(this.client, data);
+ this.client.users.set(user.id, user);
+ return user;
+ }
+
+ newChannel(data, guild) {
+ const already = this.client.channels.has(data.id);
+ let channel;
+ if (data.type === Constants.ChannelTypes.DM) {
+ channel = new DMChannel(this.client, data);
+ } else if (data.type === Constants.ChannelTypes.groupDM) {
+ channel = new GroupDMChannel(this.client, data);
+ } else {
+ guild = guild || this.client.guilds.get(data.guild_id);
+ if (guild) {
+ if (data.type === Constants.ChannelTypes.text) {
+ channel = new TextChannel(guild, data);
+ guild.channels.set(channel.id, channel);
+ } else if (data.type === Constants.ChannelTypes.voice) {
+ channel = new VoiceChannel(guild, data);
+ guild.channels.set(channel.id, channel);
+ }
+ }
+ }
+
+ if (channel) {
+ if (this.pastReady && !already) this.client.emit(Constants.Events.CHANNEL_CREATE, channel);
+ this.client.channels.set(channel.id, channel);
+ return channel;
+ }
+
+ return null;
+ }
+
+ killGuild(guild) {
+ const already = this.client.guilds.has(guild.id);
+ this.client.guilds.delete(guild.id);
+ if (already && this.pastReady) this.client.emit(Constants.Events.GUILD_DELETE, guild);
+ }
+
+ killUser(user) {
+ this.client.users.delete(user.id);
+ }
+
+ killChannel(channel) {
+ this.client.channels.delete(channel.id);
+ if (channel instanceof GuildChannel) channel.guild.channels.delete(channel.id);
+ }
+
+ updateGuild(currentGuild, newData) {
+ const oldGuild = cloneObject(currentGuild);
+ currentGuild.setup(newData);
+ if (this.pastReady) this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild);
+ }
+
+ updateChannel(currentChannel, newData) {
+ currentChannel.setup(newData);
+ }
+}
+
+module.exports = ClientDataManager;
diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js
new file mode 100644
index 000000000..b93a40382
--- /dev/null
+++ b/src/client/ClientDataResolver.js
@@ -0,0 +1,206 @@
+const path = require('path');
+const fs = require('fs');
+const request = require('superagent');
+
+const Constants = require('../util/Constants');
+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`);
+
+/**
+ * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
+ * extracting a User from a Message object.
+ * @private
+ */
+class ClientDataResolver {
+ /**
+ * @param {Client} client The client the resolver is for
+ */
+ constructor(client) {
+ this.client = client;
+ }
+
+ /**
+ * Data that resolves to give a User object. This can be:
+ * * A User object
+ * * A User ID
+ * * A Message (resolves to the message author)
+ * * A Guild (owner of the guild)
+ * * A Guild Member
+ * @typedef {User|string|Message|Guild|GuildMember} UserResolvable
+ */
+
+ /**
+ * Resolves a UserResolvable to a User object
+ * @param {UserResolvable} user The UserResolvable to identify
+ * @returns {?User}
+ */
+ resolveUser(user) {
+ if (user instanceof User) {
+ return user;
+ } else if (typeof user === 'string') {
+ return this.client.users.get(user);
+ } else if (user instanceof Message) {
+ return user.author;
+ } else if (user instanceof Guild) {
+ return user.owner;
+ } else if (user instanceof GuildMember) {
+ return user.user;
+ }
+
+ return null;
+ }
+
+ /**
+ * Data that resolves to give a Guild object. This can be:
+ * * A Guild object
+ * @typedef {Guild} 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);
+ return null;
+ }
+
+ /**
+ * Data that resolves to give a GuildMember object. This can be:
+ * * A GuildMember object
+ * * A User object
+ * @typedef {Guild} GuildMemberResolvable
+ */
+
+ /**
+ * Resolves a GuildMemberResolvable to a GuildMember object
+ * @param {GuildResolvable} guild The guild that the member is part of
+ * @param {UserResolvable} user The user that is part of the guild
+ * @returns {?GuildMember}
+ */
+ resolveGuildMember(guild, user) {
+ if (user instanceof GuildMember) return user;
+
+ guild = this.resolveGuild(guild);
+ user = this.resolveUser(user);
+ if (!guild || !user) return null;
+
+ return guild.members.get(user.id);
+ }
+
+ /**
+ * 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 Channel. This can be:
+ * * An instance of a Channel
+ * * An ID of a Channel
+ * @typedef {Channel|string} ChannelResolvable
+ */
+
+ /**
+ * Resolves a ChannelResolvable to a Channel object
+ * @param {ChannelResolvable} channel The channel resolvable to resolve
+ * @returns {?Channel}
+ */
+ resolveChannel(channel) {
+ if (channel instanceof Channel) return channel;
+ if (typeof channel === 'string') return this.client.channels.get(channel.id);
+ return null;
+ }
+
+ /**
+ * Data that can be resolved to give a permission number. This can be:
+ * * A string
+ * * A permission number
+ * @typedef {string|number} PermissionResolvable
+ */
+
+ /**
+ * Resolves a PermissionResolvable to a permission number
+ * @param {PermissionResolvable} permission The permission resolvable to resolve
+ * @returns {number}
+ */
+ resolvePermission(permission) {
+ if (typeof permission === 'string') permission = Constants.PermissionFlags[permission];
+ if (!permission) throw new Error(Constants.Errors.NOT_A_PERMISSION);
+ return permission;
+ }
+
+ /**
+ * Data that can be resolved to give a string. This can be:
+ * * A string
+ * * An Array (joined with a new line delimiter to give a string)
+ * * Any value
+ * @typedef {string|Array|*} StringResolvable
+ */
+
+ /**
+ * Resolves a StringResolvable to a string
+ * @param {StringResolvable} data The string resolvable to resolve
+ * @returns {string}
+ */
+ resolveString(data) {
+ if (typeof data === 'string') return data;
+ if (data instanceof Array) return data.join('\n');
+ return String(data);
+ }
+
+ /**
+ * Data that can be resolved to give a Buffer. This can be:
+ * * A Buffer
+ * * The path to a local file
+ * * A URL
+ * @typedef {string|Buffer} FileResolvable
+ */
+
+ /**
+ * Resolves a FileResolvable to a Buffer
+ * @param {FileResolvable} resource The file resolvable to resolve
+ * @returns {Promise}
+ */
+ resolveFile(resource) {
+ if (typeof resource === 'string') {
+ return new Promise((resolve, reject) => {
+ if (/^https?:\/\//.test(resource)) {
+ request.get(resource)
+ .set('Content-Type', 'blob')
+ .end((err, res) => err ? reject(err) : resolve(res.body));
+ } else {
+ const file = path.resolve(resource);
+ fs.stat(file, (err, stats) => {
+ if (err) reject(err);
+ if (!stats.isFile()) throw new Error(`The file could not be found: ${file}`);
+ fs.readFile(file, (err2, data) => {
+ if (err2) reject(err2); else resolve(data);
+ });
+ });
+ }
+ });
+ }
+
+ if (resource instanceof Buffer) return Promise.resolve(resource);
+ return Promise.reject(new TypeError('resource is not a string or Buffer'));
+ }
+}
+
+module.exports = ClientDataResolver;
diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js
new file mode 100644
index 000000000..0af72e110
--- /dev/null
+++ b/src/client/ClientManager.js
@@ -0,0 +1,63 @@
+const Constants = require('../util/Constants');
+
+/**
+ * Manages the State and Background Tasks of the Client
+ * @private
+ */
+class ClientManager {
+ constructor(client) {
+ /**
+ * The Client that instantiated this Manager
+ * @type {Client}
+ */
+ this.client = client;
+ /**
+ * The heartbeat interval, null if not yet set
+ * @type {?number}
+ */
+ this.heartbeatInterval = null;
+ }
+
+ /**
+ * Connects the Client to the WebSocket
+ * @param {string} token The authorization token
+ * @param {function} resolve Function to run when connection is successful
+ * @param {function} reject Function to run when connection fails
+ */
+ connectToWebSocket(token, resolve, reject) {
+ this.client.emit('debug', `authenticated using token ${token}`);
+ this.client.token = token;
+ this.client.rest.methods.getGateway().then(gateway => {
+ this.client.emit('debug', `using gateway ${gateway}`);
+ this.client.ws.connect(gateway);
+ this.client.once(Constants.Events.READY, () => resolve(token));
+ }).catch(reject);
+ this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300);
+ }
+
+ /**
+ * Sets up a keep-alive interval to keep the Client's connection valid
+ * @param {number} time The interval in milliseconds at which heartbeat packets should be sent
+ */
+ setupKeepAlive(time) {
+ this.heartbeatInterval = this.client.setInterval(() => {
+ this.client.ws.send({
+ op: Constants.OPCodes.HEARTBEAT,
+ d: Date.now(),
+ }, true);
+ }, time);
+ }
+
+ destroy() {
+ return new Promise((resolve) => {
+ if (!this.client.user.bot) {
+ this.client.rest.methods.logout().then(resolve);
+ } else {
+ this.client.ws.destroy();
+ resolve();
+ }
+ });
+ }
+}
+
+module.exports = ClientManager;
diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js
new file mode 100644
index 000000000..8fdadc92b
--- /dev/null
+++ b/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/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js
new file mode 100644
index 000000000..e70cdcbd8
--- /dev/null
+++ b/src/client/actions/ActionsManager.js
@@ -0,0 +1,30 @@
+class ActionsManager {
+ constructor(client) {
+ this.client = client;
+
+ this.register('MessageCreate');
+ this.register('MessageDelete');
+ this.register('MessageDeleteBulk');
+ this.register('MessageUpdate');
+ this.register('ChannelCreate');
+ this.register('ChannelDelete');
+ this.register('ChannelUpdate');
+ this.register('GuildDelete');
+ this.register('GuildUpdate');
+ this.register('GuildMemberRemove');
+ this.register('GuildBanRemove');
+ this.register('GuildRoleCreate');
+ this.register('GuildRoleDelete');
+ this.register('GuildRoleUpdate');
+ this.register('UserGet');
+ this.register('UserUpdate');
+ this.register('GuildSync');
+ }
+
+ register(name) {
+ const Action = require(`./${name}`);
+ this[name] = new Action(this.client);
+ }
+}
+
+module.exports = ActionsManager;
diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js
new file mode 100644
index 000000000..dc4704152
--- /dev/null
+++ b/src/client/actions/ChannelCreate.js
@@ -0,0 +1,13 @@
+const Action = require('./Action');
+
+class ChannelCreateAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = client.dataManager.newChannel(data);
+ return {
+ channel,
+ };
+ }
+}
+
+module.exports = ChannelCreateAction;
diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js
new file mode 100644
index 000000000..b54783bcc
--- /dev/null
+++ b/src/client/actions/ChannelDelete.js
@@ -0,0 +1,31 @@
+const Action = require('./Action');
+
+class ChannelDeleteAction extends Action {
+ constructor(client) {
+ super(client);
+ this.deleted = new Map();
+ }
+
+ handle(data) {
+ const client = this.client;
+
+ let channel = client.channels.get(data.id);
+ if (channel) {
+ client.dataManager.killChannel(channel);
+ this.deleted.set(channel.id, channel);
+ this.scheduleForDeletion(channel.id);
+ } else {
+ channel = this.deleted.get(data.id) || null;
+ }
+
+ return {
+ channel,
+ };
+ }
+
+ scheduleForDeletion(id) {
+ this.client.setTimeout(() => this.deleted.delete(id), this.client.options.rest_ws_bridge_timeout);
+ }
+}
+
+module.exports = ChannelDeleteAction;
diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js
new file mode 100644
index 000000000..636addd2e
--- /dev/null
+++ b/src/client/actions/ChannelUpdate.js
@@ -0,0 +1,34 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+const cloneObject = require('../../util/CloneObject');
+
+class ChannelUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const channel = client.channels.get(data.id);
+ if (channel) {
+ const oldChannel = cloneObject(channel);
+ channel.setup(data);
+ if (!oldChannel.equals(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/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js
new file mode 100644
index 000000000..0276a523f
--- /dev/null
+++ b/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/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js
new file mode 100644
index 000000000..a8a6a3bbf
--- /dev/null
+++ b/src/client/actions/GuildDelete.js
@@ -0,0 +1,51 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+
+class GuildDeleteAction extends Action {
+ constructor(client) {
+ super(client);
+ this.deleted = new Map();
+ }
+
+ handle(data) {
+ const client = this.client;
+
+ let guild = client.guilds.get(data.id);
+ if (guild) {
+ if (guild.available && data.unavailable) {
+ // guild is unavailable
+ guild.available = false;
+ client.emit(Constants.Events.GUILD_UNAVAILABLE, guild);
+
+ // stops the GuildDelete packet thinking a guild was actually deleted,
+ // handles emitting of event itself
+ return {
+ guild: null,
+ };
+ }
+
+ // delete guild
+ client.guilds.delete(guild.id);
+ this.deleted.set(guild.id, guild);
+ this.scheduleForDeletion(guild.id);
+ } else {
+ guild = this.deleted.get(data.id) || null;
+ }
+
+ return {
+ guild,
+ };
+ }
+
+ scheduleForDeletion(id) {
+ this.client.setTimeout(() => this.deleted.delete(id), this.client.options.rest_ws_bridge_timeout);
+ }
+}
+
+/**
+ * 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/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js
new file mode 100644
index 000000000..7b21db16a
--- /dev/null
+++ b/src/client/actions/GuildMemberRemove.js
@@ -0,0 +1,50 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+
+class GuildMemberRemoveAction extends Action {
+ constructor(client) {
+ super(client);
+ this.deleted = new Map();
+ }
+
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.get(data.guild_id);
+ if (guild) {
+ let member = guild.members.get(data.user.id);
+ if (member) {
+ guild.memberCount--;
+ guild._removeMember(member);
+ this.deleted.set(guild.id + data.user.id, member);
+ if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, guild, member);
+ this.scheduleForDeletion(guild.id, data.user.id);
+ } else {
+ member = this.deleted.get(guild.id + data.user.id) || null;
+ }
+
+ return {
+ guild,
+ member,
+ };
+ }
+
+ return {
+ guild,
+ member: null,
+ };
+ }
+
+ scheduleForDeletion(guildID, userID) {
+ this.client.setTimeout(() => this.deleted.delete(guildID + userID), this.client.options.rest_ws_bridge_timeout);
+ }
+}
+
+/**
+ * Emitted whenever a member leaves a guild, or is kicked.
+ * @event Client#guildMemberRemove
+ * @param {Guild} guild The guild that the member has left.
+ * @param {GuildMember} member The member that has left the guild.
+ */
+
+module.exports = GuildMemberRemoveAction;
diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js
new file mode 100644
index 000000000..206fb043a
--- /dev/null
+++ b/src/client/actions/GuildRoleCreate.js
@@ -0,0 +1,33 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+const Role = require('../../structures/Role');
+
+class GuildRoleCreate extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.get(data.guild_id);
+ if (guild) {
+ const already = guild.roles.has(data.role.id);
+ const role = new Role(guild, data.role);
+ guild.roles.set(role.id, role);
+ if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, guild, role);
+ return {
+ role,
+ };
+ }
+
+ return {
+ role: null,
+ };
+ }
+}
+
+/**
+ * Emitted whenever a guild role is created.
+ * @event Client#guildRoleCreate
+ * @param {Guild} guild The guild that the role was created in.
+ * @param {Role} role The role that was created.
+ */
+
+module.exports = GuildRoleCreate;
diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js
new file mode 100644
index 000000000..fe4edcceb
--- /dev/null
+++ b/src/client/actions/GuildRoleDelete.js
@@ -0,0 +1,47 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+
+class GuildRoleDeleteAction extends Action {
+ constructor(client) {
+ super(client);
+ this.deleted = new Map();
+ }
+
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.get(data.guild_id);
+ if (guild) {
+ let role = guild.roles.get(data.role_id);
+ if (role) {
+ guild.roles.delete(data.role_id);
+ this.deleted.set(guild.id + data.role_id, role);
+ this.scheduleForDeletion(guild.id, data.role_id);
+ client.emit(Constants.Events.GUILD_ROLE_DELETE, guild, role);
+ } else {
+ role = this.deleted.get(guild.id + data.role_id) || null;
+ }
+
+ return {
+ role,
+ };
+ }
+
+ return {
+ role: null,
+ };
+ }
+
+ scheduleForDeletion(guildID, roleID) {
+ this.client.setTimeout(() => this.deleted.delete(guildID + roleID), this.client.options.rest_ws_bridge_timeout);
+ }
+}
+
+/**
+ * Emitted whenever a guild role is deleted.
+ * @event Client#guildRoleDelete
+ * @param {Guild} guild The guild that the role was deleted in.
+ * @param {Role} role The role that was deleted.
+ */
+
+module.exports = GuildRoleDeleteAction;
diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js
new file mode 100644
index 000000000..81baf652d
--- /dev/null
+++ b/src/client/actions/GuildRoleUpdate.js
@@ -0,0 +1,42 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+const cloneObject = require('../../util/CloneObject');
+
+class GuildRoleUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.get(data.guild_id);
+ if (guild) {
+ const roleData = data.role;
+ let oldRole = null;
+
+ const role = guild.roles.get(roleData.id);
+ if (role && !role.equals(roleData)) {
+ oldRole = cloneObject(role);
+ role.setup(data.role);
+ client.emit(Constants.Events.GUILD_ROLE_UPDATE, guild, oldRole, role);
+ }
+
+ return {
+ old: oldRole,
+ updated: role,
+ };
+ }
+
+ return {
+ old: null,
+ updated: null,
+ };
+ }
+}
+
+/**
+ * Emitted whenever a guild role is updated.
+ * @event Client#guildRoleUpdated
+ * @param {Guild} guild The guild that the role was updated in.
+ * @param {Role} oldRole The role before the update.
+ * @param {Role} newRole The role after the update.
+ */
+
+module.exports = GuildRoleUpdateAction;
diff --git a/src/client/actions/GuildSync.js b/src/client/actions/GuildSync.js
new file mode 100644
index 000000000..763acf788
--- /dev/null
+++ b/src/client/actions/GuildSync.js
@@ -0,0 +1,31 @@
+const Action = require('./Action');
+
+class GuildSync extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.get(data.id);
+ if (guild) {
+ data.presences = data.presences || [];
+ for (const presence of data.presences) {
+ const user = client.users.get(presence.user.id);
+ if (user) {
+ user.status = presence.status;
+ user.game = presence.game;
+ }
+ }
+
+ data.members = data.members || [];
+ for (const syncMember of data.members) {
+ const member = guild.members.get(syncMember.user.id);
+ if (member) {
+ guild._updateMember(member, syncMember);
+ } else {
+ guild._addMember(syncMember);
+ }
+ }
+ }
+ }
+}
+
+module.exports = GuildSync;
diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js
new file mode 100644
index 000000000..c1cdf6d6c
--- /dev/null
+++ b/src/client/actions/GuildUpdate.js
@@ -0,0 +1,34 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+const cloneObject = require('../../util/CloneObject');
+
+class GuildUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const guild = client.guilds.get(data.id);
+ if (guild) {
+ const oldGuild = cloneObject(guild);
+ guild.setup(data);
+ if (!oldGuild.equals(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/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js
new file mode 100644
index 000000000..10626179c
--- /dev/null
+++ b/src/client/actions/MessageCreate.js
@@ -0,0 +1,22 @@
+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.channel_id);
+ if (channel) {
+ const message = channel._cacheMessage(new Message(channel, data, client));
+ return {
+ message,
+ };
+ }
+
+ return {
+ message: null,
+ };
+ }
+}
+
+module.exports = MessageCreateAction;
diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js
new file mode 100644
index 000000000..5618efacb
--- /dev/null
+++ b/src/client/actions/MessageDelete.js
@@ -0,0 +1,40 @@
+const Action = require('./Action');
+
+class MessageDeleteAction extends Action {
+ constructor(client) {
+ super(client);
+ this.deleted = new Map();
+ }
+
+ handle(data) {
+ const client = this.client;
+
+ const channel = client.channels.get(data.channel_id);
+ if (channel) {
+ let message = channel.messages.get(data.id);
+
+ if (message) {
+ channel.messages.delete(message.id);
+ this.deleted.set(channel.id + message.id, message);
+ this.scheduleForDeletion(channel.id, message.id);
+ } else {
+ message = this.deleted.get(channel.id + data.id) || null;
+ }
+
+ return {
+ message,
+ };
+ }
+
+ return {
+ message: null,
+ };
+ }
+
+ scheduleForDeletion(channelID, messageID) {
+ this.client.setTimeout(() => this.deleted.delete(channelID + messageID),
+ this.client.options.rest_ws_bridge_timeout);
+ }
+}
+
+module.exports = MessageDeleteAction;
diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js
new file mode 100644
index 000000000..6a12ef19b
--- /dev/null
+++ b/src/client/actions/MessageDeleteBulk.js
@@ -0,0 +1,24 @@
+const Action = require('./Action');
+const Collection = require('../../util/Collection');
+const Constants = require('../../util/Constants');
+
+class MessageDeleteBulkAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const channel = client.channels.get(data.channel_id);
+
+ const ids = data.ids;
+ const messages = new Collection();
+ for (const id of ids) {
+ const message = channel.messages.get(id);
+ if (message) messages.set(message.id, message);
+ }
+
+ if (messages.size > 0) client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages);
+ return {
+ messages,
+ };
+ }
+}
+
+module.exports = MessageDeleteBulkAction;
diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js
new file mode 100644
index 000000000..59c84dd30
--- /dev/null
+++ b/src/client/actions/MessageUpdate.js
@@ -0,0 +1,42 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+const cloneObject = require('../../util/CloneObject');
+
+class MessageUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const channel = client.channels.get(data.channel_id);
+ if (channel) {
+ const message = channel.messages.get(data.id);
+ if (message && !message.equals(data, true)) {
+ const oldMessage = cloneObject(message);
+ message.patch(data);
+ client.emit(Constants.Events.MESSAGE_UPDATE, oldMessage, message);
+ return {
+ old: oldMessage,
+ updated: message,
+ };
+ }
+
+ return {
+ old: message,
+ updated: message,
+ };
+ }
+
+ return {
+ old: null,
+ updated: null,
+ };
+ }
+}
+
+/**
+ * Emitted whenever a message is updated - e.g. embed or content change.
+ * @event Client#messageUpdate
+ * @param {Message} oldMessage The message before the update.
+ * @param {Message} newMessage The message after the update.
+ */
+
+module.exports = MessageUpdateAction;
diff --git a/src/client/actions/UserGet.js b/src/client/actions/UserGet.js
new file mode 100644
index 000000000..65e7c9540
--- /dev/null
+++ b/src/client/actions/UserGet.js
@@ -0,0 +1,13 @@
+const Action = require('./Action');
+
+class UserGetAction extends Action {
+ handle(data) {
+ const client = this.client;
+ const user = client.dataManager.newUser(data);
+ return {
+ user,
+ };
+ }
+}
+
+module.exports = UserGetAction;
diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js
new file mode 100644
index 000000000..a8d479465
--- /dev/null
+++ b/src/client/actions/UserUpdate.js
@@ -0,0 +1,40 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+const cloneObject = require('../../util/CloneObject');
+
+class UserUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ if (client.user) {
+ if (client.user.equals(data)) {
+ return {
+ old: client.user,
+ updated: client.user,
+ };
+ }
+
+ const oldUser = cloneObject(client.user);
+ client.user.setup(data);
+ client.emit(Constants.Events.USER_UPDATE, oldUser, client.user);
+ return {
+ old: oldUser,
+ updated: client.user,
+ };
+ }
+
+ return {
+ old: null,
+ updated: null,
+ };
+ }
+}
+
+/**
+ * Emitted whenever a detail of the logged in User changes - e.g. username.
+ * @event Client#userUpdate
+ * @param {ClientUser} oldClientUser The client user before the update.
+ * @param {ClientUser} newClientUser The client user after the update.
+ */
+
+module.exports = UserUpdateAction;
diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js
new file mode 100644
index 000000000..1c88bc7c1
--- /dev/null
+++ b/src/client/rest/APIRequest.js
@@ -0,0 +1,40 @@
+const request = require('superagent');
+const Constants = require('../../util/Constants');
+
+class APIRequest {
+ constructor(rest, method, url, auth, data, file) {
+ this.rest = rest;
+ this.method = method;
+ this.url = url;
+ this.auth = auth;
+ this.data = data;
+ this.file = file;
+ }
+
+ getEndpoint() {
+ return `${this.method} ${this.url}`;
+ }
+
+ getAuth() {
+ if (this.rest.client.token && this.rest.client.user && this.rest.client.user.bot) {
+ return `Bot ${this.rest.client.token}`;
+ } else if (this.rest.client.token) {
+ return this.rest.client.token;
+ }
+ throw new Error(Constants.Errors.NO_TOKEN);
+ }
+
+ gen() {
+ const apiRequest = request[this.method](this.url);
+ if (this.auth) apiRequest.set('authorization', this.getAuth());
+ if (this.file && this.file.file) {
+ apiRequest.set('Content-Type', 'multipart/form-data');
+ apiRequest.attach('file', this.file.file, this.file.name);
+ }
+ if (this.data) apiRequest.send(this.data);
+ apiRequest.set('User-Agent', this.rest.userAgentManager.userAgent);
+ return apiRequest;
+ }
+}
+
+module.exports = APIRequest;
diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js
new file mode 100644
index 000000000..a2c11dd94
--- /dev/null
+++ b/src/client/rest/RESTManager.js
@@ -0,0 +1,48 @@
+const UserAgentManager = require('./UserAgentManager');
+const RESTMethods = require('./RESTMethods');
+const SequentialRequestHandler = require('./RequestHandlers/Sequential');
+const APIRequest = require('./APIRequest');
+const Constants = require('../../util/Constants');
+
+class RESTManager {
+ constructor(client) {
+ this.client = client;
+ this.handlers = {};
+ this.userAgentManager = new UserAgentManager(this);
+ this.methods = new RESTMethods(this);
+ this.rateLimitedEndpoints = {};
+ this.globallyRateLimited = false;
+ }
+
+ push(handler, apiRequest) {
+ return new Promise((resolve, reject) => {
+ handler.push({
+ request: apiRequest,
+ resolve,
+ reject,
+ });
+ });
+ }
+
+ getRequestHandler() {
+ switch (this.client.options.api_request_method) {
+ case 'sequential':
+ return SequentialRequestHandler;
+ default:
+ throw new Error(Constants.Errors.INVALID_RATE_LIMIT_METHOD);
+ }
+ }
+
+ makeRequest(method, url, auth, data, file) {
+ const apiRequest = new APIRequest(this, method, url, auth, data, file);
+
+ if (!this.handlers[apiRequest.getEndpoint()]) {
+ const RequestHandlerType = this.getRequestHandler();
+ this.handlers[apiRequest.getEndpoint()] = new RequestHandlerType(this, apiRequest.getEndpoint());
+ }
+
+ return this.push(this.handlers[apiRequest.getEndpoint()], apiRequest);
+ }
+}
+
+module.exports = RESTManager;
diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js
new file mode 100644
index 000000000..a9e8fddf5
--- /dev/null
+++ b/src/client/rest/RESTMethods.js
@@ -0,0 +1,443 @@
+const Constants = require('../../util/Constants');
+const Collection = require('../../util/Collection');
+
+const requireStructure = name => require(`../../structures/${name}`);
+const User = requireStructure('User');
+const GuildMember = requireStructure('GuildMember');
+const Role = requireStructure('Role');
+const Invite = requireStructure('Invite');
+
+class RESTMethods {
+ constructor(restManager) {
+ this.rest = restManager;
+ }
+
+ loginEmailPassword(email, password) {
+ return new Promise((resolve, reject) => {
+ this.rest.client.emit('debug', 'client launched using email and password - should use token instead');
+ this.rest.client.email = email;
+ this.rest.client.password = password;
+ this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
+ .then(data => {
+ this.rest.client.manager.connectToWebSocket(data.token, resolve, reject);
+ })
+ .catch(reject);
+ });
+ }
+
+ loginToken(token) {
+ return new Promise((resolve, reject) => {
+ this.rest.client.manager.connectToWebSocket(token, resolve, reject);
+ });
+ }
+
+ logout() {
+ return this.rest.makeRequest('post', Constants.Endpoints.logout, true);
+ }
+
+ getGateway() {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('get', Constants.Endpoints.gateway, true)
+ .then(res => {
+ this.rest.client.ws.gateway = `${res.url}/?encoding=json&v=${this.rest.client.options.protocol_version}`;
+ resolve(this.rest.client.ws.gateway);
+ })
+ .catch(reject);
+ });
+ }
+
+ sendMessage(channel, content, tts, nonce, file) {
+ return new Promise((resolve, reject) => {
+ const $this = this;
+
+ function req() {
+ $this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
+ content, tts, nonce,
+ }, file)
+ .then(data => resolve($this.rest.client.actions.MessageCreate.handle(data).message))
+ .catch(reject);
+ }
+
+ if (channel instanceof User || channel instanceof GuildMember) {
+ this.createDM(channel).then(chan => {
+ channel = chan;
+ req();
+ })
+ .catch(reject);
+ } else {
+ req();
+ }
+ });
+ }
+
+ deleteMessage(message) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('del', Constants.Endpoints.channelMessage(message.channel.id, message.id), true)
+ .then(() => {
+ resolve(this.rest.client.actions.MessageDelete.handle({
+ id: message.id,
+ channel_id: message.channel.id,
+ }).message);
+ })
+ .catch(reject);
+ });
+ }
+
+ bulkDeleteMessages(channel, messages) {
+ return new Promise((resolve, reject) => {
+ const options = { messages };
+ this.rest.makeRequest('post', `${Constants.Endpoints.channelMessages(channel.id)}/bulk_delete`, true, options)
+ .then(() => {
+ resolve(this.rest.client.actions.MessageDeleteBulk.handle({
+ channel_id: channel.id,
+ ids: messages,
+ }).messages);
+ })
+ .catch(reject);
+ });
+ }
+
+ updateMessage(message, content) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('patch', Constants.Endpoints.channelMessage(message.channel.id, message.id), true, {
+ content,
+ }).then(data => {
+ resolve(this.rest.client.actions.MessageUpdate.handle(data).updated);
+ }).catch(reject);
+ });
+ }
+
+ createChannel(guild, channelName, channelType) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('post', Constants.Endpoints.guildChannels(guild.id), true, {
+ name: channelName,
+ type: channelType,
+ }).then(data => {
+ resolve(this.rest.client.actions.ChannelCreate.handle(data).channel);
+ }).catch(reject);
+ });
+ }
+
+ getExistingDM(recipient) {
+ const dmChannel = Array.from(this.rest.client.channels.values())
+ .filter(channel => channel.recipient)
+ .filter(channel => channel.recipient.id === recipient.id);
+ return dmChannel[0];
+ }
+
+ createDM(recipient) {
+ return new Promise((resolve, reject) => {
+ const dmChannel = this.getExistingDM(recipient);
+ if (dmChannel) return resolve(dmChannel);
+ return this.rest.makeRequest('post', Constants.Endpoints.userChannels(this.rest.client.user.id), true, {
+ recipient_id: recipient.id,
+ }).then(data => resolve(this.rest.client.actions.ChannelCreate.handle(data).channel)).catch(reject);
+ });
+ }
+
+ deleteChannel(channel) {
+ return new Promise((resolve, reject) => {
+ if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel);
+ this.rest.makeRequest('del', Constants.Endpoints.channel(channel.id), true).then(data => {
+ data.id = channel.id;
+ resolve(this.rest.client.actions.ChannelDelete.handle(data).channel);
+ }).catch(reject);
+ });
+ }
+
+ updateChannel(channel, data) {
+ return new Promise((resolve, reject) => {
+ data.name = (data.name || channel.name).trim();
+ data.topic = data.topic || channel.topic;
+ data.position = data.position || channel.position;
+ data.bitrate = data.bitrate || channel.bitrate;
+
+ this.rest.makeRequest('patch', Constants.Endpoints.channel(channel.id), true, data).then(newData => {
+ resolve(this.rest.client.actions.ChannelUpdate.handle(newData).updated);
+ }).catch(reject);
+ });
+ }
+
+ leaveGuild(guild) {
+ if (guild.ownerID === this.rest.client.user.id) return this.deleteGuild(guild);
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('del', Constants.Endpoints.meGuild(guild.id), true).then(() => {
+ resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild);
+ }).catch(reject);
+ });
+ }
+
+ // untested but probably will work
+ deleteGuild(guild) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('del', Constants.Endpoints.guild(guild.id), true).then(() => {
+ resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild);
+ }).catch(reject);
+ });
+ }
+
+ getUser(userID) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('get', Constants.Endpoints.user(userID), true).then((data) => {
+ resolve(this.rest.client.actions.UserGet.handle(data).user);
+ }).catch(reject);
+ });
+ }
+
+ updateCurrentUser(_data) {
+ return new Promise((resolve, reject) => {
+ const user = this.rest.client.user;
+
+ const data = {};
+ data.username = _data.username || user.username;
+ data.avatar = this.rest.client.resolver.resolveBase64(_data.avatar) || user.avatar;
+ if (!user.bot) {
+ data.password = this.rest.client.password;
+ data.email = _data.email || this.rest.client.email;
+ data.new_password = _data.newPassword;
+ }
+
+ this.rest.makeRequest('patch', Constants.Endpoints.me, true, data)
+ .then(newData => resolve(this.rest.client.actions.UserUpdate.handle(newData).updated))
+ .catch(reject);
+ });
+ }
+
+ updateGuild(guild, _data) {
+ return new Promise((resolve, reject) => {
+ const data = {};
+ if (_data.name) data.name = _data.name;
+ if (_data.region) data.region = _data.region;
+ if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel);
+ if (_data.afkChannel) data.afk_channel_id = this.rest.client.resolver.resolveChannel(_data.afkChannel).id;
+ if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout);
+ if (_data.icon) data.icon = this.rest.client.resolver.resolveBase64(_data.icon);
+ if (_data.owner) data.owner_id = this.rest.client.resolver.resolveUser(_data.owner).id;
+ if (_data.splash) data.splash = this.rest.client.resolver.resolveBase64(_data.splash);
+
+ this.rest.makeRequest('patch', Constants.Endpoints.guild(guild.id), true, data)
+ .then(newData => resolve(this.rest.client.actions.GuildUpdate.handle(newData).updated))
+ .catch(reject);
+ });
+ }
+
+ kickGuildMember(guild, member) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('del', Constants.Endpoints.guildMember(guild.id, member.id), true).then(() => {
+ resolve(this.rest.client.actions.GuildMemberRemove.handle({
+ guild_id: guild.id,
+ user: member.user,
+ }).member);
+ }).catch(reject);
+ });
+ }
+
+ createGuildRole(guild) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true).then(role => {
+ resolve(this.rest.client.actions.GuildRoleCreate.handle({
+ guild_id: guild.id,
+ role,
+ }).role);
+ }).catch(reject);
+ });
+ }
+
+ deleteGuildRole(role) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('del', Constants.Endpoints.guildRole(role.guild.id, role.id), true).then(() => {
+ resolve(this.rest.client.actions.GuildRoleDelete.handle({
+ guild_id: role.guild.id,
+ role_id: role.id,
+ }).role);
+ }).catch(reject);
+ });
+ }
+
+ setChannelOverwrite(channel, payload) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('put', `${Constants.Endpoints.channelPermissions(channel.id)}/${payload.id}`, true, payload)
+ .then(resolve)
+ .catch(reject);
+ });
+ }
+
+ deletePermissionOverwrites(overwrite) {
+ return new Promise((resolve, reject) => {
+ const endpoint = `${Constants.Endpoints.channelPermissions(overwrite.channel.id)}/${overwrite.id}`;
+ this.rest.makeRequest('del', endpoint, true)
+ .then(() => resolve(overwrite))
+ .catch(reject);
+ });
+ }
+
+ getChannelMessages(channel, payload = {}) {
+ return new Promise((resolve, reject) => {
+ const params = [];
+ if (payload.limit) params.push(`limit=${payload.limit}`);
+ if (payload.around) params.push(`around=${payload.around}`);
+ else if (payload.before) params.push(`before=${payload.before}`);
+ else if (payload.after) params.push(`after=${payload.after}`);
+
+ let endpoint = Constants.Endpoints.channelMessages(channel.id);
+ if (params.length > 0) endpoint += `?${params.join('&')}`;
+ this.rest.makeRequest('get', endpoint, true)
+ .then(resolve)
+ .catch(reject);
+ });
+ }
+
+ updateGuildMember(member, data) {
+ return new Promise((resolve, reject) => {
+ if (data.channel) data.channel_id = this.rest.client.resolver.resolveChannel(data.channel).id;
+ if (data.roles) {
+ if (data.roles instanceof Collection) data.roles = data.roles.array();
+ data.roles = data.roles.map(role => role instanceof Role ? role.id : role);
+ }
+
+ let endpoint = Constants.Endpoints.guildMember(member.guild.id, member.id);
+ // fix your endpoints, discord ;-;
+ if (member.id === this.rest.client.user.id) {
+ if (Object.keys(data).length === 1 && Object.keys(data)[0] === 'nick') {
+ endpoint = Constants.Endpoints.stupidInconsistentGuildEndpoint(member.guild.id);
+ }
+ }
+
+ this.rest.makeRequest('patch', endpoint, true, data)
+ .then(resData => resolve(member.guild._updateMember(member, resData).mem))
+ .catch(reject);
+ });
+ }
+
+ sendTyping(channelID) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('post', `${Constants.Endpoints.channel(channelID)}/typing`, true)
+ .then(resolve)
+ .catch(reject);
+ });
+ }
+
+ banGuildMember(member, deleteDays) {
+ return new Promise((resolve, reject) => {
+ const data = {
+ 'delete-message-days': deleteDays,
+ };
+ this.rest.makeRequest('put', `${Constants.Endpoints.guildBans(member.guild.id)}/${member.id}`, true, data)
+ .then(() => resolve(member))
+ .catch(reject);
+ });
+ }
+
+ unbanGuildMember(guild, member) {
+ return new Promise((resolve, reject) => {
+ member = this.rest.client.resolver.resolveUser(member);
+ if (!member) throw new Error('cannot unban a user that is not a user resolvable');
+ const listener = (eGuild, eUser) => {
+ if (guild.id === guild.id && member.id === eUser.id) {
+ this.rest.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
+ resolve(eUser);
+ }
+ };
+ this.rest.client.on(Constants.Events.GUILD_BAN_REMOVE, listener);
+ this.rest.makeRequest('del', `${Constants.Endpoints.guildBans(guild.id)}/${member.id}`, true).catch(reject);
+ });
+ }
+
+ getGuildBans(guild) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('get', Constants.Endpoints.guildBans(guild.id), true).then(banItems => {
+ const bannedUsers = new Collection();
+ for (const banItem of banItems) {
+ const user = this.rest.client.dataManager.newUser(banItem.user);
+ bannedUsers.set(user.id, user);
+ }
+ resolve(bannedUsers);
+ }).catch(reject);
+ });
+ }
+
+ updateGuildRole(role, _data) {
+ return new Promise((resolve, reject) => {
+ const data = {};
+ data.name = _data.name || role.name;
+ data.position = _data.position || role.position;
+ data.color = _data.color || role.color;
+ if (data.color.startsWith('#')) data.color = parseInt(data.color.replace('#', ''), 16);
+ data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
+
+ if (_data.permissions) {
+ let perms = 0;
+ for (let perm of _data.permissions) {
+ if (typeof perm === 'string') perm = Constants.PermissionFlags[perm];
+ perms |= perm;
+ }
+ data.permissions = perms;
+ } else {
+ data.permissions = role.permissions;
+ }
+
+ this.rest.makeRequest('patch', Constants.Endpoints.guildRole(role.guild.id, role.id), true, data).then(_role => {
+ resolve(this.rest.client.actions.GuildRoleUpdate.handle({
+ role: _role,
+ guild_id: role.guild.id,
+ }).updated);
+ }).catch(reject);
+ });
+ }
+
+ pinMessage(message) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('put', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true)
+ .then(() => resolve(message))
+ .catch(reject);
+ });
+ }
+
+ unpinMessage(message) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('del', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true)
+ .then(() => resolve(message))
+ .catch(reject);
+ });
+ }
+
+ getChannelPinnedMessages(channel) {
+ return this.rest.makeRequest('get', `${Constants.Endpoints.channel(channel.id)}/pins`, true);
+ }
+
+ createChannelInvite(channel, options) {
+ return new Promise((resolve, reject) => {
+ const payload = {};
+ payload.temporary = options.temporary;
+ payload.max_age = options.maxAge;
+ payload.max_uses = options.maxUses;
+
+ this.rest.makeRequest('post', `${Constants.Endpoints.channelInvites(channel.id)}`, true, payload)
+ .then(invite => resolve(new Invite(this.rest.client, invite)))
+ .catch(reject);
+ });
+ }
+
+ deleteInvite(invite) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('del', Constants.Endpoints.invite(invite.code), true)
+ .then(() => resolve(invite))
+ .catch(reject);
+ });
+ }
+
+ getGuildInvites(guild) {
+ return new Promise((resolve, reject) => {
+ this.rest.makeRequest('get', Constants.Endpoints.guildInvites(guild.id), true).then(inviteItems => {
+ const invites = new Collection();
+ for (const inviteItem of inviteItems) {
+ const invite = new Invite(this.rest.client, inviteItem);
+ invites.set(invite.code, invite);
+ }
+ resolve(invites);
+ }).catch(reject);
+ });
+ }
+}
+
+module.exports = RESTMethods;
diff --git a/src/client/rest/RequestHandlers/Batch.js b/src/client/rest/RequestHandlers/Batch.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/client/rest/RequestHandlers/RequestHandler.js b/src/client/rest/RequestHandlers/RequestHandler.js
new file mode 100644
index 000000000..a1a2f3475
--- /dev/null
+++ b/src/client/rest/RequestHandlers/RequestHandler.js
@@ -0,0 +1,51 @@
+/**
+ * A base class for different types of rate limiting handlers for the REST API.
+ * @private
+ */
+class RequestHandler {
+ /**
+ * @param {RESTManager} restManager The REST manager to use
+ */
+ constructor(restManager) {
+ /**
+ * The RESTManager that instantiated this RequestHandler
+ * @type {RESTManager}
+ */
+ this.restManager = restManager;
+
+ /**
+ * A list of requests that have yet to be processed.
+ * @type {APIRequest[]}
+ */
+ this.queue = [];
+ }
+
+ /**
+ * Whether or not the client is being rate limited on every endpoint.
+ * @type {boolean}
+ */
+ get globalLimit() {
+ return this.restManager.globallyRateLimited;
+ }
+
+ set globalLimit(value) {
+ this.restManager.globallyRateLimited = value;
+ }
+
+ /**
+ * Push a new API request into this bucket
+ * @param {APIRequest} request The new request to push into the queue
+ */
+ push(request) {
+ this.queue.push(request);
+ }
+
+ /**
+ * Attempts to get this RequestHandler to process its current queue
+ */
+ handle() {
+ return;
+ }
+}
+
+module.exports = RequestHandler;
diff --git a/src/client/rest/RequestHandlers/Sequential.js b/src/client/rest/RequestHandlers/Sequential.js
new file mode 100644
index 000000000..c971c198f
--- /dev/null
+++ b/src/client/rest/RequestHandlers/Sequential.js
@@ -0,0 +1,103 @@
+const RequestHandler = require('./RequestHandler');
+
+/**
+ * Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto
+ * the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account,
+ * but it can be slower.
+ * @extends {RequestHandler}
+ * @private
+ */
+class SequentialRequestHandler extends RequestHandler {
+ /**
+ * @param {RESTManager} restManager The REST manager to use
+ * @param {string} endpoint The endpoint to handle
+ */
+ constructor(restManager, endpoint) {
+ super(restManager, endpoint);
+
+ /**
+ * Whether this rate limiter is waiting for a response from a request
+ * @type {boolean}
+ */
+ this.waiting = false;
+
+ /**
+ * The endpoint that this handler is handling
+ * @type {string}
+ */
+ this.endpoint = endpoint;
+
+ /**
+ * The time difference between Discord's Dates and the local computer's Dates. A positive number means the local
+ * computer's time is ahead of Discord's.
+ * @type {number}
+ */
+ this.timeDifference = 0;
+ }
+
+ push(request) {
+ super.push(request);
+ this.handle();
+ }
+
+ /**
+ * Performs a request then resolves a promise to indicate its readiness for a new request
+ * @param {APIRequest} item The item to execute
+ * @returns {Promise}
+ */
+ execute(item) {
+ return new Promise(resolve => {
+ item.request.gen().end((err, res) => {
+ if (res && res.headers) {
+ this.requestLimit = res.headers['x-ratelimit-limit'];
+ this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
+ this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
+ this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
+ }
+ if (err) {
+ if (err.status === 429) {
+ this.restManager.client.setTimeout(() => {
+ this.waiting = false;
+ this.globalLimit = false;
+ resolve();
+ }, Number(res.headers['retry-after']) + 500);
+ if (res.headers['x-ratelimit-global']) {
+ this.globalLimit = true;
+ }
+ } else {
+ this.queue.shift();
+ this.waiting = false;
+ item.reject(err);
+ resolve(err);
+ }
+ } else {
+ this.queue.shift();
+ this.globalLimit = false;
+ const data = res && res.body ? res.body : {};
+ item.resolve(data);
+ if (this.requestRemaining === 0) {
+ this.restManager.client.setTimeout(() => {
+ this.waiting = false;
+ resolve(data);
+ }, (this.requestResetTime - Date.now()) + this.timeDifference + 1000);
+ } else {
+ this.waiting = false;
+ resolve(data);
+ }
+ }
+ });
+ });
+ }
+
+ handle() {
+ super.handle();
+
+ if (this.waiting || this.queue.length === 0 || this.globalLimit) return;
+ this.waiting = true;
+
+ const item = this.queue[0];
+ this.execute(item).then(() => this.handle());
+ }
+}
+
+module.exports = SequentialRequestHandler;
diff --git a/src/client/rest/UserAgentManager.js b/src/client/rest/UserAgentManager.js
new file mode 100644
index 000000000..12393ffd1
--- /dev/null
+++ b/src/client/rest/UserAgentManager.js
@@ -0,0 +1,22 @@
+const Constants = require('../../util/Constants');
+
+class UserAgentManager {
+ constructor(restManager) {
+ this.restManager = restManager;
+ this._userAgent = {
+ url: 'https://github.com/hydrabolt/discord.js',
+ version: Constants.Package.version,
+ };
+ }
+
+ set(info) {
+ this._userAgent.url = info.url || 'https://github.com/hydrabolt/discord.js';
+ this._userAgent.version = info.version || Constants.Package.version;
+ }
+
+ get userAgent() {
+ return `DiscordBot (${this._userAgent.url}, ${this._userAgent.version})`;
+ }
+}
+
+module.exports = UserAgentManager;
diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js
new file mode 100644
index 000000000..0b119914b
--- /dev/null
+++ b/src/client/voice/ClientVoiceManager.js
@@ -0,0 +1,124 @@
+const Collection = require('../../util/Collection');
+const mergeDefault = require('../../util/MergeDefault');
+const Constants = require('../../util/Constants');
+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}
+ */
+ this.connections = new Collection();
+ /**
+ * Pending connection attempts, maps Guild ID to VoiceChannel
+ * @type {Collection}
+ */
+ this.pending = new Collection();
+ }
+
+ /**
+ * Checks whether a pending request can be processed
+ * @private
+ * @param {string} guildID The ID of the Guild
+ */
+ _checkPendingReady(guildID) {
+ const pendingRequest = this.pending.get(guildID);
+ if (!pendingRequest) throw new Error('Guild not pending');
+ if (pendingRequest.token && pendingRequest.sessionID && pendingRequest.endpoint) {
+ const { channel, token, sessionID, endpoint, resolve, reject } = pendingRequest;
+ const voiceConnection = new VoiceConnection(this, channel, token, sessionID, endpoint, resolve, reject);
+ this.pending.delete(guildID);
+ this.connections.set(guildID, voiceConnection);
+ voiceConnection.once('disconnected', () => {
+ this.connections.delete(guildID);
+ });
+ }
+ }
+
+ /**
+ * Called when the Client receives information about this voice server update.
+ * @param {string} guildID The ID of the Guild
+ * @param {string} token The token to authorise with
+ * @param {string} endpoint The endpoint to connect to
+ */
+ _receivedVoiceServer(guildID, token, endpoint) {
+ const pendingRequest = this.pending.get(guildID);
+ if (!pendingRequest) throw new Error('Guild not pending');
+ pendingRequest.token = token;
+ // remove the port otherwise it errors ¯\_(ツ)_/¯
+ pendingRequest.endpoint = endpoint.match(/([^:]*)/)[0];
+ this._checkPendingReady(guildID);
+ }
+
+ /**
+ * Called when the Client receives information about the voice state update.
+ * @param {string} guildID The ID of the Guild
+ * @param {string} sessionID The session id to authorise with
+ */
+ _receivedVoiceStateUpdate(guildID, sessionID) {
+ const pendingRequest = this.pending.get(guildID);
+ if (!pendingRequest) throw new Error('Guild not pending');
+ pendingRequest.sessionID = sessionID;
+ this._checkPendingReady(guildID);
+ }
+
+ /**
+ * Sends a request to the main gateway to join a voice channel
+ * @param {VoiceChannel} channel The channel to join
+ * @param {Object} [options] The options to provide
+ */
+ _sendWSJoin(channel, options = {}) {
+ options = mergeDefault({
+ guild_id: channel.guild.id,
+ channel_id: channel.id,
+ self_mute: false,
+ self_deaf: false,
+ }, options);
+ this.client.ws.send({
+ op: Constants.OPCodes.VOICE_STATE_UPDATE,
+ d: options,
+ });
+ }
+
+ /**
+ * Sets up a request to join a voice channel
+ * @param {VoiceChannel} channel The voice channel to join
+ * @returns {Promise}
+ */
+ joinChannel(channel) {
+ return new Promise((resolve, reject) => {
+ if (this.pending.get(channel.guild.id)) throw new Error('already connecting to a channel in this guild');
+ const existingConn = this.connections.get(channel.guild.id);
+ if (existingConn) {
+ if (existingConn.channel.id !== channel.id) {
+ this._sendWSJoin(channel);
+ this.connections.get(channel.guild.id).channel = channel;
+ }
+ resolve(existingConn);
+ return;
+ }
+ this.pending.set(channel.guild.id, {
+ channel,
+ sessionID: null,
+ token: null,
+ endpoint: null,
+ resolve,
+ reject,
+ });
+ this._sendWSJoin(channel);
+ this.client.setTimeout(() => reject(new Error('connection not established in 15s time period')), 15000);
+ });
+ }
+}
+
+module.exports = ClientVoiceManager;
diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js
new file mode 100644
index 000000000..d719b4560
--- /dev/null
+++ b/src/client/voice/VoiceConnection.js
@@ -0,0 +1,259 @@
+const VoiceConnectionWebSocket = require('./VoiceConnectionWebSocket');
+const VoiceConnectionUDPClient = require('./VoiceConnectionUDPClient');
+const VoiceReceiver = require('./receiver/VoiceReceiver');
+const Constants = require('../../util/Constants');
+const EventEmitter = require('events').EventEmitter;
+const DefaultPlayer = require('./player/DefaultPlayer');
+
+/**
+ * Represents a connection to a Voice Channel in Discord.
+ * ```js
+ * // obtained using:
+ * voiceChannel.join().then(connection => {
+ *
+ * });
+ * ```
+ * @extends {EventEmitter}
+ */
+class VoiceConnection extends EventEmitter {
+ constructor(manager, channel, token, sessionID, endpoint, resolve, reject) {
+ super();
+ /**
+ * The voice manager of this connection
+ * @type {ClientVoiceManager}
+ * @private
+ */
+ this.manager = manager;
+ /**
+ * The player
+ * @type {BasePlayer}
+ */
+ this.player = new DefaultPlayer(this);
+ /**
+ * The endpoint of the connection
+ * @type {string}
+ */
+ this.endpoint = endpoint;
+ /**
+ * The VoiceChannel for this connection
+ * @type {VoiceChannel}
+ */
+ this.channel = channel;
+ /**
+ * The WebSocket connection for this voice connection
+ * @type {VoiceConnectionWebSocket}
+ * @private
+ */
+ this.websocket = new VoiceConnectionWebSocket(this, channel.guild.id, token, sessionID, endpoint);
+ /**
+ * Whether or not the connection is ready
+ * @type {boolean}
+ */
+ this.ready = false;
+ /**
+ * The resolve function for the promise associated with creating this connection
+ * @type {function}
+ * @private
+ */
+ this._resolve = resolve;
+ /**
+ * The reject function for the promise associated with creating this connection
+ * @type {function}
+ * @private
+ */
+ this._reject = reject;
+ this.ssrcMap = new Map();
+ this.queue = [];
+ this.receivers = [];
+ this.bindListeners();
+ }
+
+ /**
+ * Executed whenever an error occurs with the UDP/WebSocket sub-client
+ * @private
+ * @param {Error} err The encountered error
+ */
+ _onError(err) {
+ this._reject(err);
+ /**
+ * Emitted whenever the connection encounters a fatal error.
+ * @event VoiceConnection#error
+ * @param {Error} error The encountered error
+ */
+ this.emit('error', err);
+ this._shutdown(err);
+ }
+
+ /**
+ * Disconnects the Client from the Voice Channel
+ * @param {string} [reason='user requested'] The reason of the disconnection
+ */
+ disconnect(reason = 'user requested') {
+ this.manager.client.ws.send({
+ op: Constants.OPCodes.VOICE_STATE_UPDATE,
+ d: {
+ guild_id: this.channel.guild.id,
+ channel_id: null,
+ self_mute: false,
+ self_deaf: false,
+ },
+ });
+ this._shutdown(reason);
+ }
+
+ _onClose(e) {
+ e = e && e.code === 1000 ? null : e;
+ return this._shutdown(e);
+ }
+
+ _shutdown(e) {
+ if (!this.ready) return;
+ this.ready = false;
+ this.websocket._shutdown();
+ this.player._shutdown();
+ if (this.udp) this.udp._shutdown();
+ /**
+ * Emit once the voice connection has disconnected.
+ * @event VoiceConnection#disconnected
+ * @param {Error} error The encountered error, if any
+ */
+ this.emit('disconnected', e);
+ }
+
+ /**
+ * Binds listeners to the WebSocket and UDP sub-clients
+ * @private
+ */
+ bindListeners() {
+ this.websocket.on('error', err => this._onError(err));
+ this.websocket.on('close', err => this._onClose(err));
+ this.websocket.on('ready-for-udp', data => {
+ this.udp = new VoiceConnectionUDPClient(this, data);
+ this.data = data;
+ this.udp.on('error', err => this._onError(err));
+ this.udp.on('close', err => this._onClose(err));
+ });
+ this.websocket.on('ready', secretKey => {
+ this.data.secret = secretKey;
+ this.ready = true;
+ /**
+ * Emitted once the connection is ready (joining voice channels resolves when the connection is ready anyway)
+ * @event VoiceConnection#ready
+ */
+ this._resolve(this);
+ this.emit('ready');
+ });
+ this.once('ready', () => {
+ setImmediate(() => {
+ for (const item of this.queue) this.emit(...item);
+ this.queue = [];
+ });
+ });
+ this.manager.client.on(Constants.Events.VOICE_STATE_UPDATE, (oldM, newM) => {
+ if (oldM.voiceChannel && oldM.voiceChannel.guild.id === this.channel.guild.id && !newM.voiceChannel) {
+ const user = newM.user;
+ for (const receiver of this.receivers) {
+ const opusStream = receiver.opusStreams.get(user.id);
+ const pcmStream = receiver.pcmStreams.get(user.id);
+ if (opusStream) {
+ opusStream.push(null);
+ opusStream.open = false;
+ receiver.opusStreams.delete(user.id);
+ }
+ if (pcmStream) {
+ pcmStream.push(null);
+ pcmStream.open = false;
+ receiver.pcmStreams.delete(user.id);
+ }
+ }
+ }
+ });
+ this.websocket.on('speaking', data => {
+ const guild = this.channel.guild;
+ const user = this.manager.client.users.get(data.user_id);
+ this.ssrcMap.set(+data.ssrc, user);
+ if (!data.speaking) {
+ for (const receiver of this.receivers) {
+ const opusStream = receiver.opusStreams.get(user.id);
+ const pcmStream = receiver.pcmStreams.get(user.id);
+ if (opusStream) {
+ opusStream.push(null);
+ opusStream.open = false;
+ receiver.opusStreams.delete(user.id);
+ }
+ if (pcmStream) {
+ pcmStream.push(null);
+ pcmStream.open = false;
+ receiver.pcmStreams.delete(user.id);
+ }
+ }
+ }
+ /**
+ * Emitted whenever a user starts/stops speaking
+ * @event VoiceConnection#speaking
+ * @param {User} user The user that has started/stopped speaking
+ * @param {boolean} speaking Whether or not the user is speaking
+ */
+ if (this.ready) this.emit('speaking', user, data.speaking);
+ else this.queue.push(['speaking', user, data.speaking]);
+ guild._memberSpeakUpdate(data.user_id, data.speaking);
+ });
+ }
+
+ /**
+ * Play the given file in the voice connection
+ * @param {string} file The path to the file
+ * @returns {StreamDispatcher}
+ * @example
+ * // play files natively
+ * voiceChannel.join()
+ * .then(connection => {
+ * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
+ * })
+ * .catch(console.log);
+ */
+ playFile(file) {
+ return this.player.playFile(file);
+ }
+
+ /**
+ * Plays and converts an audio stream in the voice connection
+ * @param {ReadableStream} stream The audio stream to play
+ * @returns {StreamDispatcher}
+ * @example
+ * // play streams using ytdl-core
+ * const ytdl = require('ytdl-core');
+ * voiceChannel.join()
+ * .then(connection => {
+ * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', {filter : 'audioonly'});
+ * const dispatcher = connection.playStream(stream);
+ * })
+ * .catch(console.log);
+ */
+ playStream(stream) {
+ return this.player.playStream(stream);
+ }
+
+ /**
+ * Plays a stream of 16-bit signed stereo PCM at 48KHz.
+ * @param {ReadableStream} stream The audio stream to play.
+ * @returns {StreamDispatcher}
+ */
+ playConvertedStream(stream) {
+ this._shutdown();
+ const dispatcher = this.player.playPCMStream(stream);
+ return dispatcher;
+ }
+
+ /**
+ * Creates a VoiceReceiver so you can start listening to voice data. It's recommended to only create one of these.
+ * @returns {VoiceReceiver}
+ */
+ createReceiver() {
+ const rcv = new VoiceReceiver(this);
+ this.receivers.push(rcv);
+ return rcv;
+ }
+}
+
+module.exports = VoiceConnection;
diff --git a/src/client/voice/VoiceConnectionUDPClient.js b/src/client/voice/VoiceConnectionUDPClient.js
new file mode 100644
index 000000000..add7b9c4f
--- /dev/null
+++ b/src/client/voice/VoiceConnectionUDPClient.js
@@ -0,0 +1,84 @@
+const udp = require('dgram');
+const dns = require('dns');
+const Constants = require('../../util/Constants');
+const EventEmitter = require('events').EventEmitter;
+
+class VoiceConnectionUDPClient extends EventEmitter {
+ constructor(voiceConnection, data) {
+ super();
+ this.voiceConnection = voiceConnection;
+ this.count = 0;
+ this.data = data;
+ this.dnsLookup();
+ }
+
+ dnsLookup() {
+ dns.lookup(this.voiceConnection.endpoint, (err, address) => {
+ if (err) {
+ this.emit('error', err);
+ return;
+ }
+ this.connectUDP(address);
+ });
+ }
+
+ send(packet) {
+ if (this.udpSocket) {
+ try {
+ this.udpSocket.send(packet, 0, packet.length, this.data.port, this.udpIP);
+ } catch (err) {
+ this.emit('error', err);
+ }
+ }
+ }
+
+ _shutdown() {
+ if (this.udpSocket) {
+ try {
+ this.udpSocket.close();
+ } catch (err) {
+ if (err.message !== 'Not running') this.emit('error', err);
+ }
+ this.udpSocket = null;
+ }
+ }
+
+ connectUDP(address) {
+ this.udpIP = address;
+ this.udpSocket = udp.createSocket('udp4');
+
+ // finding local IP
+ // https://discordapp.com/developers/docs/topics/voice-connections#ip-discovery
+ this.udpSocket.once('message', message => {
+ const packet = new Buffer(message);
+ this.localIP = '';
+ for (let i = 4; i < packet.indexOf(0, i); i++) this.localIP += String.fromCharCode(packet[i]);
+ this.localPort = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
+
+ this.voiceConnection.websocket.send({
+ op: Constants.VoiceOPCodes.SELECT_PROTOCOL,
+ d: {
+ protocol: 'udp',
+ data: {
+ address: this.localIP,
+ port: this.localPort,
+ mode: 'xsalsa20_poly1305',
+ },
+ },
+ });
+ });
+
+ this.udpSocket.on('error', (error, message) => {
+ this.emit('error', { error, message });
+ });
+ this.udpSocket.on('close', error => {
+ this.emit('close', error);
+ });
+
+ const blankMessage = new Buffer(70);
+ blankMessage.writeUIntBE(this.data.ssrc, 0, 4);
+ this.send(blankMessage);
+ }
+}
+
+module.exports = VoiceConnectionUDPClient;
diff --git a/src/client/voice/VoiceConnectionWebSocket.js b/src/client/voice/VoiceConnectionWebSocket.js
new file mode 100644
index 000000000..1c4543eed
--- /dev/null
+++ b/src/client/voice/VoiceConnectionWebSocket.js
@@ -0,0 +1,113 @@
+const WebSocket = require('ws');
+const Constants = require('../../util/Constants');
+const EventEmitter = require('events').EventEmitter;
+
+class VoiceConnectionWebSocket extends EventEmitter {
+ constructor(voiceConnection, serverID, token, sessionID, endpoint) {
+ super();
+ this.voiceConnection = voiceConnection;
+ this.token = token;
+ this.sessionID = sessionID;
+ this.serverID = serverID;
+ this.heartbeat = null;
+ this.opened = false;
+ this.endpoint = endpoint;
+ this.attempts = 6;
+ this.setupWS();
+ }
+
+ setupWS() {
+ this.attempts--;
+ this.ws = new WebSocket(`wss://${this.endpoint}`, null, { rejectUnauthorized: false });
+ this.ws.onopen = () => this._onOpen();
+ this.ws.onmessage = e => this._onMessage(e);
+ this.ws.onclose = e => this._onClose(e);
+ this.ws.onerror = e => this._onError(e);
+ }
+
+ send(data) {
+ if (this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(data));
+ }
+
+ _shutdown() {
+ if (this.ws) this.ws.close();
+ clearInterval(this.heartbeat);
+ }
+
+ _onOpen() {
+ this.opened = true;
+ this.send({
+ op: Constants.OPCodes.DISPATCH,
+ d: {
+ server_id: this.serverID,
+ user_id: this.voiceConnection.manager.client.user.id,
+ session_id: this.sessionID,
+ token: this.token,
+ },
+ });
+ }
+
+ _onClose(err) {
+ if (!this.opened && this.attempts >= 0) {
+ this.setupWS();
+ return;
+ }
+ this.emit('close', err);
+ }
+
+ _onError(e) {
+ if (!this.opened && this.attempts >= 0) {
+ this.setupWS();
+ return;
+ }
+ this.emit('error', e);
+ }
+
+ _setHeartbeat(interval) {
+ this.heartbeat = this.voiceConnection.manager.client.setInterval(() => {
+ this.send({
+ op: Constants.VoiceOPCodes.HEARTBEAT,
+ d: null,
+ });
+ }, interval);
+ this.send({
+ op: Constants.VoiceOPCodes.HEARTBEAT,
+ d: null,
+ });
+ }
+
+ _onMessage(event) {
+ let packet;
+ try {
+ packet = JSON.parse(event.data);
+ } catch (error) {
+ this._onError(error);
+ return;
+ }
+
+ switch (packet.op) {
+ case Constants.VoiceOPCodes.READY:
+ this._setHeartbeat(packet.d.heartbeat_interval);
+ this.emit('ready-for-udp', packet.d);
+ break;
+ case Constants.VoiceOPCodes.SESSION_DESCRIPTION:
+ this.encryptionMode = packet.d.mode;
+ this.secretKey = new Uint8Array(new ArrayBuffer(packet.d.secret_key.length));
+ for (const index in packet.d.secret_key) this.secretKey[index] = packet.d.secret_key[index];
+ this.emit('ready', this.secretKey);
+ break;
+ case Constants.VoiceOPCodes.SPEAKING:
+ /*
+ { op: 5,
+ d: { user_id: '123123', ssrc: 1, speaking: true } }
+ */
+ this.emit('speaking', packet.d);
+ break;
+ default:
+ this.emit('unknown', packet);
+ break;
+ }
+ }
+}
+
+module.exports = VoiceConnectionWebSocket;
diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js
new file mode 100644
index 000000000..8088017f1
--- /dev/null
+++ b/src/client/voice/dispatcher/StreamDispatcher.js
@@ -0,0 +1,260 @@
+const EventEmitter = require('events').EventEmitter;
+const NaCl = require('tweetnacl');
+
+const nonce = new Buffer(24);
+nonce.fill(0);
+
+/**
+ * The class that sends voice packet data to the voice connection.
+ * ```js
+ * // obtained using:
+ * voiceChannel.join().then(connection => {
+ * // you can play a file or a stream here:
+ * connection.playFile('./file.mp3').then(dispatcher => {
+ *
+ * });
+ * });
+ * ```
+ * @extends {EventEmitter}
+ */
+class StreamDispatcher extends EventEmitter {
+ constructor(player, stream, sd) {
+ super();
+ this.player = player;
+ this.stream = stream;
+ this.streamingData = {
+ channels: 2,
+ count: sd.count,
+ sequence: sd.sequence,
+ timestamp: sd.timestamp,
+ };
+ this._startStreaming();
+ this._triggered = false;
+ this._volume = 1;
+ }
+
+ /**
+ * Emitted when the dispatcher starts/stops speaking
+ * @event StreamDispatcher#speaking
+ * @param {boolean} value Whether or not the dispatcher is speaking
+ */
+ _setSpeaking(value) {
+ this.speaking = value;
+ this.emit('speaking', value);
+ }
+
+ _sendBuffer(buffer, sequence, timestamp) {
+ this.player.connection.udp.send(
+ this._createPacket(sequence, timestamp, this.player.opusEncoder.encode(buffer))
+ );
+ }
+
+ _createPacket(sequence, timestamp, buffer) {
+ const packetBuffer = new Buffer(buffer.length + 28);
+ packetBuffer.fill(0);
+ packetBuffer[0] = 0x80;
+ packetBuffer[1] = 0x78;
+
+ packetBuffer.writeUIntBE(sequence, 2, 2);
+ packetBuffer.writeUIntBE(timestamp, 4, 4);
+ packetBuffer.writeUIntBE(this.player.connection.data.ssrc, 8, 4);
+
+ packetBuffer.copy(nonce, 0, 0, 12);
+ buffer = NaCl.secretbox(buffer, nonce, this.player.connection.data.secret);
+
+ for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
+
+ return packetBuffer;
+ }
+
+ _applyVolume(buffer) {
+ if (this._volume === 1) return buffer;
+
+ const out = new Buffer(buffer.length);
+ for (let i = 0; i < buffer.length; i += 2) {
+ if (i >= buffer.length - 1) break;
+ const uint = Math.min(32767, Math.max(-32767, Math.floor(this._volume * buffer.readInt16LE(i))));
+ out.writeInt16LE(uint, i);
+ }
+
+ return out;
+ }
+
+ _send() {
+ try {
+ if (this._triggered) {
+ this._setSpeaking(false);
+ return;
+ }
+
+ const data = this.streamingData;
+
+ if (data.missed >= 5) {
+ this._triggerTerminalState('error', new Error('stream is not generating fast enough'));
+ return;
+ }
+
+ if (this.paused) {
+ data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
+ this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10);
+ return;
+ }
+
+ this._setSpeaking(true);
+
+ const bufferLength = 1920 * data.channels;
+ let buffer = this.stream.read(bufferLength);
+ if (!buffer) {
+ data.missed++;
+ this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10);
+ return;
+ }
+
+ data.missed = 0;
+
+ if (buffer.length !== bufferLength) {
+ const newBuffer = new Buffer(bufferLength).fill(0);
+ buffer.copy(newBuffer);
+ buffer = newBuffer;
+ }
+
+ buffer = this._applyVolume(buffer);
+
+ data.count++;
+ data.sequence = (data.sequence + 1) < (65536) ? data.sequence + 1 : 0;
+ data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
+
+ this._sendBuffer(buffer, data.sequence, data.timestamp);
+
+ const nextTime = data.startTime + (data.count * data.length);
+ this.player.connection.manager.client.setTimeout(() => this._send(), data.length + (nextTime - Date.now()));
+ } catch (e) {
+ this._triggerTerminalState('error', e);
+ }
+ }
+
+ /**
+ * Emitted once the stream has ended. Attach a `once` listener to this.
+ * @event StreamDispatcher#end
+ */
+ _triggerEnd() {
+ this.emit('end');
+ }
+
+ /**
+ * Emitted once the stream has encountered an error. Attach a `once` listener to this. Also emits `end`.
+ * @event StreamDispatcher#error
+ * @param {Error} err The encountered error
+ */
+ _triggerError(err) {
+ this.emit('end');
+ this.emit('error', err);
+ }
+
+ _triggerTerminalState(state, err) {
+ if (this._triggered) return;
+
+ /**
+ * Emitted when the stream wants to give debug information.
+ * @event StreamDispatcher#debug
+ * @param {string} information The debug information
+ */
+ this.emit('debug', `triggered terminal state ${state} - stream is now dead`);
+ this._triggered = true;
+ this._setSpeaking(false);
+ switch (state) {
+ case 'end':
+ this._triggerEnd(err);
+ break;
+ case 'error':
+ this._triggerError(err);
+ break;
+ default:
+ this.emit('error', 'unknown trigger state');
+ break;
+ }
+ }
+
+ _startStreaming() {
+ if (!this.stream) {
+ this.emit('error', 'no stream');
+ return;
+ }
+
+ this.stream.on('end', err => this._triggerTerminalState('end', err));
+ this.stream.on('error', err => this._triggerTerminalState('error', err));
+
+ const data = this.streamingData;
+ data.length = 20;
+ data.missed = 0;
+ data.startTime = Date.now();
+
+ this.stream.once('readable', () => this._send());
+ }
+
+ _setPaused(paused) {
+ if (paused) {
+ this.paused = true;
+ this._setSpeaking(false);
+ } else {
+ this.paused = false;
+ this._setSpeaking(true);
+ }
+ }
+
+ /**
+ * Stops the current stream permanently and emits an `end` event.
+ */
+ end() {
+ this._triggerTerminalState('end', 'user requested');
+ }
+
+ /**
+ * The volume of the stream, relative to the stream's input volume
+ * @type {number}
+ * @readonly
+ */
+ get volume() {
+ return this._volume;
+ }
+
+ /**
+ * Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
+ * @param {number} volume The volume that you want to set
+ */
+ setVolume(volume) {
+ this._volume = volume;
+ }
+
+ /**
+ * Set the volume in decibels
+ * @param {number} db The decibels
+ */
+ setVolumeDecibels(db) {
+ this._volume = Math.pow(10, db / 20);
+ }
+
+ /**
+ * Set the volume so that a perceived value of 0.5 is half the perceived volume etc.
+ * @param {number} value The value for the volume
+ */
+ setVolumeLogarithmic(value) {
+ this._volume = Math.pow(value, 1.660964);
+ }
+
+ /**
+ * Stops sending voice packets to the voice connection (stream may still progress however)
+ */
+ pause() {
+ this._setPaused(true);
+ }
+
+ /**
+ * Resumes sending voice packets to the voice connection (may be further on in the stream than when paused)
+ */
+ resume() {
+ this._setPaused(false);
+ }
+}
+
+module.exports = StreamDispatcher;
diff --git a/src/client/voice/opus/BaseOpusEngine.js b/src/client/voice/opus/BaseOpusEngine.js
new file mode 100644
index 000000000..6c3ba6e34
--- /dev/null
+++ b/src/client/voice/opus/BaseOpusEngine.js
@@ -0,0 +1,15 @@
+class BaseOpus {
+ constructor(player) {
+ this.player = player;
+ }
+
+ encode(buffer) {
+ return buffer;
+ }
+
+ decode(buffer) {
+ return buffer;
+ }
+}
+
+module.exports = BaseOpus;
diff --git a/src/client/voice/opus/NodeOpusEngine.js b/src/client/voice/opus/NodeOpusEngine.js
new file mode 100644
index 000000000..10f287b9d
--- /dev/null
+++ b/src/client/voice/opus/NodeOpusEngine.js
@@ -0,0 +1,27 @@
+const OpusEngine = require('./BaseOpusEngine');
+
+let opus;
+
+class NodeOpusEngine extends OpusEngine {
+ constructor(player) {
+ super(player);
+ try {
+ opus = require('node-opus');
+ } catch (err) {
+ throw err;
+ }
+ this.encoder = new opus.OpusEncoder(48000, 2);
+ }
+
+ encode(buffer) {
+ super.encode(buffer);
+ return this.encoder.encode(buffer, 1920);
+ }
+
+ decode(buffer) {
+ super.decode(buffer);
+ return this.encoder.decode(buffer, 1920);
+ }
+}
+
+module.exports = NodeOpusEngine;
diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js
new file mode 100644
index 000000000..a7f225f22
--- /dev/null
+++ b/src/client/voice/opus/OpusEngineList.js
@@ -0,0 +1,24 @@
+const list = [
+ require('./NodeOpusEngine'),
+ require('./OpusScriptEngine'),
+];
+
+function fetch(Encoder) {
+ try {
+ return new Encoder();
+ } catch (err) {
+ return null;
+ }
+}
+
+exports.add = encoder => {
+ list.push(encoder);
+};
+
+exports.fetch = () => {
+ for (const encoder of list) {
+ const fetched = fetch(encoder);
+ if (fetched) return fetched;
+ }
+ throw new Error('could not find an opus engine');
+};
diff --git a/src/client/voice/opus/OpusScriptEngine.js b/src/client/voice/opus/OpusScriptEngine.js
new file mode 100644
index 000000000..33b4ff5a6
--- /dev/null
+++ b/src/client/voice/opus/OpusScriptEngine.js
@@ -0,0 +1,27 @@
+const OpusEngine = require('./BaseOpusEngine');
+
+let OpusScript;
+
+class NodeOpusEngine extends OpusEngine {
+ constructor(player) {
+ super(player);
+ try {
+ OpusScript = require('opusscript');
+ } catch (err) {
+ throw err;
+ }
+ this.encoder = new OpusScript(48000, 2);
+ }
+
+ encode(buffer) {
+ super.encode(buffer);
+ return this.encoder.encode(buffer, 960);
+ }
+
+ decode(buffer) {
+ super.decode(buffer);
+ return this.encoder.decode(buffer);
+ }
+}
+
+module.exports = NodeOpusEngine;
diff --git a/src/client/voice/pcm/ConverterEngine.js b/src/client/voice/pcm/ConverterEngine.js
new file mode 100644
index 000000000..6b7502f90
--- /dev/null
+++ b/src/client/voice/pcm/ConverterEngine.js
@@ -0,0 +1,14 @@
+const EventEmitter = require('events').EventEmitter;
+
+class ConverterEngine extends EventEmitter {
+ constructor(player) {
+ super();
+ this.player = player;
+ }
+
+ createConvertStream() {
+ return;
+ }
+}
+
+module.exports = ConverterEngine;
diff --git a/src/client/voice/pcm/ConverterEngineList.js b/src/client/voice/pcm/ConverterEngineList.js
new file mode 100644
index 000000000..56d430e48
--- /dev/null
+++ b/src/client/voice/pcm/ConverterEngineList.js
@@ -0,0 +1 @@
+exports.fetch = () => require('./FfmpegConverterEngine');
diff --git a/src/client/voice/pcm/FfmpegConverterEngine.js b/src/client/voice/pcm/FfmpegConverterEngine.js
new file mode 100644
index 000000000..64dca3692
--- /dev/null
+++ b/src/client/voice/pcm/FfmpegConverterEngine.js
@@ -0,0 +1,40 @@
+const ConverterEngine = require('./ConverterEngine');
+const ChildProcess = require('child_process');
+
+class FfmpegConverterEngine extends ConverterEngine {
+ constructor(player) {
+ super(player);
+ this.command = chooseCommand();
+ }
+
+ handleError(encoder, err) {
+ if (encoder.destroy) encoder.destroy();
+ this.emit('error', err);
+ }
+
+ createConvertStream() {
+ super.createConvertStream();
+ const encoder = ChildProcess.spawn(this.command, [
+ '-analyzeduration', '0',
+ '-loglevel', '0',
+ '-i', '-',
+ '-f', 's16le',
+ '-ar', '48000',
+ '-ss', '0',
+ 'pipe:1',
+ ], { stdio: ['pipe', 'pipe', 'ignore'] });
+ encoder.on('error', e => this.handleError(encoder, e));
+ encoder.stdin.on('error', e => this.handleError(encoder, e));
+ encoder.stdout.on('error', e => this.handleError(encoder, e));
+ return encoder;
+ }
+}
+
+function chooseCommand() {
+ for (const cmd of ['ffmpeg', 'avconv', './ffmpeg', './avconv']) {
+ if (!ChildProcess.spawnSync(cmd, ['-h']).error) return cmd;
+ }
+ return null;
+}
+
+module.exports = FfmpegConverterEngine;
diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js
new file mode 100644
index 000000000..e41048d0a
--- /dev/null
+++ b/src/client/voice/player/BasePlayer.js
@@ -0,0 +1,100 @@
+const OpusEngines = require('../opus/OpusEngineList');
+const ConverterEngines = require('../pcm/ConverterEngineList');
+const Constants = require('../../../util/Constants');
+const StreamDispatcher = require('../dispatcher/StreamDispatcher');
+const EventEmitter = require('events').EventEmitter;
+
+class VoiceConnectionPlayer extends EventEmitter {
+ constructor(connection) {
+ super();
+ this.connection = connection;
+ this.opusEncoder = OpusEngines.fetch();
+ const Engine = ConverterEngines.fetch();
+ this.converterEngine = new Engine(this);
+ this.converterEngine.on('error', err => {
+ this._shutdown();
+ this.emit('error', err);
+ });
+ this.speaking = false;
+ this.processMap = new Map();
+ this.dispatcher = null;
+ this._streamingData = {
+ count: 0,
+ sequence: 0,
+ timestamp: 0,
+ };
+ }
+
+ convertStream(stream) {
+ const encoder = this.converterEngine.createConvertStream();
+ stream.pipe(encoder.stdin);
+ this.processMap.set(encoder.stdout, {
+ pcmConverter: encoder,
+ inputStream: stream,
+ });
+ return encoder.stdout;
+ }
+
+ _shutdown() {
+ this.speaking = false;
+ for (const stream of this.processMap.keys()) this.killStream(stream);
+ }
+
+ killStream(stream) {
+ const streams = this.processMap.get(stream);
+ this._streamingData = this.dispatcher.streamingData;
+ this.emit('debug', 'cleaning up streams after end/error');
+ if (streams) {
+ if (streams.inputStream && streams.pcmConverter) {
+ try {
+ if (streams.inputStream.unpipe) {
+ streams.inputStream.unpipe(streams.pcmConverter.stdin);
+ this.emit('debug', 'stream kill part 4/5 pass');
+ }
+ if (streams.pcmConverter.stdout.destroy) {
+ streams.pcmConverter.stdout.destroy();
+ this.emit('debug', 'stream kill part 2/5 pass');
+ }
+ if (streams.pcmConverter && streams.pcmConverter.kill) {
+ streams.pcmConverter.kill('SIGINT');
+ this.emit('debug', 'stream kill part 3/5 pass');
+ }
+ if (streams.pcmConverter.stdin) {
+ streams.pcmConverter.stdin.end();
+ this.emit('debug', 'stream kill part 1/5 pass');
+ }
+ if (streams.inputStream.destroy) {
+ streams.inputStream.destroy();
+ this.emit('debug', 'stream kill part 5/5 pass');
+ }
+ } catch (err) {
+ return err;
+ }
+ }
+ }
+ return null;
+ }
+
+ setSpeaking(value) {
+ if (this.speaking === value) return;
+ this.speaking = value;
+ this.connection.websocket.send({
+ op: Constants.VoiceOPCodes.SPEAKING,
+ d: {
+ speaking: true,
+ delay: 0,
+ },
+ });
+ }
+
+ playPCMStream(pcmStream) {
+ const dispatcher = new StreamDispatcher(this, pcmStream, this._streamingData);
+ dispatcher.on('speaking', value => this.setSpeaking(value));
+ dispatcher.on('end', () => this.killStream(pcmStream));
+ dispatcher.on('error', () => this.killStream(pcmStream));
+ this.dispatcher = dispatcher;
+ return dispatcher;
+ }
+}
+
+module.exports = VoiceConnectionPlayer;
diff --git a/src/client/voice/player/DefaultPlayer.js b/src/client/voice/player/DefaultPlayer.js
new file mode 100644
index 000000000..bd3b688bc
--- /dev/null
+++ b/src/client/voice/player/DefaultPlayer.js
@@ -0,0 +1,17 @@
+const BasePlayer = require('./BasePlayer');
+const fs = require('fs');
+
+class DefaultPlayer extends BasePlayer {
+ playFile(file) {
+ return this.playStream(fs.createReadStream(file));
+ }
+
+ playStream(stream) {
+ this._shutdown();
+ const pcmStream = this.convertStream(stream);
+ const dispatcher = this.playPCMStream(pcmStream);
+ return dispatcher;
+ }
+}
+
+module.exports = DefaultPlayer;
diff --git a/src/client/voice/receiver/VoiceReadable.js b/src/client/voice/receiver/VoiceReadable.js
new file mode 100644
index 000000000..50ace27ae
--- /dev/null
+++ b/src/client/voice/receiver/VoiceReadable.js
@@ -0,0 +1,19 @@
+const Readable = require('stream').Readable;
+
+class VoiceReadable extends Readable {
+ constructor() {
+ super();
+ this._packets = [];
+ this.open = true;
+ }
+
+ _read() {
+ return;
+ }
+
+ _push(d) {
+ if (this.open) this.push(d);
+ }
+}
+
+module.exports = VoiceReadable;
diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js
new file mode 100644
index 000000000..f28fbe463
--- /dev/null
+++ b/src/client/voice/receiver/VoiceReceiver.js
@@ -0,0 +1,123 @@
+const EventEmitter = require('events').EventEmitter;
+const NaCl = require('tweetnacl');
+const Readable = require('./VoiceReadable');
+
+const nonce = new Buffer(24);
+nonce.fill(0);
+
+/**
+ * Receives voice data from a voice connection.
+ * ```js
+ * // obtained using:
+ * voiceChannel.join().then(connection => {
+ * const receiver = connection.createReceiver();
+ * });
+ * ```
+ * @extends {EventEmitter}
+ */
+class VoiceReceiver extends EventEmitter {
+ constructor(connection) {
+ super();
+ /*
+ need a queue because we don't get the ssrc of the user speaking until after the first few packets,
+ so we queue up unknown SSRCs until they become known, then empty the queue.
+ */
+ this.queues = new Map();
+ this.pcmStreams = new Map();
+ this.opusStreams = new Map();
+ /**
+ * The VoiceConnection that instantiated this
+ * @type {VoiceConnection}
+ */
+ this.connection = connection;
+ this.connection.udp.udpSocket.on('message', msg => {
+ const ssrc = +msg.readUInt32BE(8).toString(10);
+ const user = this.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);
+ }
+ });
+ }
+
+ /**
+ * 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.connection.manager.client.resolver.resolveUser(user);
+ if (!user) {
+ throw new Error('invalid user object supplied');
+ }
+ 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.connection.manager.client.resolver.resolveUser(user);
+ if (!user) throw new Error('invalid user object supplied');
+ if (this.pcmStreams.get(user.id)) throw new Error('there is already an existing stream for that user!');
+ const stream = new Readable();
+ this.pcmStreams.set(user.id, stream);
+ return stream;
+ }
+
+ handlePacket(msg, user) {
+ msg.copy(nonce, 0, 0, 12);
+ let data = NaCl.secretbox.open(msg.slice(12), nonce, this.connection.data.secret);
+ if (!data) {
+ /**
+ * Emitted whenever a voice packet cannot be decrypted
+ * @event VoiceReceiver#warn
+ * @param {string} message The warning message
+ */
+ this.emit('warn', 'failed to decrypt voice packet');
+ return;
+ }
+ data = new Buffer(data);
+ if (this.opusStreams.get(user.id)) this.opusStreams.get(user.id)._push(data);
+ /**
+ * Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM).
+ * @event VoiceReceiver#opus
+ * @param {User} user The user that is sending the buffer (is speaking)
+ * @param {Buffer} buffer The opus buffer
+ */
+ this.emit('opus', user, data);
+ if (this.listenerCount('pcm') > 0 || this.pcmStreams.size > 0) {
+ /**
+ * Emits decoded voice data when it's received. For performance reasons, the decoding will only
+ * happen if there is at least one `pcm` listener on this receiver.
+ * @event VoiceReceiver#pcm
+ * @param {User} user The user that is sending the buffer (is speaking)
+ * @param {Buffer} buffer The decoded buffer
+ */
+ const pcm = this.connection.player.opusEncoder.decode(data);
+ if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm);
+ this.emit('pcm', user, pcm);
+ }
+ }
+}
+
+module.exports = VoiceReceiver;
diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js
new file mode 100644
index 000000000..3a0233339
--- /dev/null
+++ b/src/client/websocket/WebSocketManager.js
@@ -0,0 +1,245 @@
+const WebSocket = require('ws');
+const Constants = require('../../util/Constants');
+const zlib = require('zlib');
+const PacketManager = require('./packets/WebSocketPacketManager');
+
+/**
+ * The WebSocket Manager of the Client
+ * @private
+ */
+class WebSocketManager {
+ constructor(client) {
+ /**
+ * The Client that instantiated this WebSocketManager
+ * @type {Client}
+ */
+ this.client = client;
+ /**
+ * A WebSocket Packet manager, it handles all the messages
+ * @type {PacketManager}
+ */
+ this.packetManager = new PacketManager(this);
+ /**
+ * The status of the WebSocketManager, a type of Constants.Status. It defaults to IDLE.
+ * @type {number}
+ */
+ this.status = Constants.Status.IDLE;
+ /**
+ * The session ID of the connection, null if not yet available.
+ * @type {?string}
+ */
+ this.sessionID = null;
+ /**
+ * The packet count of the client, null if not yet available.
+ * @type {?number}
+ */
+ this.sequence = -1;
+ /**
+ * The gateway address for this WebSocket connection, null if not yet available.
+ * @type {?string}
+ */
+ this.gateway = null;
+ /**
+ * Whether READY was emitted normally (all packets received) or not
+ * @type {boolean}
+ */
+ this.normalReady = false;
+ /**
+ * The WebSocket connection to the gateway
+ * @type {?WebSocket}
+ */
+ this.ws = null;
+ }
+
+ /**
+ * Connects the client to a given gateway
+ * @param {string} gateway The gateway to connect to
+ */
+ connect(gateway) {
+ this.client.emit('debug', `connecting to gateway ${gateway}`);
+ this.normalReady = false;
+ this.status = Constants.Status.CONNECTING;
+ this.ws = new WebSocket(gateway);
+ this.ws.onopen = () => this.eventOpen();
+ this.ws.onclose = (d) => this.eventClose(d);
+ this.ws.onmessage = (e) => this.eventMessage(e);
+ this.ws.onerror = (e) => this.eventError(e);
+ this._queue = [];
+ this._remaining = 3;
+ }
+
+ /**
+ * Sends a packet to the gateway
+ * @param {Object} data An object that can be JSON stringified
+ * @param {boolean} force Whether or not to send the packet immediately
+ */
+ send(data, force = false) {
+ if (force) {
+ this.ws.send(JSON.stringify(data));
+ return;
+ }
+ this._queue.push(JSON.stringify(data));
+ this.doQueue();
+ }
+
+ destroy() {
+ this.ws.close(1000);
+ this._queue = [];
+ this.status = Constants.Status.IDLE;
+ }
+
+ doQueue() {
+ const item = this._queue[0];
+ if (this.ws.readyState === WebSocket.OPEN && item) {
+ if (this._remaining === 0) {
+ this.client.setTimeout(() => {
+ this.doQueue();
+ }, 1000);
+ return;
+ }
+ this._remaining--;
+ this.ws.send(item);
+ this._queue.shift();
+ this.doQueue();
+ this.client.setTimeout(() => this._remaining++, 1000);
+ }
+ }
+
+ /**
+ * Run whenever the gateway connections opens up
+ */
+ eventOpen() {
+ this.client.emit('debug', 'connection to gateway opened');
+ if (this.reconnecting) this._sendResume();
+ else this._sendNewIdentify();
+ }
+
+ /**
+ * Sends a gateway resume packet, in cases of unexpected disconnections.
+ */
+ _sendResume() {
+ const payload = {
+ token: this.client.token,
+ session_id: this.sessionID,
+ seq: this.sequence,
+ };
+
+ this.send({
+ op: Constants.OPCodes.RESUME,
+ d: payload,
+ });
+ }
+
+ /**
+ * Sends a new identification packet, in cases of new connections or failed reconnections.
+ */
+ _sendNewIdentify() {
+ this.reconnecting = false;
+ const payload = this.client.options.ws;
+ payload.token = this.client.token;
+ if (this.client.options.shard_count > 0) {
+ payload.shard = [Number(this.client.options.shard_id), Number(this.client.options.shard_count)];
+ }
+ this.send({
+ op: Constants.OPCodes.IDENTIFY,
+ d: payload,
+ });
+ }
+
+ /**
+ * Run whenever the connection to the gateway is closed, it will try to reconnect the client.
+ * @param {Object} event The received websocket data
+ */
+ eventClose(event) {
+ if (event.code === 4004) throw new Error(Constants.Errors.BAD_LOGIN);
+ if (!this.reconnecting && event.code !== 1000) this.tryReconnect();
+ }
+
+ /**
+ * Run whenever a message is received from the WebSocket. Returns `true` if the message
+ * was handled properly.
+ * @param {Object} event The received websocket data
+ * @returns {boolean}
+ */
+ eventMessage(event) {
+ let packet;
+ try {
+ if (event.binary) event.data = zlib.inflateSync(event.data).toString();
+ packet = JSON.parse(event.data);
+ } catch (e) {
+ return this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE));
+ }
+
+ this.client.emit('raw', packet);
+
+ if (packet.op === 10) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
+ return this.packetManager.handle(packet);
+ }
+
+ /**
+ * Run whenever an error occurs with the WebSocket connection. Tries to reconnect
+ * @param {Error} err The encountered error
+ */
+ eventError(err) {
+ /**
+ * Emitted whenever the Client encounters a serious connection error
+ * @event Client#error
+ * @param {Error} error The encountered error
+ */
+ this.client.emit('error', err);
+ this.tryReconnect();
+ }
+
+ _emitReady(normal = true) {
+ /**
+ * Emitted when the Client becomes ready to start working
+ * @event Client#ready
+ */
+ this.status = Constants.Status.READY;
+ this.client.emit(Constants.Events.READY);
+ this.packetManager.handleQueue();
+ this.normalReady = normal;
+ }
+
+ /**
+ * Runs on new packets before `READY` to see if the Client is ready yet, if it is prepares
+ * the `READY` event.
+ */
+ checkIfReady() {
+ if (this.status !== Constants.Status.READY && this.status !== Constants.Status.NEARLY) {
+ let unavailableCount = 0;
+ for (const guildID of this.client.guilds.keys()) {
+ unavailableCount += this.client.guilds.get(guildID).available ? 0 : 1;
+ }
+ if (unavailableCount === 0) {
+ this.status = Constants.Status.NEARLY;
+ if (this.client.options.fetch_all_members) {
+ const promises = this.client.guilds.array().map(g => g.fetchMembers());
+ Promise.all(promises).then(() => this._emitReady()).catch(e => {
+ this.client.emit('warn', `error on pre-ready guild member fetching - ${e}`);
+ this._emitReady();
+ });
+ return;
+ }
+ this._emitReady();
+ }
+ }
+ }
+
+ /**
+ * Tries to reconnect the client, changing the status to Constants.Status.RECONNECTING.
+ */
+ tryReconnect() {
+ this.status = Constants.Status.RECONNECTING;
+ this.ws.close();
+ this.packetManager.handleQueue();
+ /**
+ * Emitted when the Client tries to reconnect after being disconnected
+ * @event Client#reconnecting
+ */
+ this.client.emit(Constants.Events.RECONNECTING);
+ this.connect(this.client.ws.gateway);
+ }
+}
+
+module.exports = WebSocketManager;
diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js
new file mode 100644
index 000000000..bf81fd538
--- /dev/null
+++ b/src/client/websocket/packets/WebSocketPacketManager.js
@@ -0,0 +1,98 @@
+const Constants = require('../../../util/Constants');
+
+const BeforeReadyWhitelist = [
+ Constants.WSEvents.READY,
+ Constants.WSEvents.GUILD_CREATE,
+ Constants.WSEvents.GUILD_DELETE,
+ Constants.WSEvents.GUILD_MEMBERS_CHUNK,
+ Constants.WSEvents.GUILD_MEMBER_ADD,
+ Constants.WSEvents.GUILD_MEMBER_REMOVE,
+];
+
+class WebSocketPacketManager {
+ constructor(websocketManager) {
+ this.ws = websocketManager;
+ this.handlers = {};
+ this.queue = [];
+
+ this.register(Constants.WSEvents.READY, 'Ready');
+ this.register(Constants.WSEvents.GUILD_CREATE, 'GuildCreate');
+ this.register(Constants.WSEvents.GUILD_DELETE, 'GuildDelete');
+ this.register(Constants.WSEvents.GUILD_UPDATE, 'GuildUpdate');
+ this.register(Constants.WSEvents.GUILD_BAN_ADD, 'GuildBanAdd');
+ this.register(Constants.WSEvents.GUILD_BAN_REMOVE, 'GuildBanRemove');
+ this.register(Constants.WSEvents.GUILD_MEMBER_ADD, 'GuildMemberAdd');
+ this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, 'GuildMemberRemove');
+ this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, 'GuildMemberUpdate');
+ this.register(Constants.WSEvents.GUILD_ROLE_CREATE, 'GuildRoleCreate');
+ this.register(Constants.WSEvents.GUILD_ROLE_DELETE, 'GuildRoleDelete');
+ this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, 'GuildRoleUpdate');
+ this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, 'GuildMembersChunk');
+ this.register(Constants.WSEvents.CHANNEL_CREATE, 'ChannelCreate');
+ this.register(Constants.WSEvents.CHANNEL_DELETE, 'ChannelDelete');
+ this.register(Constants.WSEvents.CHANNEL_UPDATE, 'ChannelUpdate');
+ this.register(Constants.WSEvents.PRESENCE_UPDATE, 'PresenceUpdate');
+ this.register(Constants.WSEvents.USER_UPDATE, 'UserUpdate');
+ this.register(Constants.WSEvents.VOICE_STATE_UPDATE, 'VoiceStateUpdate');
+ this.register(Constants.WSEvents.TYPING_START, 'TypingStart');
+ this.register(Constants.WSEvents.MESSAGE_CREATE, 'MessageCreate');
+ this.register(Constants.WSEvents.MESSAGE_DELETE, 'MessageDelete');
+ this.register(Constants.WSEvents.MESSAGE_UPDATE, 'MessageUpdate');
+ this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, 'VoiceServerUpdate');
+ this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
+ this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
+ this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
+ }
+
+ get client() {
+ return this.ws.client;
+ }
+
+ register(event, handle) {
+ const Handler = require(`./handlers/${handle}`);
+ this.handlers[event] = new Handler(this);
+ }
+
+ handleQueue() {
+ this.queue.forEach((element, index) => {
+ this.handle(this.queue[index]);
+ this.queue.splice(index, 1);
+ });
+ }
+
+ setSequence(s) {
+ if (s && s > this.ws.sequence) this.ws.sequence = s;
+ }
+
+ handle(packet) {
+ if (packet.op === Constants.OPCodes.RECONNECT) {
+ this.setSequence(packet.s);
+ this.ws.tryReconnect();
+ return false;
+ }
+
+ if (packet.op === Constants.OPCodes.INVALID_SESSION) {
+ this.ws._sendNewIdentify();
+ return false;
+ }
+
+ if (this.ws.reconnecting) {
+ this.ws.reconnecting = false;
+ this.ws.checkIfReady();
+ }
+
+ this.setSequence(packet.s);
+
+ if (this.ws.status !== Constants.Status.READY) {
+ if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
+ this.queue.push(packet);
+ return false;
+ }
+ }
+
+ if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet);
+ return false;
+ }
+}
+
+module.exports = WebSocketPacketManager;
diff --git a/src/client/websocket/packets/handlers/AbstractHandler.js b/src/client/websocket/packets/handlers/AbstractHandler.js
new file mode 100644
index 000000000..c1c2a5a2d
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/ChannelCreate.js b/src/client/websocket/packets/handlers/ChannelCreate.js
new file mode 100644
index 000000000..166371642
--- /dev/null
+++ b/src/client/websocket/packets/handlers/ChannelCreate.js
@@ -0,0 +1,20 @@
+const AbstractHandler = require('./AbstractHandler');
+
+const Constants = require('../../../../util/Constants');
+
+class ChannelCreateHandler extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ const response = client.actions.ChannelCreate.handle(data);
+ if (response.channel) client.emit(Constants.Events.CHANNEL_CREATE, response.channel);
+ }
+}
+
+/**
+ * Emitted whenever a Channel is created.
+ * @event Client#channelCreate
+ * @param {Channel} channel The channel that was created
+ */
+
+module.exports = ChannelCreateHandler;
diff --git a/src/client/websocket/packets/handlers/ChannelDelete.js b/src/client/websocket/packets/handlers/ChannelDelete.js
new file mode 100644
index 000000000..ec49df0d5
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js
new file mode 100644
index 000000000..65d862959
--- /dev/null
+++ b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js
@@ -0,0 +1,31 @@
+const AbstractHandler = require('./AbstractHandler');
+const Constants = require('../../../../util/Constants');
+
+/*
+{ t: 'CHANNEL_PINS_UPDATE',
+ s: 666,
+ op: 0,
+ d:
+ { last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00',
+ channel_id: '314866471639044027' } }
+*/
+
+class ChannelPinsUpdate extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ const channel = client.channels.get(data.channel_id);
+ const time = new Date(data.last_pin_timestamp);
+ if (channel && time) client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time);
+ }
+}
+
+/**
+ * Emitted whenever the pins of a Channel are updated. Due to the nature of the WebSocket event, not much information
+ * can be provided easily here - you need to manually check the pins yourself.
+ * @event Client#channelPinsUpdate
+ * @param {Channel} channel The channel that the pins update occured in
+ * @param {Date} time The time of the pins update
+ */
+
+module.exports = ChannelPinsUpdate;
diff --git a/src/client/websocket/packets/handlers/ChannelUpdate.js b/src/client/websocket/packets/handlers/ChannelUpdate.js
new file mode 100644
index 000000000..fa535b143
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildBanAdd.js b/src/client/websocket/packets/handlers/GuildBanAdd.js
new file mode 100644
index 000000000..60ce72d0a
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildBanRemove.js b/src/client/websocket/packets/handlers/GuildBanRemove.js
new file mode 100644
index 000000000..c4edbdeb6
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildCreate.js b/src/client/websocket/packets/handlers/GuildCreate.js
new file mode 100644
index 000000000..c7fbd7e7c
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildDelete.js b/src/client/websocket/packets/handlers/GuildDelete.js
new file mode 100644
index 000000000..9b74d56f6
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildMemberAdd.js b/src/client/websocket/packets/handlers/GuildMemberAdd.js
new file mode 100644
index 000000000..d4d122f70
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildMemberRemove.js b/src/client/websocket/packets/handlers/GuildMemberRemove.js
new file mode 100644
index 000000000..6ec1bfe64
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildMemberUpdate.js b/src/client/websocket/packets/handlers/GuildMemberUpdate.js
new file mode 100644
index 000000000..94ac71f47
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js
new file mode 100644
index 000000000..532d73bed
--- /dev/null
+++ b/src/client/websocket/packets/handlers/GuildMembersChunk.js
@@ -0,0 +1,29 @@
+// ##untested##
+
+const AbstractHandler = require('./AbstractHandler');
+const Constants = require('../../../../util/Constants');
+
+class GuildMembersChunkHandler extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ const guild = client.guilds.get(data.guild_id);
+ const members = [];
+
+ if (guild) {
+ for (const member of data.members) members.push(guild._addMember(member, true));
+ }
+
+ guild._checkChunks();
+ client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, guild, members);
+ }
+}
+
+/**
+ * Emitted whenever a chunk of Guild members is received
+ * @event Client#guildMembersChunk
+ * @param {Guild} guild The guild that the chunks relate to
+ * @param {GuildMember[]} members The members in the chunk
+ */
+
+module.exports = GuildMembersChunkHandler;
diff --git a/src/client/websocket/packets/handlers/GuildRoleCreate.js b/src/client/websocket/packets/handlers/GuildRoleCreate.js
new file mode 100644
index 000000000..8581d53f6
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildRoleDelete.js b/src/client/websocket/packets/handlers/GuildRoleDelete.js
new file mode 100644
index 000000000..63439b0fe
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildRoleUpdate.js b/src/client/websocket/packets/handlers/GuildRoleUpdate.js
new file mode 100644
index 000000000..6fbdc109a
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildSync.js b/src/client/websocket/packets/handlers/GuildSync.js
new file mode 100644
index 000000000..0b9f5aa86
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/GuildUpdate.js b/src/client/websocket/packets/handlers/GuildUpdate.js
new file mode 100644
index 000000000..70eff52c4
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/MessageCreate.js b/src/client/websocket/packets/handlers/MessageCreate.js
new file mode 100644
index 000000000..058dc854e
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/MessageDelete.js b/src/client/websocket/packets/handlers/MessageDelete.js
new file mode 100644
index 000000000..b06ce9887
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/MessageDeleteBulk.js b/src/client/websocket/packets/handlers/MessageDeleteBulk.js
new file mode 100644
index 000000000..6cd36484e
--- /dev/null
+++ b/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} messages The deleted messages, mapped by their ID
+ */
+
+module.exports = MessageDeleteBulkHandler;
diff --git a/src/client/websocket/packets/handlers/MessageUpdate.js b/src/client/websocket/packets/handlers/MessageUpdate.js
new file mode 100644
index 000000000..527632d4e
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js
new file mode 100644
index 000000000..3ba602cda
--- /dev/null
+++ b/src/client/websocket/packets/handlers/PresenceUpdate.js
@@ -0,0 +1,71 @@
+const AbstractHandler = require('./AbstractHandler');
+const Constants = require('../../../../util/Constants');
+const cloneObject = require('../../../../util/CloneObject');
+
+class PresenceUpdateHandler extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ let user = client.users.get(data.user.id);
+ const guild = client.guilds.get(data.guild_id);
+
+ // step 1
+ if (!user) {
+ if (data.user.username) {
+ user = client.dataManager.newUser(data.user);
+ } else {
+ return;
+ }
+ }
+
+ if (guild) {
+ const memberInGuild = guild.members.get(user.id);
+ if (!memberInGuild) {
+ const member = guild._addMember({
+ user,
+ roles: data.roles,
+ deaf: false,
+ mute: false,
+ }, true);
+ client.emit(Constants.Events.GUILD_MEMBER_AVAILABLE, guild, member);
+ }
+ }
+
+ data.user.username = data.user.username || user.username;
+ data.user.id = data.user.id || user.id;
+ data.user.discriminator = data.user.discriminator || user.discriminator;
+ // comment out avatar patching as it causes bugs (see #297)
+ // data.user.avatar = data.user.avatar || user.avatar;
+ data.user.status = data.status || user.status;
+ data.user.game = data.game;
+
+ const same = data.user.username === user.username &&
+ data.user.id === user.id &&
+ data.user.discriminator === user.discriminator &&
+ data.user.avatar === user.avatar &&
+ data.user.status === user.status &&
+ JSON.stringify(data.user.game) === JSON.stringify(user.game);
+
+ if (!same) {
+ const oldUser = cloneObject(user);
+ user.setup(data.user);
+ client.emit(Constants.Events.PRESENCE_UPDATE, oldUser, user);
+ }
+ }
+}
+
+/**
+ * Emitted whenever a user changes one of their details or starts/stop playing a game
+ * @event Client#presenceUpdate
+ * @param {User} oldUser The user before the presence update
+ * @param {User} newUser The user after the presence update
+ */
+
+/**
+ * Emitted whenever a member becomes available in a large Guild
+ * @event Client#guildMemberAvailable
+ * @param {Guild} guild The guild that the member became available in
+ * @param {GuildMember} member The member that became available
+ */
+
+module.exports = PresenceUpdateHandler;
diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js
new file mode 100644
index 000000000..0c3804e27
--- /dev/null
+++ b/src/client/websocket/packets/handlers/Ready.js
@@ -0,0 +1,31 @@
+const AbstractHandler = require('./AbstractHandler');
+
+const getStructure = name => require(`../../../../structures/${name}`);
+const ClientUser = getStructure('ClientUser');
+
+class ReadyHandler extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+
+ const clientUser = new ClientUser(client, data.user);
+ client.user = clientUser;
+ client.readyTime = new Date();
+ client.users.set(clientUser.id, clientUser);
+
+ for (const guild of data.guilds) client.dataManager.newGuild(guild);
+ for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
+
+ if (!client.user.bot) client.setInterval(client.syncGuilds.bind(client), 30000);
+ client.once('ready', client.syncGuilds.bind(client));
+
+ client.setTimeout(() => {
+ if (!client.ws.normalReady) client.ws._emitReady(false);
+ }, 1200 * data.guilds.length);
+
+ this.packetManager.ws.sessionID = data.session_id;
+ this.packetManager.ws.checkIfReady();
+ }
+}
+
+module.exports = ReadyHandler;
diff --git a/src/client/websocket/packets/handlers/TypingStart.js b/src/client/websocket/packets/handlers/TypingStart.js
new file mode 100644
index 000000000..faaa49865
--- /dev/null
+++ b/src/client/websocket/packets/handlers/TypingStart.js
@@ -0,0 +1,64 @@
+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);
+
+ function tooLate() {
+ return client.setTimeout(() => {
+ client.emit(Constants.Events.TYPING_STOP, channel, user, channel.typingMap[user.id]);
+ delete channel.typingMap[user.id];
+ }, 6000);
+ }
+
+ if (channel && user) {
+ if (channel.typingMap[user.id]) {
+ // already typing, renew
+ const mapping = channel.typingMap[user.id];
+ mapping.lastTimestamp = timestamp;
+ mapping.resetTimeout(tooLate());
+ } else {
+ channel.typingMap[user.id] = new TypingData(timestamp, timestamp, tooLate());
+ client.emit(Constants.Events.TYPING_START, channel, user);
+ }
+ }
+ }
+}
+
+class TypingData {
+ constructor(since, lastTimestamp, _timeout) {
+ this.since = since;
+ this.lastTimestamp = lastTimestamp;
+ this._timeout = _timeout;
+ }
+
+ resetTimeout(_timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = _timeout;
+ }
+
+ get elapsedTime() {
+ return Date.now() - this.since;
+ }
+}
+
+/**
+ * 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/src/client/websocket/packets/handlers/UserUpdate.js b/src/client/websocket/packets/handlers/UserUpdate.js
new file mode 100644
index 000000000..bc34f347d
--- /dev/null
+++ b/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/src/client/websocket/packets/handlers/VoiceServerUpdate.js b/src/client/websocket/packets/handlers/VoiceServerUpdate.js
new file mode 100644
index 000000000..146a3efc3
--- /dev/null
+++ b/src/client/websocket/packets/handlers/VoiceServerUpdate.js
@@ -0,0 +1,21 @@
+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;
+ if (client.voice.pending.has(data.guild_id)) {
+ client.voice._receivedVoiceServer(data.guild_id, data.token, data.endpoint);
+ }
+ }
+}
+
+module.exports = VoiceServerUpdate;
diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js
new file mode 100644
index 000000000..639da210b
--- /dev/null
+++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js
@@ -0,0 +1,49 @@
+const AbstractHandler = require('./AbstractHandler');
+
+const Constants = require('../../../../util/Constants');
+const cloneObject = require('../../../../util/CloneObject');
+
+class VoiceStateUpdateHandler extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+
+ const guild = client.guilds.get(data.guild_id);
+ if (guild) {
+ const member = guild.members.get(data.user_id);
+ if (member) {
+ const oldVoiceChannelMember = cloneObject(member);
+ if (member.voiceChannel && member.voiceChannel.id !== data.channel_id) {
+ member.voiceChannel.members.delete(oldVoiceChannelMember.id);
+ }
+
+ // if the member left the voice channel, unset their speaking property
+ if (!data.channel_id) member.speaking = null;
+
+ if (client.voice.pending.has(guild.id) && member.user.id === client.user.id && data.channel_id) {
+ client.voice._receivedVoiceStateUpdate(data.guild_id, data.session_id);
+ }
+
+ const newChannel = client.channels.get(data.channel_id);
+ if (newChannel) newChannel.members.set(member.user.id, member);
+
+ member.serverMute = data.mute;
+ member.serverDeaf = data.deaf;
+ member.selfMute = data.self_mute;
+ member.selfDeaf = data.self_deaf;
+ member.voiceSessionID = data.session_id;
+ member.voiceChannelID = data.channel_id;
+ client.emit(Constants.Events.VOICE_STATE_UPDATE, oldVoiceChannelMember, member);
+ }
+ }
+ }
+}
+
+/**
+ * Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
+ * @event Client#voiceStateUpdate
+ * @param {GuildMember} oldMember The member before the voice state update
+ * @param {GuildMember} newMember The member after the voice state update
+ */
+
+module.exports = VoiceStateUpdateHandler;
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 000000000..7804bd0e7
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,5 @@
+const Client = require('./client/Client');
+const ShardingManager = require('./sharding/ShardingManager');
+
+exports.Client = Client;
+exports.ShardingManager = ShardingManager;
diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js
new file mode 100644
index 000000000..08c37bab5
--- /dev/null
+++ b/src/sharding/Shard.js
@@ -0,0 +1,27 @@
+const childProcess = require('child_process');
+const path = require('path');
+
+/**
+ * Represents a Shard spawned by the ShardingManager.
+ */
+class Shard {
+ constructor(manager, id) {
+ /**
+ * The manager of the spawned shard
+ * @type {ShardingManager}
+ */
+ this.manager = manager;
+ /**
+ * The shard id
+ * @type {number}
+ */
+ this.id = id;
+ /**
+ * The process of the shard
+ * @type {process}
+ */
+ this.process = childProcess.fork(path.resolve(this.manager.file), [id, this.manager.shards.size]);
+ }
+}
+
+module.exports = Shard;
diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js
new file mode 100644
index 000000000..4e49e7b4f
--- /dev/null
+++ b/src/sharding/ShardingManager.js
@@ -0,0 +1,56 @@
+const path = require('path');
+const EventEmitter = require('events').EventEmitter;
+const Collection = require('../util/Collection');
+const Shard = require('./Shard');
+
+/**
+ * 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.
+ * The Sharding Manager is still experimental
+ * @extends {EventEmitter}
+ */
+class ShardingManager extends EventEmitter {
+ /**
+ * Creates an instance of ShardingManager.
+ * @param {string} file the path to your file
+ * @param {number} totalShards the number of shards you would like to spawn
+ */
+ constructor(file, totalShards) {
+ super();
+ this.file = file;
+ if (!path.isAbsolute(file)) {
+ this.file = path.resolve(`${process.cwd()}${file}`);
+ }
+ /**
+ * The amount of shards that this manager is going to spawn
+ * @type {number}
+ */
+ this.totalShards = totalShards;
+ /**
+ * A collection of shards that this manager has spawned.
+ * @type {Collection}
+ */
+ this.shards = new Collection();
+ this.waiting = new Collection();
+ }
+
+ createShard() {
+ const id = this.shards.size;
+ const shard = new Shard(this, id);
+ this.shards.set(id, shard);
+ this.emit('launch', id, shard);
+ }
+
+ spawn(amount) {
+ this.totalShards = amount;
+ this.createShard();
+ const interval = setInterval(() => {
+ if (this.shards.size === this.totalShards) {
+ return clearInterval(interval);
+ }
+ return this.createShard();
+ }, 5500);
+ }
+}
+
+module.exports = ShardingManager;
diff --git a/src/structures/Channel.js b/src/structures/Channel.js
new file mode 100644
index 000000000..ae36545ac
--- /dev/null
+++ b/src/structures/Channel.js
@@ -0,0 +1,48 @@
+/**
+ * Represents any Channel on Discord
+ */
+class Channel {
+ constructor(client, data, guild) {
+ /**
+ * The client that instantiated the Channel
+ * @type {Client}
+ */
+ this.client = client;
+ this.typingMap = {};
+ this.typingTimeouts = [];
+ if (guild) this.guild = guild;
+ /**
+ * The type of the channel, either:
+ * * `dm` - a DM channel
+ * * `group` - a Group DM channel
+ * * `text` - a guild text channel
+ * * `voice` - a guild voice channel
+ * @type {string}
+ */
+ this.type = null;
+ if (data) this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The unique ID of the channel
+ * @type {string}
+ */
+ this.id = data.id;
+ }
+
+ /**
+ * Deletes the channel
+ * @returns {Promise}
+ * @example
+ * // delete the channel
+ * channel.delete()
+ * .then() // success
+ * .catch(console.log); // log error
+ */
+ delete() {
+ return this.client.rest.methods.deleteChannel(this);
+ }
+}
+
+module.exports = Channel;
diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js
new file mode 100644
index 000000000..80c8988e4
--- /dev/null
+++ b/src/structures/ClientUser.js
@@ -0,0 +1,136 @@
+const User = require('./User');
+
+/**
+ * Represents the logged in client's Discord User
+ * @extends {User}
+ */
+class ClientUser extends User {
+ setup(data) {
+ super.setup(data);
+ /**
+ * Whether or not this account has been verified
+ * @type {boolean}
+ */
+ this.verified = data.verified;
+ /**
+ * The email of this account
+ * @type {string}
+ */
+ this.email = data.email;
+
+ this._typing = new Map();
+ }
+
+ /**
+ * Set the username of the logged in Client.
+ * Changing usernames in Discord is heavily rate limited, with only 2 requests
+ * every hour. Use this sparingly!
+ * @param {string} username The new username
+ * @returns {Promise}
+ * @example
+ * // set username
+ * client.user.setUsername('discordjs')
+ * .then(user => console.log(`My new username is ${user.username}`))
+ * .catch(console.log);
+ */
+ setUsername(username) {
+ return this.client.rest.methods.updateCurrentUser({ username });
+ }
+
+ /**
+ * If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the
+ * email here.
+ * @param {string} email The new email
+ * @returns {Promise}
+ * @example
+ * // set email
+ * client.user.setEmail('bob@gmail.com')
+ * .then(user => console.log(`My new email is ${user.email}`))
+ * .catch(console.log);
+ */
+ setEmail(email) {
+ return this.client.rest.methods.updateCurrentUser({ email });
+ }
+
+ /**
+ * If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the
+ * password here.
+ * @param {string} password The new password
+ * @returns {Promise}
+ * @example
+ * // set password
+ * client.user.setPassword('password')
+ * .then(user => console.log('New password set!'))
+ * .catch(console.log);
+ */
+ setPassword(password) {
+ return this.client.rest.methods.updateCurrentUser({ password });
+ }
+
+ /**
+ * Set the avatar of the logged in Client.
+ * @param {Base64Resolvable} avatar The new avatar
+ * @returns {Promise}
+ * @example
+ * // set avatar
+ * client.user.setAvatar(fs.readFileSync('./avatar.png'))
+ * .then(user => console.log(`New avatar set!`))
+ * .catch(console.log);
+ */
+ setAvatar(avatar) {
+ return this.client.rest.methods.updateCurrentUser({ avatar });
+ }
+
+ /**
+ * Set the status and playing game of the logged in client.
+ * @param {string} [status] The status, can be `online` or `idle`
+ * @param {string|Object} [game] The game that is being played
+ * @returns {Promise}
+ * @example
+ * // set status
+ * client.user.setStatus('status', 'game')
+ * .then(user => console.log('Changed status!'))
+ * .catch(console.log);
+ */
+ setStatus(status, game) {
+ return new Promise(resolve => {
+ if (status === 'online' || status === 'here' || status === 'available') {
+ this.idleStatus = null;
+ } else if (status === 'idle' || status === 'away') {
+ this.idleStatus = Date.now();
+ } else {
+ this.idleStatus = this.idleStatus || null;
+ }
+
+ if (typeof game === 'string' && !game.length) game = null;
+
+ if (game === null) {
+ this.userGame = null;
+ } else if (!game) {
+ this.userGame = this.userGame || null;
+ } else if (typeof game === 'string') {
+ this.userGame = { name: game };
+ } else {
+ this.userGame = game;
+ }
+
+ this.client.ws.send({
+ op: 3,
+ d: {
+ idle_since: this.idleStatus,
+ game: this.userGame,
+ },
+ });
+
+ this.status = this.idleStatus ? 'idle' : 'online';
+ this.game = this.userGame;
+ resolve(this);
+ });
+ }
+
+ edit(data) {
+ return this.client.rest.methods.updateCurrentUser(data);
+ }
+}
+
+module.exports = ClientUser;
diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js
new file mode 100644
index 000000000..c590bb4c9
--- /dev/null
+++ b/src/structures/DMChannel.js
@@ -0,0 +1,97 @@
+const User = require('./User');
+const Channel = require('./Channel');
+const TextBasedChannel = require('./interface/TextBasedChannel');
+const Collection = require('../util/Collection');
+
+/**
+ * Represents a Direct Message Channel between two users.
+ * @extends {Channel}
+ * @implements {TextBasedChannel}
+ */
+class DMChannel extends Channel {
+ constructor(client, data) {
+ super(client, data);
+ this.messages = new Collection();
+ }
+
+ setup(data) {
+ super.setup(data);
+ const recipient = this.client.users.get(data.recipients[0].id) || new User(this.client, data.recipients[0]);
+ /**
+ * The recipient on the other end of the DM
+ * @type {User}
+ */
+ this.recipient = recipient;
+ /**
+ * The ID of the last sent message, if available
+ * @type {?string}
+ */
+ this.lastMessageID = data.last_message_id;
+ this.type = 'dm';
+ }
+
+ /**
+ * 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();
+ }
+
+ sendMessage() {
+ return;
+ }
+
+ sendTTSMessage() {
+ return;
+ }
+
+ sendFile() {
+ return;
+ }
+
+ _cacheMessage() {
+ return;
+ }
+
+ fetchMessages() {
+ return;
+ }
+
+ bulkDelete() {
+ return;
+ }
+
+ startTyping() {
+ return;
+ }
+
+ stopTyping() {
+ return;
+ }
+
+ get typing() {
+ return;
+ }
+
+ get typingCount() {
+ return;
+ }
+
+ fetchPinnedMessages() {
+ return;
+ }
+
+ createCollector() {
+ return;
+ }
+
+ awaitMessages() {
+ return;
+ }
+}
+
+TextBasedChannel.applyToClass(DMChannel, true);
+
+module.exports = DMChannel;
diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js
new file mode 100644
index 000000000..28c93bb98
--- /dev/null
+++ b/src/structures/Emoji.js
@@ -0,0 +1,81 @@
+const Constants = require('../util/Constants');
+const Collection = require('../util/Collection');
+
+/**
+ * Represents a Custom Emoji
+ */
+class Emoji {
+ constructor(guild, data) {
+ /**
+ * The Client that instantiated this object
+ * @type {Client}
+ */
+ this.client = guild.client;
+ /**
+ * The Guild this emoji is part of
+ * @type {Guild}
+ */
+ this.guild = guild;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The ID of the Emoji
+ * @type {string}
+ */
+ this.id = data.id;
+ /**
+ * The name of the Emoji
+ * @type {string}
+ */
+ this.name = data.name;
+ this.roleIDS = data.roles;
+ /**
+ * 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;
+ }
+
+ /**
+ * A collection of roles this emoji is active for (empty if all), mapped by role ID.
+ * @type {Collection}
+ * @readonly
+ */
+ get roles() {
+ const roles = new Collection();
+ for (const role of this.roleIDS) {
+ if (this.guild.roles.get(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}/emojis/${this.id}.png`;
+ }
+
+ /**
+ * 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.array()[0];
+ * msg.reply(`Hello! ${emoji}`);
+ */
+ toString() {
+ return this.requiresColons ? `<:${this.name}:${this.id}>` : this.name;
+ }
+}
+
+module.exports = Emoji;
diff --git a/src/structures/EvaluatedPermissions.js b/src/structures/EvaluatedPermissions.js
new file mode 100644
index 000000000..0c66cd437
--- /dev/null
+++ b/src/structures/EvaluatedPermissions.js
@@ -0,0 +1,47 @@
+const Constants = require('../util/Constants');
+
+/**
+ * The final evaluated permissions for a member in a channel
+ */
+class EvaluatedPermissions {
+ constructor(member, permissions) {
+ /**
+ * The member this permissions refer to
+ * @type {GuildMember}
+ */
+ this.member = member;
+ /**
+ * A number representing the packed permissions.
+ * @private
+ * @type {number}
+ */
+ this.permissions = permissions;
+ }
+
+ /**
+ * Get an object mapping permission name, e.g. `READ_MESSAGES` to a boolean - whether the user
+ * can perform this or not.
+ * @returns {Object}
+ */
+ serialize() {
+ const serializedPermissions = {};
+ for (const permissionName in Constants.PermissionFlags) {
+ serializedPermissions[permissionName] = this.hasPermission(permissionName);
+ }
+ return serializedPermissions;
+ }
+
+ /**
+ * Checks whether the user has a certain permission, e.g. `READ_MESSAGES`.
+ * @param {PermissionResolvable} permission The permission to check for
+ * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permission
+ * @returns {boolean}
+ */
+ hasPermission(permission, explicit = false) {
+ permission = this.member.client.resolver.resolvePermission(permission);
+ if (!explicit && (this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true;
+ return (this.permissions & permission) > 0;
+ }
+}
+
+module.exports = EvaluatedPermissions;
diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js
new file mode 100644
index 000000000..1c788fc0e
--- /dev/null
+++ b/src/structures/GroupDMChannel.js
@@ -0,0 +1,156 @@
+const Channel = require('./Channel');
+const TextBasedChannel = require('./interface/TextBasedChannel');
+const Collection = require('../util/Collection');
+const arraysEqual = require('../util/ArraysEqual');
+
+/*
+{ 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.messages = new Collection();
+ }
+
+ equals(other) {
+ const equal = other &&
+ this.id === other.id &&
+ this.name === other.name &&
+ this.icon === other.icon &&
+ this.owner.id === other.owner_id;
+
+ if (equal) {
+ const thisIDs = this.recipients.array().map(r => r.id);
+ const otherIDs = other.recipients.map(r => r.id);
+ return arraysEqual(thisIDs, otherIDs);
+ }
+
+ return equal;
+ }
+
+ setup(data) {
+ super.setup(data);
+
+ if (!this.recipients) {
+ /**
+ * A collection of the recipients of this DM, mapped by their ID.
+ * @type {Collection}
+ */
+ 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 name of this Group DM, can be null if one isn't set.
+ * @type {string}
+ */
+ this.name = data.name;
+ /**
+ * The ID of this Group DM Channel.
+ * @type {string}
+ */
+ this.id = data.id;
+ /**
+ * A hash of the Group DM icon.
+ * @type {string}
+ */
+ this.icon = data.icon;
+ /**
+ * The ID of the last message in the channel, if one was sent.
+ * @type {?string}
+ */
+ this.lastMessageID = data.last_message_id;
+ /**
+ * The owner of this Group DM.
+ * @type {User}
+ */
+ this.owner = this.client.users.get(data.owner_id);
+ this.type = 'group';
+ }
+
+ sendMessage() {
+ return;
+ }
+
+ sendTTSMessage() {
+ return;
+ }
+
+ sendFile() {
+ return;
+ }
+
+ _cacheMessage() {
+ return;
+ }
+
+ fetchMessages() {
+ return;
+ }
+
+ bulkDelete() {
+ return;
+ }
+
+ startTyping() {
+ return;
+ }
+
+ stopTyping() {
+ return;
+ }
+
+ get typing() {
+ return;
+ }
+
+ get typingCount() {
+ return;
+ }
+
+ fetchPinnedMessages() {
+ return;
+ }
+
+ createCollector() {
+ return;
+ }
+
+ awaitMessages() {
+ return;
+ }
+}
+
+TextBasedChannel.applyToClass(GroupDMChannel, true);
+
+module.exports = GroupDMChannel;
diff --git a/src/structures/Guild.js b/src/structures/Guild.js
new file mode 100644
index 000000000..59d1fb5e7
--- /dev/null
+++ b/src/structures/Guild.js
@@ -0,0 +1,614 @@
+const User = require('./User');
+const Role = require('./Role');
+const Emoji = require('./Emoji');
+const GuildMember = require('./GuildMember');
+const Constants = require('../util/Constants');
+const Collection = require('../util/Collection');
+const cloneObject = require('../util/CloneObject');
+const arraysEqual = require('../util/ArraysEqual');
+
+/**
+ * Represents a Guild (or a Server) on Discord.
+ * 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`.
+ */
+class Guild {
+ constructor(client, data) {
+ /**
+ * The Client that created the instance of the the Guild.
+ * @type {Client}
+ */
+ this.client = client;
+
+ /**
+ * A Collection of members that are in this Guild. The key is the member's ID, the value is the member.
+ * @type {Collection}
+ */
+ 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}
+ */
+ 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}
+ */
+ this.roles = new Collection();
+
+ if (!data) return;
+
+ if (data.unavailable) {
+ /**
+ * Whether the Guild is available to access. If it is not available, it indicates a server outage.
+ * @type {boolean}
+ */
+ this.available = false;
+ /**
+ * The Unique ID of the Guild, useful for comparisons.
+ * @type {string}
+ */
+ this.id = data.id;
+ } else {
+ this.available = true;
+ this.setup(data);
+ }
+ }
+
+ _checkChunks() {
+ if (this._fetchWaiter) {
+ if (this.members.size === this.memberCount) {
+ this._fetchWaiter(this);
+ this._fetchWaiter = null;
+ }
+ }
+ }
+
+ _addMember(guildUser, noEvent) {
+ 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.get(member.user.id)) {
+ const voiceState = this._rawVoiceStates.get(member.user.id);
+ member.serverMute = voiceState.mute;
+ member.serverDeaf = voiceState.deaf;
+ member.selfMute = voiceState.self_mute;
+ member.selfDeaf = voiceState.self_deaf;
+ member.voiceSessionID = voiceState.session_id;
+ member.voiceChannelID = voiceState.channel_id;
+ this.channels.get(voiceState.channel_id).members.set(member.user.id, member);
+ }
+ /**
+ * Emitted whenever a user joins a guild.
+ * @event Client#guildMemberAdd
+ * @param {Guild} guild The guild that the user has joined
+ * @param {GuildMember} member The member that has joined
+ */
+ if (this.client.ws.status === Constants.Status.READY && !noEvent) {
+ this.client.emit(Constants.Events.GUILD_MEMBER_ADD, this, member);
+ }
+
+ this._checkChunks();
+ return member;
+ }
+
+ _updateMember(member, data) {
+ const oldMember = cloneObject(member);
+
+ if (data.roles) member._roles = data.roles;
+ else member.nickname = data.nick;
+
+ const notSame = member.nickname !== oldMember.nickname && !arraysEqual(member._roles, oldMember._roles);
+
+ if (this.client.ws.status === Constants.Status.READY && notSame) {
+ /**
+ * Emitted whenever a Guild Member changes - i.e. new role, removed role, nickname
+ * @event Client#guildMemberUpdate
+ * @param {Guild} guild The guild that the update affects
+ * @param {GuildMember} oldMember The member before the update
+ * @param {GuildMember} newMember The member after the update
+ */
+ this.client.emit(Constants.Events.GUILD_MEMBER_UPDATE, this, oldMember, member);
+ }
+
+ return {
+ old: oldMember,
+ mem: member,
+ };
+ }
+
+ _removeMember(guildMember) {
+ this.members.delete(guildMember.id);
+ this._checkChunks();
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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|null}
+ * @example
+ * // get the guild member of a user
+ * const member = guild.member(message.author);
+ */
+ member(user) {
+ return this.client.resolver.resolveGuildMember(this, user);
+ }
+
+ /**
+ * 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
+ * @returns {boolean}
+ */
+ equals(guild) {
+ let equal =
+ guild &&
+ this.id === guild.id &&
+ this.available === !guild.unavailable &&
+ this.splash === guild.splash &&
+ this.region === guild.region &&
+ this.name === guild.name &&
+ this.memberCount === guild.member_count &&
+ this.large === guild.large &&
+ this.icon === guild.icon &&
+ arraysEqual(this.features, guild.features) &&
+ this.ownerID === guild.owner_id &&
+ this.verificationLevel === guild.verification_level &&
+ this.embedEnabled === guild.embed_enabled;
+
+ if (equal) {
+ if (this.embedChannel) {
+ if (this.embedChannel.id !== guild.embed_channel_id) equal = false;
+ } else if (guild.embed_channel_id) {
+ equal = false;
+ }
+ }
+
+ return equal;
+ }
+
+ _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);
+ }
+ }
+
+ /**
+ * Sets up the Guild
+ * @param {*} data The raw data of the guild
+ * @private
+ */
+ setup(data) {
+ this.id = data.id;
+ this.available = !data.unavailable;
+ /**
+ * The hash of the guild splash image, or null if no splash (VIP only)
+ * @type {?string}
+ */
+ this.splash = data.splash;
+ /**
+ * The region the guild is located in
+ * @type {string}
+ */
+ this.region = data.region;
+ /**
+ * The name of the guild
+ * @type {string}
+ */
+ this.name = data.name;
+ /**
+ * The full amount of members in this Guild as of `READY`
+ * @type {number}
+ */
+ this.memberCount = data.member_count;
+ /**
+ * Whether the guild is "large" (has more than 250 members)
+ * @type {boolean}
+ */
+ this.large = data.large;
+ this._joinDate = new Date(data.joined_at).getTime();
+ /**
+ * The hash of the guild icon, or null if there is no icon.
+ * @type {?string}
+ */
+ this.icon = data.icon;
+ /**
+ * An array of guild features.
+ * @type {Object[]}
+ */
+ this.features = data.features;
+ /**
+ * An array of guild emojis.
+ * @type {Object[]}
+ */
+ this.emojis = new Collection();
+ for (const emoji of data.emojis) {
+ this.emojis.set(emoji.id, new Emoji(this, emoji));
+ }
+ /**
+ * The time in seconds before a user is counted as "away from keyboard".
+ * @type {?number}
+ */
+ this.afkTimeout = data.afk_timeout;
+ /**
+ * The ID of the voice channel where AFK members are moved.
+ * @type {?string}
+ */
+ this.afkChannelID = data.afk_channel_id;
+ /**
+ * Whether embedded images are enabled on this guild.
+ * @type {boolean}
+ */
+ this.embedEnabled = data.embed_enabled;
+ /**
+ * The verification level of the guild.
+ * @type {number}
+ */
+ this.verificationLevel = data.verification_level;
+ this.features = data.features || [];
+
+ if (data.members) {
+ this.members.clear();
+ for (const guildUser of data.members) {
+ this._addMember(guildUser);
+ }
+ }
+
+ if (data.owner_id) {
+ 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) {
+ const user = this.client.users.get(presence.user.id);
+ if (user) {
+ user.status = presence.status;
+ user.game = presence.game;
+ }
+ }
+ }
+
+ this._rawVoiceStates = new Collection();
+
+ if (data.voice_states) {
+ for (const voiceState of data.voice_states) {
+ this._rawVoiceStates.set(voiceState.user_id, voiceState);
+ const member = this.members.get(voiceState.user_id);
+ if (member) {
+ member.serverMute = voiceState.mute;
+ member.serverDeaf = voiceState.deaf;
+ member.selfMute = voiceState.self_mute;
+ member.selfDeaf = voiceState.self_deaf;
+ member.voiceSessionID = voiceState.session_id;
+ member.voiceChannelID = voiceState.channel_id;
+ this.channels.get(voiceState.channel_id).members.set(member.user.id, member);
+ }
+ }
+ }
+ }
+
+ /**
+ * The date at which the logged-in client joined the guild.
+ * @type {Date}
+ */
+ get joinDate() {
+ return new Date(this._joinDate);
+ }
+
+ /**
+ * Creates a new Channel in the Guild.
+ * @param {string} name The name of the new channel
+ * @param {string} type The type of the new channel, either `text` or `voice`
+ * @returns {Promise}
+ * @example
+ * // create a new text channel
+ * guild.createChannel('new general', 'text')
+ * .then(channel => console.log(`Created new channel ${channel}`))
+ * .catch(console.log);
+ */
+ createChannel(name, type) {
+ return this.client.rest.methods.createChannel(this, name, type);
+ }
+
+ /**
+ * Creates a new role in the guild, as of now this is just a blank role.
+ * @returns {Promise}
+ * @example
+ * // create a new role
+ * guild.createRole()
+ * .then(role => console.log(`Created role ${role}`))
+ * .catch(console.log);
+ */
+ createRole() {
+ return this.client.rest.methods.createGuildRole(this);
+ }
+
+ /**
+ * Causes the Client to leave the guild.
+ * @returns {Promise}
+ * @example
+ * // leave a guild
+ * guild.leave()
+ * .then(g => console.log(`Left the guild ${g}`))
+ * .catch(console.log);
+ */
+ leave() {
+ return this.client.rest.methods.leaveGuild(this);
+ }
+
+ /**
+ * Causes the Client to delete the guild.
+ * @returns {Promise}
+ * @example
+ * // delete a guild
+ * guild.delete()
+ * .then(g => console.log(`Deleted the guild ${g}`))
+ * .catch(console.log);
+ */
+ delete() {
+ return this.client.rest.methods.deleteGuild(this);
+ }
+
+ /**
+ * Updates the Guild with new information - e.g. a new name.
+ * @param {GuildEditData} data The data to update the guild with
+ * @returns {Promise}
+ * @example
+ * // set the guild name and region
+ * guild.edit({
+ * name: 'Discord Guild',
+ * region: 'london',
+ * })
+ * .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`))
+ * .catch(console.log);
+ */
+ edit(data) {
+ return this.client.rest.methods.updateGuild(this, data);
+ }
+
+ /**
+ * Edit the name of the Guild.
+ * @param {string} name The new name of the Guild
+ * @returns {Promise}
+ * @example
+ * // edit the guild name
+ * guild.setName('Discord Guild')
+ * .then(updated => console.log(`Updated guild name to ${guild.name}`))
+ * .catch(console.log);
+ */
+ setName(name) {
+ return this.edit({ name });
+ }
+
+ /**
+ * Edit the region of the Guild.
+ * @param {Region} region The new region of the guild.
+ * @returns {Promise}
+ * @example
+ * // edit the guild region
+ * guild.setRegion('london')
+ * .then(updated => console.log(`Updated guild region to ${guild.region}`))
+ * .catch(console.log);
+ */
+ setRegion(region) {
+ return this.edit({ region });
+ }
+
+ /**
+ * Edit the verification level of the Guild.
+ * @param {VerificationLevel} verificationLevel The new verification level of the guild
+ * @returns {Promise}
+ * @example
+ * // edit the guild verification level
+ * guild.setVerificationLevel(1)
+ * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`))
+ * .catch(console.log);
+ */
+ setVerificationLevel(verificationLevel) {
+ return this.edit({ verificationLevel });
+ }
+
+ /**
+ * Edit the AFK channel of the Guild.
+ * @param {GuildChannelResolvable} afkChannel The new AFK channel
+ * @returns {Promise}
+ * @example
+ * // edit the guild AFK channel
+ * guild.setAFKChannel(channel)
+ * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`))
+ * .catch(console.log);
+ */
+ setAFKChannel(afkChannel) {
+ return this.edit({ afkChannel });
+ }
+
+ /**
+ * Edit the AFK timeout of the Guild.
+ * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK
+ * @returns {Promise}
+ * @example
+ * // edit the guild AFK channel
+ * guild.setAFKTimeout(60)
+ * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`))
+ * .catch(console.log);
+ */
+ setAFKTimeout(afkTimeout) {
+ return this.edit({ afkTimeout });
+ }
+
+ /**
+ * Set a new Guild Icon.
+ * @param {Base64Resolvable} icon The new icon of the guild
+ * @returns {Promise}
+ * @example
+ * // edit the guild icon
+ * guild.setIcon(fs.readFileSync('./icon.png'))
+ * .then(updated => console.log('Updated the guild icon'))
+ * .catch(console.log);
+ */
+ setIcon(icon) {
+ return this.edit({ icon });
+ }
+
+ /**
+ * Sets a new owner of the Guild.
+ * @param {GuildMemberResolvable} owner The new owner of the Guild
+ * @returns {Promise}
+ * @example
+ * // edit the guild owner
+ * guild.setOwner(guilds.members[0])
+ * .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`))
+ * .catch(console.log);
+ */
+ setOwner(owner) {
+ return this.edit({ owner });
+ }
+
+ /**
+ * Set a new Guild Splash Logo.
+ * @param {Base64Resolvable} splash The new splash screen of the guild
+ * @returns {Promise}
+ * @example
+ * // edit the guild splash
+ * guild.setIcon(fs.readFileSync('./splash.png'))
+ * .then(updated => console.log('Updated the guild splash'))
+ * .catch(console.log);
+ */
+ setSplash(splash) {
+ return this.edit({ splash });
+ }
+
+ /**
+ * Unbans a member from the Guild
+ * @param {UserResolvable} member The member to unban
+ * @returns {Promise}
+ * @example
+ * // unban a member
+ * guild.unban('123123123123')
+ * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
+ * .catch(reject);
+ */
+ unban(member) {
+ return this.client.rest.methods.unbanGuildMember(this, member);
+ }
+
+ /**
+ * Fetch a Collection of banned users in this Guild.
+ * @returns {Promise>}
+ */
+ fetchBans() {
+ return this.client.rest.methods.getGuildBans(this);
+ }
+
+ /**
+ * Fetch a Collection of invites to this Guild. Resolves with a Collection mapping invites by their codes.
+ * @returns {Promise>}
+ */
+ fetchInvites() {
+ return this.client.rest.methods.getGuildInvites(this);
+ }
+
+ /**
+ * Fetches all the members in the Guild, even if they are offline. If the Guild has less than 250 members,
+ * this should not be necessary.
+ * @param {string} [query=''] An optional query to provide when fetching members
+ * @returns {Promise}
+ */
+ fetchMembers(query = '') {
+ return new Promise((resolve, reject) => {
+ if (this._fetchWaiter) {
+ throw new Error('already fetching guild members');
+ }
+ if (this.memberCount === this.members.size) {
+ resolve(this);
+ return;
+ }
+ this._fetchWaiter = resolve;
+ this.client.ws.send({
+ op: Constants.OPCodes.REQUEST_GUILD_MEMBERS,
+ d: {
+ guild_id: this.id,
+ query,
+ limit: 0,
+ },
+ });
+ this._checkChunks();
+ this.client.setTimeout(() => reject(new Error('members not here in time')), 120 * 1000);
+ });
+ }
+
+ /**
+ * Gets the URL to this guild's icon (if it has one, otherwise it returns null)
+ * @type {?string}
+ * @readonly
+ */
+ get iconURL() {
+ if (!this.icon) return null;
+ return Constants.Endpoints.guildIcon(this.id, this.icon);
+ }
+
+ /**
+ * The owner of the Guild
+ * @type {GuildMember}
+ * @readonly
+ */
+ get owner() {
+ return this.members.get(this.ownerID);
+ }
+
+ /**
+ * Syncs this guild (already done automatically every 30 seconds). Only applicable to user accounts.
+ */
+ sync() {
+ if (!this.client.user.bot) {
+ this.client.syncGuilds([this]);
+ }
+ }
+}
+
+module.exports = Guild;
diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js
new file mode 100644
index 000000000..aeef11603
--- /dev/null
+++ b/src/structures/GuildChannel.js
@@ -0,0 +1,273 @@
+const Channel = require('./Channel');
+const Role = require('./Role');
+const PermissionOverwrites = require('./PermissionOverwrites');
+const EvaluatedPermissions = require('./EvaluatedPermissions');
+const Constants = require('../util/Constants');
+const Collection = require('../util/Collection');
+const arraysEqual = require('../util/ArraysEqual');
+
+/**
+ * Represents a Guild Channel (i.e. Text Channels and Voice Channels)
+ * @extends {Channel}
+ */
+class GuildChannel extends Channel {
+ constructor(guild, data) {
+ super(guild.client, data, guild);
+ }
+
+ setup(data) {
+ super.setup(data);
+ /**
+ * The topic of the Guild Channel, if there is one.
+ * @type {?string}
+ */
+ this.topic = data.topic;
+ /**
+ * The position of the channel in the list.
+ * @type {number}
+ */
+ this.position = data.position;
+ /**
+ * The name of the Guild Channel
+ * @type {string}
+ */
+ this.name = data.name;
+ this.ow = data.permission_overwrites;
+ /**
+ * A map of permission overwrites in this channel for roles and users.
+ * @type {Collection}
+ */
+ this.permissionOverwrites = new Collection();
+ if (data.permission_overwrites) {
+ for (const overwrite of data.permission_overwrites) {
+ this.permissionOverwrites.set(overwrite.id, new PermissionOverwrites(this, overwrite));
+ }
+ }
+ }
+
+ /**
+ * 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 The channel to compare this channel to
+ * @returns {boolean}
+ */
+ equals(channel) {
+ let equal = channel &&
+ this.type === channel.type &&
+ this.topic === channel.topic &&
+ this.position === channel.position &&
+ this.name === channel.name &&
+ this.id === channel.id;
+
+ if (equal) {
+ if (channel.permission_overwrites) {
+ const thisIDSet = Array.from(this.permissionOverwrites.keys());
+ const otherIDSet = channel.permission_overwrites.map(overwrite => overwrite.id);
+ equal = arraysEqual(thisIDSet, otherIDSet);
+ } else {
+ equal = false;
+ }
+ }
+
+ return equal;
+ }
+
+ /**
+ * Gets the overall set of permissions for a user in this channel, taking into account roles and permission
+ * overwrites.
+ * @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for
+ * @returns {?EvaluatedPermissions}
+ */
+ permissionsFor(member) {
+ member = this.client.resolver.resolveGuildMember(this.guild, member);
+ if (member) {
+ if (this.guild.owner.id === member.id) return new EvaluatedPermissions(member, Constants.ALL_PERMISSIONS);
+
+ const roles = member.roles;
+ let permissions = 0;
+ const overwrites = this.overwritesFor(member, true);
+
+ for (const role of roles.values()) permissions |= role.permissions;
+ for (const overwrite of overwrites.role.concat(overwrites.member)) {
+ permissions &= ~overwrite.denyData;
+ permissions |= overwrite.allowData;
+ }
+
+ const admin = Boolean(permissions & (Constants.PermissionFlags.ADMINISTRATOR));
+ if (admin) permissions = Constants.ALL_PERMISSIONS;
+
+ return new EvaluatedPermissions(member, permissions);
+ }
+
+ return null;
+ }
+
+ overwritesFor(member, verified) {
+ // for speed
+ if (!verified) member = this.client.resolver.resolveGuildMember(this.guild, member);
+ if (member) {
+ const memberRoles = member._roles;
+
+ const roleOverwrites = [];
+ const memberOverwrites = [];
+
+ for (const overwrite of this.permissionOverwrites.values()) {
+ if (overwrite.id === member.id) {
+ memberOverwrites.push(overwrite);
+ } else if (memberRoles.indexOf(overwrite.id) > -1) {
+ roleOverwrites.push(overwrite);
+ }
+ }
+
+ return {
+ role: roleOverwrites,
+ member: memberOverwrites,
+ };
+ }
+
+ return [];
+ }
+
+ /**
+ * An object mapping permission flags to `true` (enabled) or `false` (disabled)
+ * ```js
+ * {
+ * 'SEND_MESSAGES': true,
+ * 'ATTACH_FILES': false,
+ * }
+ * ```
+ * @typedef {Object} PermissionOverwriteOptions
+ */
+
+ /**
+ * Overwrites the permissions for a user or role in this channel.
+ * @param {Role|UserResolvable} userOrRole The user or role to update
+ * @param {PermissionOverwriteOptions} options The configuration for the update
+ * @returns {Promise}
+ * @example
+ * // overwrite permissions for a message author
+ * message.channel.overwritePermissions(message.author, {
+ * SEND_MESSAGES: false
+ * })
+ * .then(() => console.log('Done!'))
+ * .catch(console.log);
+ */
+ overwritePermissions(userOrRole, options) {
+ const payload = {
+ allow: 0,
+ deny: 0,
+ };
+
+ if (userOrRole instanceof Role) {
+ 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 or a role'));
+ }
+
+ payload.id = userOrRole.id;
+
+ const prevOverwrite = this.permissionOverwrites.get(userOrRole.id);
+
+ if (prevOverwrite) {
+ payload.allow = prevOverwrite.allow;
+ payload.deny = prevOverwrite.deny;
+ }
+
+ for (const perm in options) {
+ if (options[perm] === true) {
+ payload.allow |= Constants.PermissionFlags[perm] || 0;
+ payload.deny &= ~(Constants.PermissionFlags[perm] || 0);
+ } else if (options[perm] === false) {
+ payload.allow &= ~(Constants.PermissionFlags[perm] || 0);
+ payload.deny |= Constants.PermissionFlags[perm] || 0;
+ }
+ }
+
+ return this.client.rest.methods.setChannelOverwrite(this, payload);
+ }
+
+ edit(data) {
+ return this.client.rest.methods.updateChannel(this, data);
+ }
+
+ /**
+ * Set a new name for the Guild Channel
+ * @param {string} name The new name for the guild channel
+ * @returns {Promise}
+ * @example
+ * // set a new channel name
+ * channel.setName('not general')
+ * .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))
+ * .catch(console.log);
+ */
+ setName(name) {
+ return this.client.rest.methods.updateChannel(this, { name });
+ }
+
+ /**
+ * Set a new position for the Guild Channel
+ * @param {number} position The new position for the guild channel
+ * @returns {Promise}
+ * @example
+ * // set a new channel position
+ * channel.setPosition(2)
+ * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
+ * .catch(console.log);
+ */
+ setPosition(position) {
+ return this.client.rest.methods.updateChannel(this, { position });
+ }
+
+ /**
+ * Set a new topic for the Guild Channel
+ * @param {string} topic The new topic for the guild channel
+ * @returns {Promise}
+ * @example
+ * // set a new channel topic
+ * channel.setTopic('needs more rate limiting')
+ * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
+ * .catch(console.log);
+ */
+ setTopic(topic) {
+ return this.client.rest.methods.updateChannel(this, { topic });
+ }
+
+ /**
+ * When concatenated with a string, this automatically returns the Channel's mention instead of the Channel object.
+ * @returns {string}
+ * @example
+ * // Outputs: Hello from #general
+ * console.log(`Hello from ${channel}`);
+ * @example
+ * // Outputs: Hello from #general
+ * console.log('Hello from ' + channel);
+ */
+ toString() {
+ return `<#${this.id}>`;
+ }
+
+ /**
+ * Options given when creating a Guild Channel Invite:
+ * ```js
+ * {
+ * temporary: false, // whether the invite should kick users after 24hrs if they are not given a new role
+ * maxAge: 0, // the time in seconds the invite expires in
+ * maxUses: 0, // the maximum amount of uses for this invite
+ * }
+ * ```
+ * @typedef {Object} InviteOptions
+ */
+
+ /**
+ * Create an invite to this Guild Channel
+ * @param {InviteOptions} [options={}] The options for the invite
+ * @returns {Promise}
+ */
+ createInvite(options = {}) {
+ return this.client.rest.methods.createChannelInvite(this, options);
+ }
+}
+
+module.exports = GuildChannel;
diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js
new file mode 100644
index 000000000..e6a0e291b
--- /dev/null
+++ b/src/structures/GuildMember.js
@@ -0,0 +1,236 @@
+const TextBasedChannel = require('./interface/TextBasedChannel');
+const Collection = require('../util/Collection');
+
+/**
+ * Represents a Member of a Guild on Discord
+ * @implements {TextBasedChannel}
+ */
+class GuildMember {
+ constructor(guild, data) {
+ /**
+ * The client that instantiated this GuildMember
+ * @type {Client}
+ */
+ this.client = guild.client;
+ /**
+ * The guild that this member is part of
+ * @type {Guild}
+ */
+ this.guild = guild;
+ /**
+ * The user that this guild member instance Represents
+ * @type {User}
+ */
+ this.user = {};
+ this._roles = [];
+ if (data) this.setup(data);
+ }
+
+ setup(data) {
+ this.user = data.user;
+ /**
+ * Whether this member is deafened server-wide
+ * @type {boolean}
+ */
+ this.serverDeaf = data.deaf;
+ /**
+ * Whether this member is muted server-wide
+ * @type {boolean}
+ */
+ this.serverMute = data.mute;
+ /**
+ * Whether this member is self-muted
+ * @type {boolean}
+ */
+ this.selfMute = data.self_mute;
+ /**
+ * Whether this member is self-deafened
+ * @type {boolean}
+ */
+ this.selfDeaf = data.self_deaf;
+ /**
+ * The voice session ID of this member, if any
+ * @type {?string}
+ */
+ this.voiceSessionID = data.session_id;
+ /**
+ * The voice channel ID of this member, if any
+ * @type {?string}
+ */
+ this.voiceChannelID = data.channel_id;
+ this._joinDate = new Date(data.joined_at).getTime();
+ /**
+ * Whether this meember is speaking
+ * @type {?boolean}
+ */
+ this.speaking = this.speaking;
+ /**
+ * The nickname of this Guild Member, if they have one
+ * @type {?string}
+ */
+ this.nickname = data.nick;
+ this._roles = data.roles;
+ }
+
+ /**
+ * The date this member joined the guild
+ * @type {Date}
+ */
+ get joinDate() {
+ return new Date(this._joinDate);
+ }
+
+ /**
+ * A list of roles that are applied to this GuildMember, mapped by the role ID.
+ * @type {Collection}
+ * @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;
+ }
+
+ /**
+ * Whether this member is muted in any way
+ * @type {boolean}
+ * @readonly
+ */
+ get mute() {
+ return this.selfMute || this.serverMute;
+ }
+
+ /**
+ * Whether this member is deafened in any way
+ * @type {boolean}
+ * @readonly
+ */
+ get deaf() {
+ return this.selfDeaf || this.serverDeaf;
+ }
+
+ /**
+ * The voice channel this member is in, if any
+ * @type {?VoiceChannel}
+ * @readonly
+ */
+ get voiceChannel() {
+ return this.guild.channels.get(this.voiceChannelID);
+ }
+
+ /**
+ * The ID of this User
+ * @type {string}
+ * @readonly
+ */
+ get id() {
+ return this.user.id;
+ }
+
+ /**
+ * Mute/unmute a user
+ * @param {boolean} mute Whether or not the member should be muted
+ * @returns {Promise}
+ */
+ setMute(mute) {
+ return this.edit({ mute });
+ }
+
+ /**
+ * Deafen/undeafen a user
+ * @param {boolean} deaf Whether or not the member should be deafened
+ * @returns {Promise}
+ */
+ setDeaf(deaf) {
+ return this.edit({ deaf });
+ }
+
+ /**
+ * Moves the Guild Member to the given channel.
+ * @param {ChannelResolvable} channel The channel to move the member to
+ * @returns {Promise}
+ */
+ setVoiceChannel(channel) {
+ return this.edit({ channel });
+ }
+
+ /**
+ * Sets the Roles applied to the member.
+ * @param {Collection|Role[]} roles The roles to apply
+ * @returns {Promise}
+ */
+ setRoles(roles) {
+ return this.edit({ roles });
+ }
+
+ /**
+ * Set the nickname for the Guild Member
+ * @param {string} nick The nickname for the Guild Member
+ * @returns {Promise}
+ */
+ setNickname(nick) {
+ return this.edit({ nick });
+ }
+
+ /**
+ * Edit a Guild Member
+ * @param {GuildmemberEditData} data The data to edit the member with
+ * @returns {Promise}
+ */
+ edit(data) {
+ return this.client.rest.methods.updateGuildMember(this, data);
+ }
+
+ /**
+ * Deletes any DMs with this Guild Member
+ * @returns {Promise}
+ */
+ deleteDM() {
+ return this.client.rest.methods.deleteChannel(this);
+ }
+
+ /**
+ * Kick this member from the Guild
+ * @returns {Promise}
+ */
+ kick() {
+ return this.client.rest.methods.kickGuildMember(this.guild, this);
+ }
+
+ /**
+ * Ban this Guild Member
+ * @param {number} [deleteDays=0] The amount of days worth of messages from this member that should
+ * also be deleted. Between `0` and `7`.
+ * @returns {Promise}
+ * @example
+ * // ban a guild member
+ * guildMember.ban(7);
+ */
+ ban(deleteDays = 0) {
+ return this.client.rest.methods.banGuildMember(this, deleteDays);
+ }
+
+ sendMessage() {
+ return;
+ }
+
+ sendTTSMessage() {
+ return;
+ }
+
+ sendFile() {
+ return;
+ }
+}
+
+TextBasedChannel.applyToClass(GuildMember);
+
+module.exports = GuildMember;
diff --git a/src/structures/Invite.js b/src/structures/Invite.js
new file mode 100644
index 000000000..1c09e6425
--- /dev/null
+++ b/src/structures/Invite.js
@@ -0,0 +1,108 @@
+const PartialGuild = require('./PartialGuild');
+const PartialGuildChannel = require('./PartialGuildChannel');
+
+/*
+{ max_age: 86400,
+ code: 'CG9A5',
+ guild:
+ { splash: null,
+ id: '123123123',
+ icon: '123123123',
+ name: 'name' },
+ created_at: '2016-08-28T19:07:04.763368+00:00',
+ temporary: false,
+ uses: 0,
+ max_uses: 0,
+ inviter:
+ { username: '123',
+ discriminator: '4204',
+ bot: true,
+ id: '123123123',
+ avatar: '123123123' },
+ channel: { type: 0, id: '123123', name: 'heavy-testing' } }
+*/
+
+/**
+ * Represents an Invitation to a Guild Channel
+ */
+class Invite {
+ constructor(client, data) {
+ /**
+ * The client that instantiated the invite
+ * @type {Client}
+ */
+ this.client = client;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The maximum age of the invite, in seconds
+ * @type {?number}
+ */
+ this.maxAge = data.max_age;
+
+ /**
+ * The code for this invite
+ * @type {string}
+ */
+ this.code = data.code;
+ this._creationDate = new Date(data.created_at).getTime();
+
+ /**
+ * Whether or not this invite is temporary
+ * @type {boolean}
+ */
+ this.temporary = data.temporary;
+
+ /**
+ * 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;
+
+ /**
+ * The user who created this invite
+ * @type {User}
+ */
+ this.inviter = this.client.dataManager.newUser(data.inviter);
+
+ /**
+ * 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 Partial Guild.
+ * @type {Guild|PartialGuild}
+ */
+ this.guild = this.client.guilds.get(data.guild.id) || new PartialGuild(this.client, data.guild);
+
+ /**
+ * 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 Partial Guild Channel.
+ * @type {GuildChannel|PartialGuildChannel}
+ */
+ this.channels = this.client.channels.get(data.channel.id) || new PartialGuildChannel(this.client, data.channel);
+ }
+
+ /**
+ * The creation date of the invite
+ * @type {Date}
+ */
+ get creationDate() {
+ return new Date(this._creationDate);
+ }
+
+ /**
+ * Deletes this invite
+ * @returns {Promise}
+ */
+ delete() {
+ return this.client.rest.methods.deleteInvite(this);
+ }
+}
+
+module.exports = Invite;
diff --git a/src/structures/Message.js b/src/structures/Message.js
new file mode 100644
index 000000000..f0411460b
--- /dev/null
+++ b/src/structures/Message.js
@@ -0,0 +1,303 @@
+const Attachment = require('./MessageAttachment');
+const Embed = require('./MessageEmbed');
+const Collection = require('../util/Collection');
+
+/**
+ * Represents a Message on Discord
+ */
+class Message {
+ constructor(channel, data, client) {
+ this._type = 'message';
+ /**
+ * The channel that the message was sent in
+ * @type {TextChannel|DMChannel|GroupDMChannel}
+ */
+ this.channel = channel;
+
+ if (channel.guild) {
+ /**
+ * If the message was sent in a guild, this will be the guild the message was sent in
+ * @type {?Guild}
+ */
+ this.guild = channel.guild;
+ }
+
+ /**
+ * The client that instantiated the Message
+ * @type {Client}
+ */
+ this.client = client;
+ if (data) this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * Whether or not this message is pinned
+ * @type {boolean}
+ */
+ this.pinned = data.pinned;
+ /**
+ * The author of the message
+ * @type {User}
+ */
+ this.author = this.client.dataManager.newUser(data.author);
+ if (this.guild) {
+ /**
+ * 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.member(this.author);
+ }
+ /**
+ * The content of the message
+ * @type {string}
+ */
+ this.content = data.content;
+ this._timestamp = new Date(data.timestamp).getTime();
+ this._editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
+ /**
+ * Whether or not the message was Text-To-Speech
+ * @type {boolean}
+ */
+ this.tts = data.tts;
+ /**
+ * A random number used for checking message delivery
+ * @type {string}
+ */
+ this.nonce = data.nonce;
+ /**
+ * A list of embeds in the message - e.g. YouTube Player
+ * @type {Embed[]}
+ */
+ 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}
+ */
+ this.attachments = new Collection();
+ for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment));
+ /**
+ * An object containing a further users, roles or channels collections
+ * @type {Object}
+ * @property {Collection} mentions.users Mentioned users, maps their ID to the user object.
+ * @property {Collection} mentions.roles Mentioned roles, maps their ID to the role object.
+ * @property {Collection} mentions.channels Mentioned channels,
+ * maps their ID to the channel object.
+ * @property {boolean} mentions.everyone Whether or not @everyone was mentioned.
+ */
+ this.mentions = {
+ users: new Collection(),
+ roles: new Collection(),
+ channels: new Collection(),
+ everyone: data.mention_everyone,
+ };
+ /**
+ * The ID of the message (unique in the channel it was sent)
+ * @type {string}
+ */
+ this.id = data.id;
+
+ for (const mention of data.mentions) {
+ let user = this.client.users.get(mention.id);
+ if (user) {
+ this.mentions.users.set(user.id, user);
+ } else {
+ user = this.client.dataManager.newUser(mention);
+ this.mentions.users.set(user.id, user);
+ }
+ }
+
+ if (data.mention_roles) {
+ for (const mention of data.mention_roles) {
+ const role = this.channel.guild.roles.get(mention);
+ if (role) this.mentions.roles.set(role.id, role);
+ }
+ }
+
+ if (this.channel.guild) {
+ const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || [];
+ for (const raw of channMentionsRaw) {
+ const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]);
+ if (chan) this.mentions.channels.set(chan.id, chan);
+ }
+ }
+
+ /**
+ * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
+ * @type {boolean}
+ */
+ this.system = false;
+ if (data.type === 6) this.system = true;
+ }
+ /**
+ * When the message was sent
+ * @type {Date}
+ */
+ get timestamp() {
+ return new Date(this._timestamp);
+ }
+
+ /**
+ * If the message was edited, the timestamp at which it was last edited
+ * @type {?Date}
+ */
+ get editedTimestamp() {
+ return new Date(this._editedTimestamp);
+ }
+
+ patch(data) { // eslint-disable-line complexity
+ if (data.author) {
+ this.author = this.client.users.get(data.author.id);
+ if (this.guild) this.member = this.guild.member(this.author);
+ }
+ if (data.content) this.content = data.content;
+ if (data.timestamp) this._timestamp = new Date(data.timestamp).getTime();
+ if (data.edited_timestamp) {
+ this._editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
+ }
+ if ('tts' in data) this.tts = data.tts;
+ if ('mention_everyone' in data) this.mentions.everyone = data.mention_everyone;
+ if (data.nonce) this.nonce = data.nonce;
+ if (data.embeds) this.embeds = data.embeds.map(e => new Embed(this, e));
+ if (data.type > -1) {
+ this.system = false;
+ if (data.type === 6) {
+ this.system = true;
+ }
+ }
+ if (data.attachments) {
+ this.attachments = new Collection();
+ for (const attachment of data.attachments) {
+ this.attachments.set(attachment.id, new Attachment(this, attachment));
+ }
+ }
+ if (data.mentions) {
+ for (const mention of data.mentions) {
+ let user = this.client.users.get(mention.id);
+ if (user) {
+ this.mentions.users.set(user.id, user);
+ } else {
+ user = this.client.dataManager.newUser(mention);
+ this.mentions.users.set(user.id, user);
+ }
+ }
+ }
+ if (data.mention_roles) {
+ for (const mention of data.mention_roles) {
+ const role = this.channel.guild.roles.get(mention);
+ if (role) {
+ this.mentions.roles.set(role.id, role);
+ }
+ }
+ }
+ if (data.id) this.id = data.id;
+ if (this.channel.guild && data.content) {
+ const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || [];
+ for (const raw of channMentionsRaw) {
+ const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]);
+ if (chan) {
+ this.mentions.channels.set(chan.id, chan);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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._timestamp === new Date(rawData.timestamp).getTime() &&
+ this._editedTimestamp === new Date(rawData.edited_timestamp).getTime();
+ }
+
+ return equal;
+ }
+
+ /**
+ * Deletes the message
+ * @param {number} [timeout=0] How long to wait to delete the message in milliseconds
+ * @returns {Promise}
+ * @example
+ * // delete a message
+ * message.delete()
+ * .then(msg => console.log(`Deleted message from ${msg.author}`))
+ * .catch(console.log);
+ */
+ delete(timeout = 0) {
+ return new Promise((resolve, reject) => {
+ this.client.setTimeout(() => {
+ this.client.rest.methods.deleteMessage(this)
+ .then(resolve)
+ .catch(reject);
+ }, timeout);
+ });
+ }
+
+ /**
+ * Edit the content of a message
+ * @param {string} content The new content for the message
+ * @returns {Promise}
+ * @example
+ * // update the content of a message
+ * message.edit('This is my new content!')
+ * .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
+ * .catch(console.log);
+ */
+ edit(content) {
+ return this.client.rest.methods.updateMessage(this, content);
+ }
+
+ /**
+ * Reply to a message
+ * @param {string} content The content for the message
+ * @param {MessageOptions} [options = {}] The options to provide
+ * @returns {Promise}
+ * @example
+ * // reply to a message
+ * message.reply('Hey, I'm a reply!')
+ * .then(msg => console.log(`Sent a reply to ${msg.author}`))
+ * .catch(console.log);
+ */
+ reply(content, options = {}) {
+ const newContent = this.guild ? `${this.author}, ${content}` : content;
+ return this.client.rest.methods.sendMessage(this.channel, newContent, options.tts);
+ }
+
+ /**
+ * Pins this message to the channel's pinned messages
+ * @returns {Promise}
+ */
+ pin() {
+ return this.client.rest.methods.pinMessage(this);
+ }
+
+ /**
+ * Unpins this message from the channel's pinned messages
+ * @returns {Promise}
+ */
+ unpin() {
+ return this.client.rest.methods.unpinMessage(this);
+ }
+}
+
+module.exports = Message;
diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js
new file mode 100644
index 000000000..ad74479e8
--- /dev/null
+++ b/src/structures/MessageAttachment.js
@@ -0,0 +1,58 @@
+/**
+ * Represents an Attachment in a Message
+ */
+class MessageAttachment {
+ constructor(message, data) {
+ /**
+ * The Client that instantiated this Message.
+ * @type {Client}
+ */
+ this.client = message.client;
+ /**
+ * The message this attachment is part of.
+ * @type {Message}
+ */
+ this.message = message;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The ID of this attachment
+ * @type {string}
+ */
+ this.id = data.id;
+ /**
+ * The file name of this attachment
+ * @type {string}
+ */
+ this.filename = data.filename;
+ /**
+ * The size of this attachment in bytes
+ * @type {number}
+ */
+ this.filesize = data.size;
+ /**
+ * The URL to this attachment
+ * @type {string}
+ */
+ this.url = data.url;
+ /**
+ * The Proxy URL to this attachment
+ * @type {string}
+ */
+ this.proxyURL = data.url;
+ /**
+ * The height of this attachment (if an image)
+ * @type {?number}
+ */
+ this.height = data.height;
+ /**
+ * The width of this attachment (if an image)
+ * @type {?number}
+ */
+ this.width = data.width;
+ }
+}
+
+module.exports = MessageAttachment;
diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js
new file mode 100644
index 000000000..50f3c2d90
--- /dev/null
+++ b/src/structures/MessageEmbed.js
@@ -0,0 +1,121 @@
+/**
+ * Represents an embed in an image - e.g. preview of image
+ */
+class MessageEmbed {
+ constructor(message, data) {
+ /**
+ * The message this embed is part of
+ * @type {Message}
+ */
+ this.message = message;
+ /**
+ * The client that instantiated this embed
+ * @type {Client}
+ */
+ this.client = message.client;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The title of this embed, if there is one
+ * @type {?string}
+ */
+ this.title = data.title;
+ /**
+ * The type of this embed
+ * @type {string}
+ */
+ this.type = data.type;
+ /**
+ * The description of this embed, if there is one
+ * @type {?string}
+ */
+ this.description = data.description;
+ /**
+ * The URL of this embed
+ * @type {string}
+ */
+ this.url = data.url;
+ if (data.thumbnail) {
+ /**
+ * The thumbnail of this embed, if there is one
+ * @type {MessageEmbedThumbnail}
+ */
+ this.thumbnail = new MessageEmbedThumbnail(this, data.thumbnail);
+ }
+ if (data.provider) {
+ /**
+ * The provider of this embed, if there is one
+ * @type {MessageEmbedProvider}
+ */
+ this.provider = new MessageEmbedProvider(this, data.provider);
+ }
+ }
+}
+
+/**
+ * Represents a thumbnail for a Message embed
+ */
+class MessageEmbedThumbnail {
+ constructor(embed, data) {
+ /**
+ * The embed this thumbnail is part of
+ * @type {MessageEmbed}
+ */
+ this.embed = embed;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The URL for this thumbnail
+ * @type {string}
+ */
+ this.url = data.url;
+ /**
+ * The Proxy URL for this thumbnail
+ * @type {string}
+ */
+ this.proxyURL = data.proxy_url;
+ /**
+ * The height of the thumbnail
+ * @type {number}
+ */
+ this.height = data.height;
+ /**
+ * The width of the thumbnail
+ * @type {number}
+ */
+ this.width = data.width;
+ }
+}
+
+/**
+ * Represents a Provider for a Message embed
+ */
+class MessageEmbedProvider {
+ constructor(embed, data) {
+ /**
+ * The embed this provider is part of
+ * @type {MessageEmbed}
+ */
+ this.embed = embed;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The name of this provider
+ * @type {string}
+ */
+ this.name = data.name;
+ /**
+ * The URL of this provider
+ * @type {string}
+ */
+ this.url = data.url;
+ }
+}
+
+module.exports = MessageEmbed;
diff --git a/src/structures/PartialGuild.js b/src/structures/PartialGuild.js
new file mode 100644
index 000000000..ac6334abd
--- /dev/null
+++ b/src/structures/PartialGuild.js
@@ -0,0 +1,45 @@
+/*
+{ 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
+ * @type {Client}
+ */
+ this.client = client;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The hash of the guild splash image, or null if no splash (VIP only)
+ * @type {?string}
+ */
+ this.splash = data.splash;
+ /**
+ * The ID of this guild
+ * @type {string}
+ */
+ this.id = data.id;
+ /**
+ * The hash of this guild's icon, or null if there is none.
+ * @type {?string}
+ */
+ this.icon = data.icon;
+ /**
+ * The name of this guild
+ * @type {string}
+ */
+ this.name = data.name;
+ }
+}
+
+module.exports = PartialGuild;
diff --git a/src/structures/PartialGuildChannel.js b/src/structures/PartialGuildChannel.js
new file mode 100644
index 000000000..01a5ae73d
--- /dev/null
+++ b/src/structures/PartialGuildChannel.js
@@ -0,0 +1,39 @@
+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
+ * @type {Client}
+ */
+ this.client = client;
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The ID of this Guild Channel
+ * @type {string}
+ */
+ this.id = data.id;
+ /**
+ * The name of this Guild Channel
+ * @type {string}
+ */
+ this.name = data.name;
+ /**
+ * The type of this Guild Channel - `text` or `voice`
+ * @type {string}
+ */
+ this.type = Constants.ChannelTypes.text === data.type ? 'text' : 'voice';
+ }
+}
+
+module.exports = PartialGuildChannel;
diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js
new file mode 100644
index 000000000..7c31c7fc2
--- /dev/null
+++ b/src/structures/PermissionOverwrites.js
@@ -0,0 +1,38 @@
+/**
+ * Represents a permission overwrite for a Role or Member in a Guild Channel.
+ */
+class PermissionOverwrites {
+ constructor(guildChannel, data) {
+ /**
+ * The GuildChannel this overwrite is for
+ * @type {GuildChannel}
+ */
+ this.channel = guildChannel;
+ if (data) this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The type of this overwrite
+ * @type {string}
+ */
+ this.type = data.type;
+ /**
+ * The ID of this overwrite, either a User ID or a Role ID
+ * @type {string}
+ */
+ this.id = data.id;
+ this.denyData = data.deny;
+ this.allowData = data.allow;
+ }
+
+ /**
+ * Delete this Permission Overwrite.
+ * @returns {Promise}
+ */
+ delete() {
+ return this.channel.client.rest.methods.deletePermissionOverwrites(this);
+ }
+}
+
+module.exports = PermissionOverwrites;
diff --git a/src/structures/Role.js b/src/structures/Role.js
new file mode 100644
index 000000000..febe970b9
--- /dev/null
+++ b/src/structures/Role.js
@@ -0,0 +1,221 @@
+const Constants = require('../util/Constants');
+
+/**
+ * Represents a Role on Discord
+ */
+class Role {
+ constructor(guild, data) {
+ /**
+ * The guild that the role belongs to
+ * @type {Guild}
+ */
+ this.guild = guild;
+ /**
+ * The client that instantiated the role
+ * @type {Client}
+ */
+ this.client = guild.client;
+ if (data) this.setup(data);
+ }
+
+ 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;
+ }
+
+ setup(data) {
+ /**
+ * The ID of the role (unique to the guild it is part of)
+ * @type {string}
+ */
+ this.id = data.id;
+ /**
+ * The name of the role
+ * @type {string}
+ */
+ this.name = data.name;
+ /**
+ * The base 10 color of the role
+ * @type {number}
+ */
+ this.color = data.color;
+ /**
+ * If true, users that are part of this role will appear in a separate category in the users list
+ * @type {boolean}
+ */
+ this.hoist = data.hoist;
+ /**
+ * The position of the role in the role manager
+ * @type {number}
+ */
+ this.position = data.position;
+ /**
+ * The evaluated permissions number
+ * @type {number}
+ */
+ this.permissions = data.permissions;
+ /**
+ * Whether or not the role is managed by an external service
+ * @type {boolean}
+ */
+ this.managed = data.managed;
+ }
+
+ /**
+ * Deletes the role
+ * @returns {Promise}
+ * @example
+ * // delete a role
+ * role.delete()
+ * .then(r => console.log(`Deleted role ${r}`))
+ * .catch(console.log);
+ */
+ delete() {
+ return this.client.rest.methods.deleteGuildRole(this);
+ }
+
+ /**
+ * Edits the role
+ * @param {RoleData} data The new data for the role
+ * @returns {Promise}
+ * @example
+ * // edit a role
+ * role.edit({name: 'new role'})
+ * .then(r => console.log(`Edited role ${r}`))
+ * .catch(console.log);
+ */
+ edit(data) {
+ return this.client.rest.methods.updateGuildRole(this, data);
+ }
+
+ /**
+ * Set a new name for the role
+ * @param {string} name The new name of the role
+ * @returns {Promise}
+ * @example
+ * // set the name of the role
+ * role.setName('new role')
+ * .then(r => console.log(`Edited name of role ${r}`))
+ * .catch(console.log);
+ */
+ setName(name) {
+ return this.client.rest.methods.updateGuildRole(this, { name });
+ }
+
+ /**
+ * Set a new color for the role
+ * @param {number|string} color The new color for the role, either a hex string or a base 10 number
+ * @returns {Promise}
+ * @example
+ * // set the color of a role
+ * role.setColor('#FF0000')
+ * .then(r => console.log(`Set color of role ${r}`))
+ * .catch(console.log);
+ */
+ setColor(color) {
+ return this.client.rest.methods.updateGuildRole(this, { color });
+ }
+
+ /**
+ * Set whether or not the role should be hoisted
+ * @param {boolean} hoist Whether or not to hoist the role
+ * @returns {Promise}
+ * @example
+ * // set the hoist of the role
+ * role.setHoist(true)
+ * .then(r => console.log(`Role hoisted: ${r.hoist}`))
+ * .catch(console.log);
+ */
+ setHoist(hoist) {
+ return this.client.rest.methods.updateGuildRole(this, { hoist });
+ }
+
+ /**
+ * Set the position of the role
+ * @param {number} position The position of the role
+ * @returns {Promise}
+ * @example
+ * // set the position of the role
+ * role.setPosition(1)
+ * .then(r => console.log(`Role position: ${r.position}`))
+ * .catch(console.log);
+ */
+ setPosition(position) {
+ return this.client.rest.methods.updateGuildRole(this, { position });
+ }
+
+ /**
+ * Set the permissions of the role
+ * @param {string[]} permissions The permissions of the role
+ * @returns {Promise}
+ * @example
+ * // set the permissions of the role
+ * role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS'])
+ * .then(r => console.log(`Role updated ${r}`))
+ * .catch(console.log);
+ */
+ setPermissions(permissions) {
+ return this.client.rest.methods.updateGuildRole(this, { permissions });
+ }
+
+ /**
+ * Get an object mapping permission names to whether or not the role enables that permission
+ * @returns {Object}
+ * @example
+ * // print the serialized role
+ * console.log(role.serialize());
+ */
+ serialize() {
+ const serializedPermissions = {};
+ for (const permissionName in Constants.PermissionFlags) {
+ serializedPermissions[permissionName] = this.hasPermission(permissionName);
+ }
+ return serializedPermissions;
+ }
+
+ /**
+ * Whether or not the role includes the given permission
+ * @param {PermissionResolvable} permission The name of the permission to test
+ * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission
+ * @returns {boolean}
+ * @example
+ * // see if a role can ban a member
+ * if (role.hasPermission('BAN_MEMBERS')) {
+ * console.log('This role can ban members');
+ * } else {
+ * console.log('This role can\'t ban members');
+ * }
+ */
+ hasPermission(permission, explicit = false) {
+ permission = this.client.resolver.resolvePermission(permission);
+ if (!explicit && (this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true;
+ return (this.permissions & permission) > 0;
+ }
+
+ /**
+ * When concatenated with a string, this automatically concatenates the Role mention rather than the Role object.
+ * @returns {string}
+ */
+ toString() {
+ return `<@&${this.id}>`;
+ }
+
+ /**
+ * 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}`;
+ }
+}
+
+module.exports = Role;
diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js
new file mode 100644
index 000000000..671ee9aac
--- /dev/null
+++ b/src/structures/TextChannel.js
@@ -0,0 +1,81 @@
+const GuildChannel = require('./GuildChannel');
+const TextBasedChannel = require('./interface/TextBasedChannel');
+const Collection = require('../util/Collection');
+
+/**
+ * Represents a Server Text Channel on Discord.
+ * @extends {GuildChannel}
+ * @implements {TextBasedChannel}
+ */
+class TextChannel extends GuildChannel {
+ constructor(guild, data) {
+ super(guild, data);
+ this.messages = new Collection();
+ }
+
+ setup(data) {
+ super.setup(data);
+ /**
+ * The ID of the last message in the channel, if one was sent.
+ * @type {?string}
+ */
+ this.lastMessageID = data.last_message_id;
+ this.type = 'text';
+ }
+
+ sendMessage() {
+ return;
+ }
+
+ sendTTSMessage() {
+ return;
+ }
+
+ sendFile() {
+ return;
+ }
+
+ _cacheMessage() {
+ return;
+ }
+
+ fetchMessages() {
+ return;
+ }
+
+ bulkDelete() {
+ return;
+ }
+
+ startTyping() {
+ return;
+ }
+
+ stopTyping() {
+ return;
+ }
+
+ get typing() {
+ return;
+ }
+
+ get typingCount() {
+ return;
+ }
+
+ fetchPinnedMessages() {
+ return;
+ }
+
+ createCollector() {
+ return;
+ }
+
+ awaitMessages() {
+ return;
+ }
+}
+
+TextBasedChannel.applyToClass(TextChannel, true);
+
+module.exports = TextChannel;
diff --git a/src/structures/User.js b/src/structures/User.js
new file mode 100644
index 000000000..53a0926c8
--- /dev/null
+++ b/src/structures/User.js
@@ -0,0 +1,122 @@
+const TextBasedChannel = require('./interface/TextBasedChannel');
+const Constants = require('../util/Constants');
+
+/**
+ * Represents a User on Discord.
+ * @implements {TextBasedChannel}
+ */
+class User {
+ constructor(client, data) {
+ this.client = client;
+ if (data) this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The username of the User
+ * @type {string}
+ */
+ this.username = data.username;
+ /**
+ * The ID of the User
+ * @type {string}
+ */
+ this.id = data.id;
+ /**
+ * A discriminator based on username for the User
+ * @type {string}
+ */
+ this.discriminator = data.discriminator;
+ /**
+ * The ID of the user's avatar
+ * @type {string}
+ */
+ this.avatar = data.avatar;
+ /**
+ * Whether or not the User is a Bot.
+ * @type {boolean}
+ */
+ this.bot = Boolean(data.bot);
+ /**
+ * The status of the user:
+ *
+ * * **`online`** - user is online
+ * * **`offline`** - user is offline
+ * * **`idle`** - user is AFK
+ * @type {string}
+ */
+ this.status = data.status || this.status || 'offline';
+ /**
+ * The game that the user is playing, `null` if they aren't playing a game.
+ * @type {string}
+ */
+ this.game = data.game || this.game;
+ }
+
+ /**
+ * 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}>`;
+ }
+
+ /**
+ * A link to the user's avatar (if they have one, otherwise null)
+ * @type {?string}
+ * @readonly
+ */
+ get avatarURL() {
+ if (!this.avatar) return null;
+ return Constants.Endpoints.avatar(this.id, this.avatar);
+ }
+
+ /**
+ * Deletes a DM Channel (if one exists) between the Client and the User. Resolves with the Channel if successful.
+ * @returns {Promise}
+ */
+ deleteDM() {
+ return this.client.rest.methods.deleteChannel(this);
+ }
+
+ /**
+ * Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
+ * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
+ * @param {User} user The user to compare
+ * @returns {boolean}
+ */
+ equals(user) {
+ let equal = user &&
+ this.username === user.username &&
+ this.id === user.id &&
+ this.discriminator === user.discriminator &&
+ this.avatar === user.avatar &&
+ this.bot === Boolean(user.bot);
+
+ if (equal) {
+ if (user.status) equal = this.status === user.status;
+ if (equal && user.game) equal = this.game === user.game;
+ }
+
+ return equal;
+ }
+
+ sendMessage() {
+ return;
+ }
+
+ sendTTSMessage() {
+ return;
+ }
+
+ sendFile() {
+ return;
+ }
+}
+
+TextBasedChannel.applyToClass(User);
+
+module.exports = User;
diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js
new file mode 100644
index 000000000..b5174c3d6
--- /dev/null
+++ b/src/structures/VoiceChannel.js
@@ -0,0 +1,72 @@
+const GuildChannel = require('./GuildChannel');
+const Collection = require('../util/Collection');
+
+/**
+ * Represents a Server Voice Channel on Discord.
+ * @extends {GuildChannel}
+ */
+class VoiceChannel extends GuildChannel {
+ constructor(guild, data) {
+ super(guild, data);
+ /**
+ * The members in this Voice Channel.
+ * @type {Collection}
+ */
+ this.members = new Collection();
+ }
+
+ setup(data) {
+ super.setup(data);
+ /**
+ * The bitrate of this voice channel
+ * @type {number}
+ */
+ this.bitrate = data.bitrate;
+ /**
+ * The maximum amount of users allowed in this channel - 0 means unlimited.
+ * @type {number}
+ */
+ this.userLimit = data.user_limit;
+ this.type = 'voice';
+ }
+
+ /**
+ * Sets the bitrate of the channel
+ * @param {number} bitrate The new bitrate
+ * @returns {Promise}
+ * @example
+ * // set the bitrate of a voice channel
+ * voiceChannel.setBitrate(48000)
+ * .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`))
+ * .catch(console.log);
+ */
+ setBitrate(bitrate) {
+ return this.rest.client.rest.methods.updateChannel(this, { bitrate });
+ }
+
+ /**
+ * Attempts to join this Voice Channel
+ * @returns {Promise}
+ * @example
+ * // join a voice channel
+ * voiceChannel.join()
+ * .then(connection => console.log('Connected!'))
+ * .catch(console.log);
+ */
+ join() {
+ return this.client.voice.joinChannel(this);
+ }
+
+ /**
+ * Leaves this voice channel
+ * @example
+ * // leave a voice channel
+ * voiceChannel.leave();
+ */
+ leave() {
+ 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/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js
new file mode 100644
index 000000000..17cfa92fb
--- /dev/null
+++ b/src/structures/interface/TextBasedChannel.js
@@ -0,0 +1,411 @@
+const path = require('path');
+const EventEmitter = require('events').EventEmitter;
+const Message = require('../Message');
+const Collection = require('../../util/Collection');
+
+/**
+ * Interface for classes that have text-channel-like features
+ * @interface
+ */
+class TextBasedChannel {
+ constructor() {
+ /**
+ * A Collection containing the messages sent to this channel.
+ * @type {Collection}
+ */
+ this.messages = new Collection();
+ }
+
+ /**
+ * Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after.
+ * @param {Collection|Message[]} messages The messages to delete
+ * @returns {Collection}
+ */
+ bulkDelete(messages) {
+ if (messages instanceof Collection) messages = messages.array();
+ if (!(messages instanceof Array)) return Promise.reject(new TypeError('messages must be an array or collection'));
+ const messageIDs = messages.map(m => m.id);
+ return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
+ }
+
+ /**
+ * Options that can be passed into sendMessage or sendTTSMessage:
+ * ```js
+ * {
+ * tts: false,
+ * nonce: '',
+ * };
+ * ```
+ * @typedef {Object} MessageOptions
+ */
+
+ /**
+ * Send a message to this channel
+ * @param {string} content The content to send
+ * @param {MessageOptions} [options={}] The options to provide
+ * @returns {Promise}
+ * @example
+ * // send a message
+ * channel.sendMessage('hello!')
+ * .then(message => console.log(`Sent message: ${message.content}`))
+ * .catch(console.log);
+ */
+ sendMessage(content, options = {}) {
+ return this.client.rest.methods.sendMessage(this, content, options.tts, options.nonce);
+ }
+
+ /**
+ * Send a text-to-speech message to this channel
+ * @param {string} content The content to send
+ * @param {MessageOptions} [options={}] The options to provide
+ * @returns {Promise}
+ * @example
+ * // send a TTS message
+ * channel.sendTTSMessage('hello!')
+ * .then(message => console.log(`Sent tts message: ${message.content}`))
+ * .catch(console.log);
+ */
+ sendTTSMessage(content, options = {}) {
+ return this.client.rest.methods.sendMessage(this, content, true, options.nonce);
+ }
+
+ /**
+ * Send a file to this channel
+ * @param {FileResolvable} attachment The file to send
+ * @param {string} [fileName="file.jpg"] The name and extension of the file
+ * @returns {Promise}
+ */
+ sendFile(attachment, fileName) {
+ if (!fileName) {
+ if (typeof attachment === 'string') {
+ fileName = path.basename(attachment);
+ } else if (attachment && attachment.path) {
+ fileName = path.basename(attachment.path);
+ } else {
+ fileName = 'file.jpg';
+ }
+ }
+ return new Promise((resolve, reject) => {
+ this.client.resolver.resolveFile(attachment).then(file => {
+ this.client.rest.methods.sendMessage(this, undefined, false, undefined, {
+ file,
+ name: fileName,
+ }).then(resolve).catch(reject);
+ }).catch(reject);
+ });
+ }
+
+ /**
+ * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and
+ * `after` are mutually exclusive. All the parameters are optional.
+ * ```js
+ * {
+ * limit: 30, // the message limit, defaults to 50
+ * before: '123', // gets messages before the given message ID
+ * after: '123', // gets messages after the given message ID
+ * around: '123', // gets messages around the given message ID
+ * }
+ * ```
+ * @typedef {Object} ChannelLogsQueryOptions
+ */
+
+ /**
+ * Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects.
+ * @param {ChannelLogsQueryOptions} [options={}] The query parameters to pass in
+ * @returns {Promise>}
+ * @example
+ * // get messages
+ * channel.fetchMessages({limit: 10})
+ * .then(messages => console.log(`Received ${messages.size} messages`))
+ * .catch(console.log);
+ */
+ fetchMessages(options = {}) {
+ return new Promise((resolve, reject) => {
+ 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);
+ }
+ resolve(messages);
+ }).catch(reject);
+ });
+ }
+
+ /**
+ * Starts a typing indicator in the channel.
+ * @param {number} [count] The number of times startTyping should be considered to have been called
+ * @example
+ * // start typing in a channel
+ * channel.startTyping();
+ */
+ startTyping(count) {
+ if (typeof count !== 'undefined' && count < 1) throw new RangeError('count must be at least 1');
+ if (!this.client.user._typing.has(this.id)) {
+ this.client.user._typing.set(this.id, {
+ count: count || 1,
+ interval: this.client.setInterval(() => {
+ this.client.rest.methods.sendTyping(this.id);
+ }, 4000),
+ });
+ this.client.rest.methods.sendTyping(this.id);
+ } else {
+ const entry = this.client.user._typing.get(this.id);
+ entry.count = count || entry.count + 1;
+ }
+ }
+
+ /**
+ * Stops the typing indicator in the channel.
+ * The indicator will only stop if this is called as many times as startTyping().
+ * It can take a few seconds for the Client User to stop typing.
+ * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop
+ * @example
+ * // stop typing in a channel
+ * channel.stopTyping();
+ * @example
+ * // force typing to fully stop in a channel
+ * channel.stopTyping(true);
+ */
+ stopTyping(force = false) {
+ if (this.client.user._typing.has(this.id)) {
+ const entry = this.client.user._typing.get(this.id);
+ entry.count--;
+ if (entry.count <= 0 || force) {
+ clearInterval(entry.interval);
+ this.client.user._typing.delete(this.id);
+ }
+ }
+ }
+
+ /**
+ * Whether or not the typing indicator is being shown in the channel.
+ * @type {boolean}
+ */
+ get typing() {
+ return this.client.user._typing.has(this.id);
+ }
+
+ /**
+ * Number of times `startTyping` has been called.
+ * @type {number}
+ */
+ get typingCount() {
+ if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count;
+ return 0;
+ }
+
+ /**
+ * Creates a Message Collector
+ * @param {CollectorFilterFunction} filter The filter to create the collector with
+ * @param {CollectorOptions} [options={}] The options to pass to the collector
+ * @returns {MessageCollector}
+ * @example
+ * // create a message collector
+ * const collector = channel.createCollector(
+ * m => m.content.includes('discord'),
+ * { time: 15000 }
+ * );
+ * collector.on('message', m => console.log(`Collected ${m.content}`));
+ * collector.on('end', collected => console.log(`Collected ${collected.size} items`));
+ */
+ createCollector(filter, options = {}) {
+ return new MessageCollector(this, filter, options);
+ }
+
+ /**
+ * An object containing the same properties as CollectorOptions, but a few more:
+ * ```js
+ * {
+ * errors: [], // an array of stop/end reasons that cause the promise to reject.
+ * }
+ * ```
+ * @typedef {Object} AwaitMessagesOptions
+ */
+
+ /**
+ * Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified
+ * filter.
+ * @param {CollectorFilterFunction} filter The filter function to use
+ * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector
+ * @returns {Promise>}
+ * @example
+ * // await !vote messages
+ * const filter = m => m.content.startsWith('!vote');
+ * // errors: ['time'] treats ending because of the time limit as an error
+ * channel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] })
+ * .then(collected => console.log(collected.size))
+ * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));
+ */
+ awaitMessages(filter, options = {}) {
+ return new Promise((resolve, reject) => {
+ const collector = this.createCollector(filter, options);
+ collector.on('end', (collection, reason) => {
+ if (options.errors && options.errors.includes(reason)) {
+ reject(collection);
+ } else {
+ resolve(collection);
+ }
+ });
+ });
+ }
+
+ _cacheMessage(message) {
+ const maxSize = this.client.options.max_message_cache;
+ if (maxSize === 0) return null;
+ if (this.messages.size >= maxSize) this.messages.delete(this.messages.keys().next().value);
+
+ this.messages.set(message.id, message);
+ return message;
+ }
+
+ /**
+ * Fetches the pinned messages of this Channel and returns a Collection of them.
+ * @returns {Promise>}
+ */
+ fetchPinnedMessages() {
+ return new Promise((resolve, reject) => {
+ 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);
+ }
+ resolve(messages);
+ }).catch(reject);
+ });
+ }
+}
+
+/**
+ * Collects messages based on a specified filter, then emits them.
+ * @extends {EventEmitter}
+ */
+class MessageCollector extends EventEmitter {
+ /**
+ * A function that takes a Message object and a MessageCollector and returns a boolean.
+ * ```js
+ * function(message, collector) {
+ * if (message.content.includes('discord')) {
+ * return true; // passed the filter test
+ * }
+ * return false; // failed the filter test
+ * }
+ * ```
+ * @typedef {function} CollectorFilterFunction
+ */
+
+ /**
+ * An object containing options used to configure a MessageCollector. All properties are optional.
+ * ```js
+ * {
+ * time: null, // time in milliseconds. If specified, the collector ends after this amount of time.
+ * max: null, // the maximum amount of messages to handle before ending.
+ * }
+ * ```
+ * @typedef {Object} CollectorOptions
+ */
+
+ /**
+ * @param {Channel} channel The channel to collect messages in
+ * @param {CollectorFilterFunction} filter The filter function
+ * @param {CollectorOptions} [options] Options for the collector
+ */
+ constructor(channel, filter, options = {}) {
+ super();
+ /**
+ * The channel this collector is operating on
+ * @type {Channel}
+ */
+ this.channel = channel;
+ /**
+ * A function used to filter messages that the collector collects.
+ * @type {CollectorFilterFunction}
+ */
+ this.filter = filter;
+ /**
+ * Options for the collecor.
+ * @type {CollectorOptions}
+ */
+ this.options = options;
+ /**
+ * Whether this collector has stopped collecting Messages.
+ * @type {boolean}
+ */
+ this.ended = false;
+ this.listener = message => this.verify(message);
+ this.channel.client.on('message', this.listener);
+ /**
+ * A collection of collected messages, mapped by message ID.
+ * @type {Collection}
+ */
+ this.collected = new Collection();
+ if (options.time) this.channel.client.setTimeout(() => this.stop('time'), options.time);
+ }
+
+ /**
+ * Verifies a message against the filter and options
+ * @private
+ * @param {Message} message The message
+ * @returns {boolean}
+ */
+ verify(message) {
+ if (this.channel ? this.channel.id !== message.channel.id : false) return false;
+ if (this.filter(message, this)) {
+ this.collected.set(message.id, message);
+ /**
+ * Emitted whenever the Collector receives a Message that passes the filter test.
+ * @param {Message} message The received message
+ * @param {MessageCollector} collector The collector the message passed through
+ * @event MessageCollector#message
+ */
+ this.emit('message', message, this);
+ if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Stops the collector and emits `end`.
+ * @param {string} [reason='user'] An optional reason for stopping the collector
+ */
+ stop(reason = 'user') {
+ if (this.ended) return;
+ this.ended = true;
+ this.channel.client.removeListener('message', this.listener);
+ /**
+ * Emitted when the Collector stops collecting.
+ * @param {Collection} collection A collection of messages collected
+ * during the lifetime of the Collector, mapped by the ID of the Messages.
+ * @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time
+ * limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it
+ * ended because it reached its message limit, it will be `limit`.
+ * @event MessageCollector#end
+ */
+ this.emit('end', this.collected, reason);
+ }
+}
+
+exports.applyToClass = (structure, full = false) => {
+ const props = ['sendMessage', 'sendTTSMessage', 'sendFile'];
+ if (full) {
+ props.push('_cacheMessage');
+ props.push('fetchMessages');
+ props.push('bulkDelete');
+ props.push('startTyping');
+ props.push('stopTyping');
+ props.push('typing');
+ props.push('typingCount');
+ props.push('fetchPinnedMessages');
+ props.push('createCollector');
+ props.push('awaitMessages');
+ }
+ for (const prop of props) applyProp(structure, prop);
+};
+
+function applyProp(structure, prop) {
+ Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop));
+}
diff --git a/src/util/ArraysEqual.js b/src/util/ArraysEqual.js
new file mode 100644
index 000000000..efd8275c9
--- /dev/null
+++ b/src/util/ArraysEqual.js
@@ -0,0 +1,14 @@
+module.exports = function arraysEqual(a, b) {
+ if (a === b) return true;
+ if (a.length !== b.length) return false;
+
+ for (const itemInd in a) {
+ const item = a[itemInd];
+ const ind = b.indexOf(item);
+ if (ind) {
+ b.splice(ind, 1);
+ }
+ }
+
+ return b.length === 0;
+};
diff --git a/src/util/CloneObject.js b/src/util/CloneObject.js
new file mode 100644
index 000000000..13366a7b7
--- /dev/null
+++ b/src/util/CloneObject.js
@@ -0,0 +1,5 @@
+module.exports = function cloneObject(obj) {
+ const cloned = Object.create(obj);
+ Object.assign(cloned, obj);
+ return cloned;
+};
diff --git a/src/util/Collection.js b/src/util/Collection.js
new file mode 100644
index 000000000..3d9726344
--- /dev/null
+++ b/src/util/Collection.js
@@ -0,0 +1,136 @@
+/**
+ * A utility class to help make it easier to access the data stores
+ * @extends {Map}
+ */
+class Collection extends Map {
+ /**
+ * Returns an ordered array of the values of this collection.
+ * @returns {array}
+ * @example
+ * // identical to:
+ * Array.from(collection.values());
+ */
+ array() {
+ return Array.from(this.values());
+ }
+
+ /**
+ * Returns the first item in this collection.
+ * @returns {*}
+ * @example
+ * // identical to:
+ * Array.from(collection.values())[0];
+ */
+ first() {
+ return this.values().next().value;
+ }
+
+ /**
+ * Returns the last item in this collection. This is a relatively slow operation,
+ * since an array copy of the values must be made to find the last element.
+ * @returns {*}
+ */
+ last() {
+ const arr = this.array();
+ return arr[arr.length - 1];
+ }
+
+ /**
+ * Returns a random item from this collection. This is a relatively slow operation,
+ * since an array copy of the values must be made to find a random element.
+ * @returns {*}
+ */
+ random() {
+ const arr = this.array();
+ return arr[Math.floor(Math.random() * arr.length)];
+ }
+
+ /**
+ * If the items in this collection have a delete method (e.g. messages), invoke
+ * the delete method. Returns an array of promises
+ * @returns {Promise[]}
+ */
+ deleteAll() {
+ const returns = [];
+ for (const item of this.values()) {
+ if (item.delete) returns.push(item.delete());
+ }
+ return returns;
+ }
+
+ /**
+ * Returns an array of items where `item[key] === value` of the collection
+ * @param {string} key The key to filter by
+ * @param {*} value The expected value
+ * @returns {array}
+ * @example
+ * collection.getAll('username', 'Bob');
+ */
+ findAll(key, value) {
+ if (typeof key !== '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[key] === value) results.push(item);
+ }
+ return results;
+ }
+
+ /**
+ * Returns a single item where `item[key] === value`
+ * @param {string} key The key to filter by
+ * @param {*} value The expected value
+ * @returns {*}
+ * @example
+ * collection.get('id', '123123...');
+ */
+ find(key, value) {
+ if (typeof key !== 'string') throw new TypeError('key must be a string');
+ if (typeof value === 'undefined') throw new Error('value must be specified');
+ for (const item of this.values()) {
+ if (item[key] === value) return item;
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the collection has an item where `item[key] === value`
+ * @param {string} key The key to filter by
+ * @param {*} value The expected value
+ * @returns {boolean}
+ * @example
+ * if (collection.exists('id', '123123...')) {
+ * console.log('user here!');
+ * }
+ */
+ exists(key, value) {
+ return Boolean(this.find(key, value));
+ }
+
+ /**
+ * Identical to
+ * [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter),
+ * but returns a Collection instead of an Array.
+ * @param {function} callback Function used to filter (should return a boolean)
+ * @param {Object} [thisArg] Value to set as this when filtering
+ * @returns {Collection}
+ */
+ filter(...args) {
+ const newArray = this.array().filter(...args);
+ const collection = new Collection();
+ for (const item of newArray) collection.set(item.id, item);
+ return collection;
+ }
+
+ /**
+ * Functionally identical shortcut to `collection.array().map(...)`.
+ * @param {function} callback Function that produces an element of the new Array, taking three arguments
+ * @param {*} [thisArg] Optional. Value to use as this when executing callback.
+ * @returns {array}
+ */
+ map(...args) {
+ return this.array().map(...args);
+ }
+}
+
+module.exports = Collection;
diff --git a/src/util/Constants.js b/src/util/Constants.js
new file mode 100644
index 000000000..6cff27244
--- /dev/null
+++ b/src/util/Constants.js
@@ -0,0 +1,242 @@
+/**
+ * Options that can be passed to a client:
+ * ```js
+ * {
+ * ws: {
+ * large_threshold: 250,
+ * compress: true,
+ * properties: {
+ * $os: process ? process.platform : 'discord.js',
+ * $browser: 'discord.js',
+ * $device: 'discord.js',
+ * $referrer: '',
+ * $referring_domain: '',
+ * },
+ * },
+ * protocol_version: 6,
+ * max_message_cache: 200,
+ * rest_ws_bridge_timeout: 5000,
+ * api_request_method: 'sequential',
+ * shard_id: 0,
+ * shard_count: 0,
+ * fetch_all_members: false,
+ * };
+ * ```
+ * @typedef {Object} ClientOptions
+ */
+exports.DefaultOptions = {
+ ws: {
+ large_threshold: 250,
+ compress: true,
+ properties: {
+ $os: process ? process.platform : 'discord.js',
+ $browser: 'discord.js',
+ $device: 'discord.js',
+ $referrer: '',
+ $referring_domain: '',
+ },
+ },
+ protocol_version: 6,
+ max_message_cache: 800,
+ rest_ws_bridge_timeout: 5000,
+ api_request_method: 'sequential',
+ shard_id: 0,
+ shard_count: 0,
+ fetch_all_members: false,
+};
+
+exports.Status = {
+ READY: 0,
+ CONNECTING: 1,
+ RECONNECTING: 2,
+ IDLE: 3,
+ NEARLY: 4,
+};
+
+exports.ChannelTypes = {
+ text: 0,
+ DM: 1,
+ voice: 2,
+ groupDM: 3,
+};
+
+exports.Package = require('../../package.json');
+
+exports.Errors = {
+ NO_TOKEN: 'request to use token, but token was unavailable to the client',
+ NO_BOT_ACCOUNT: 'you should ideally be using a bot account!',
+ BAD_WS_MESSAGE: 'a bad message was received from the websocket - bad compression or not json',
+ TOOK_TOO_LONG: 'something took too long to do',
+ NOT_A_PERMISSION: 'that is not a valid permission string or number',
+ INVALID_RATE_LIMIT_METHOD: 'unknown rate limiting method',
+ BAD_LOGIN: 'incorrect login details were provided',
+};
+
+const API = `https://discordapp.com/api/v${exports.DefaultOptions.protocol_version}`;
+
+const Endpoints = exports.Endpoints = {
+ // general endpoints
+ login: `${API}/auth/login`,
+ logout: `${API}/auth/logout`,
+ gateway: `${API}/gateway`,
+ invite: (id) => `${API}/invite/${id}`,
+ CDN: 'https://cdn.discordapp.com',
+
+ // users
+ user: (userID) => `${API}/users/${userID}`,
+ userChannels: (userID) => `${Endpoints.user(userID)}/channels`,
+ avatar: (userID, avatar) => `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
+ me: `${API}/users/@me`,
+ meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
+
+ // guilds
+ guilds: `${API}/guilds`,
+ guild: (guildID) => `${Endpoints.guilds}/${guildID}`,
+ guildIcon: (guildID, hash) => `${Endpoints.guild(guildID)}/icons/${hash}.jpg`,
+ guildPrune: (guildID) => `${Endpoints.guild(guildID)}/prune`,
+ guildEmbed: (guildID) => `${Endpoints.guild(guildID)}/embed`,
+ guildInvites: (guildID) => `${Endpoints.guild(guildID)}/invites`,
+ guildRoles: (guildID) => `${Endpoints.guild(guildID)}/roles`,
+ guildRole: (guildID, roleID) => `${Endpoints.guildRoles(guildID)}/${roleID}`,
+ guildBans: (guildID) => `${Endpoints.guild(guildID)}/bans`,
+ guildIntegrations: (guildID) => `${Endpoints.guild(guildID)}/integrations`,
+ guildMembers: (guildID) => `${Endpoints.guild(guildID)}/members`,
+ guildMember: (guildID, memberID) => `${Endpoints.guildMembers(guildID)}/${memberID}`,
+ stupidInconsistentGuildEndpoint: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`,
+ guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`,
+
+ // channels
+ channels: `${API}/channels`,
+ channel: (channelID) => `${Endpoints.channels}/${channelID}`,
+ channelMessages: (channelID) => `${Endpoints.channel(channelID)}/messages`,
+ channelInvites: (channelID) => `${Endpoints.channel(channelID)}/invites`,
+ channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`,
+ channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`,
+ channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`,
+};
+
+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,
+};
+
+exports.VoiceOPCodes = {
+ IDENTIFY: 0,
+ SELECT_PROTOCOL: 1,
+ READY: 2,
+ HEARTBEAT: 3,
+ SESSION_DESCRIPTION: 4,
+ SPEAKING: 5,
+};
+
+exports.Events = {
+ READY: 'ready',
+ GUILD_CREATE: 'guildCreate',
+ GUILD_DELETE: 'guildDelete',
+ GUILD_UNAVAILABLE: 'guildUnavailable',
+ GUILD_AVAILABLE: 'guildAvailable',
+ GUILD_UPDATE: 'guildUpdate',
+ GUILD_BAN_ADD: 'guildBanAdd',
+ GUILD_BAN_REMOVE: 'guildBanRemove',
+ GUILD_MEMBER_ADD: 'guildMemberAdd',
+ GUILD_MEMBER_REMOVE: 'guildMemberRemove',
+ GUILD_MEMBER_UPDATE: 'guildMemberUpdate',
+ GUILD_ROLE_CREATE: 'guildRoleCreate',
+ GUILD_ROLE_DELETE: 'guildRoleDelete',
+ GUILD_ROLE_UPDATE: 'guildRoleUpdate',
+ GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable',
+ CHANNEL_CREATE: 'channelCreate',
+ CHANNEL_DELETE: 'channelDelete',
+ CHANNEL_UPDATE: 'channelUpdate',
+ PRESENCE_UPDATE: 'presenceUpdate',
+ USER_UPDATE: 'userUpdate',
+ VOICE_STATE_UPDATE: 'voiceStateUpdate',
+ TYPING_START: 'typingStart',
+ TYPING_STOP: 'typingStop',
+ WARN: 'warn',
+ GUILD_MEMBERS_CHUNK: 'guildMembersChunk',
+ MESSAGE_CREATE: 'message',
+ MESSAGE_DELETE: 'messageDelete',
+ MESSAGE_UPDATE: 'messageUpdate',
+ RECONNECTING: 'reconnecting',
+ GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking',
+ MESSAGE_BULK_DELETE: 'messageDeleteBulk',
+ CHANNEL_PINS_UPDATE: 'channelPinsUpdate',
+};
+
+exports.WSEvents = {
+ CHANNEL_CREATE: 'CHANNEL_CREATE',
+ CHANNEL_DELETE: 'CHANNEL_DELETE',
+ CHANNEL_UPDATE: 'CHANNEL_UPDATE',
+ MESSAGE_CREATE: 'MESSAGE_CREATE',
+ MESSAGE_DELETE: 'MESSAGE_DELETE',
+ MESSAGE_UPDATE: 'MESSAGE_UPDATE',
+ PRESENCE_UPDATE: 'PRESENCE_UPDATE',
+ READY: 'READY',
+ GUILD_BAN_ADD: 'GUILD_BAN_ADD',
+ GUILD_BAN_REMOVE: 'GUILD_BAN_REMOVE',
+ GUILD_CREATE: 'GUILD_CREATE',
+ GUILD_DELETE: 'GUILD_DELETE',
+ GUILD_MEMBER_ADD: 'GUILD_MEMBER_ADD',
+ GUILD_MEMBER_REMOVE: 'GUILD_MEMBER_REMOVE',
+ GUILD_MEMBER_UPDATE: 'GUILD_MEMBER_UPDATE',
+ GUILD_MEMBERS_CHUNK: 'GUILD_MEMBERS_CHUNK',
+ GUILD_ROLE_CREATE: 'GUILD_ROLE_CREATE',
+ GUILD_ROLE_DELETE: 'GUILD_ROLE_DELETE',
+ GUILD_ROLE_UPDATE: 'GUILD_ROLE_UPDATE',
+ GUILD_UPDATE: 'GUILD_UPDATE',
+ TYPING_START: 'TYPING_START',
+ USER_UPDATE: 'USER_UPDATE',
+ VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE',
+ FRIEND_ADD: 'RELATIONSHIP_ADD',
+ FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
+ VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
+ MESSAGE_DELETE_BULK: 'MESSAGE_DELETE_BULK',
+ CHANNEL_PINS_UPDATE: 'CHANNEL_PINS_UPDATE',
+ GUILD_SYNC: 'GUILD_SYNC',
+};
+
+const PermissionFlags = exports.PermissionFlags = {
+ CREATE_INSTANT_INVITE: 1 << 0,
+ KICK_MEMBERS: 1 << 1,
+ BAN_MEMBERS: 1 << 2,
+ ADMINISTRATOR: 1 << 3,
+ MANAGE_CHANNELS: 1 << 4,
+ MANAGE_GUILD: 1 << 5,
+
+ READ_MESSAGES: 1 << 10,
+ SEND_MESSAGES: 1 << 11,
+ SEND_TTS_MESSAGES: 1 << 12,
+ MANAGE_MESSAGES: 1 << 13,
+ EMBED_LINKS: 1 << 14,
+ ATTACH_FILES: 1 << 15,
+ READ_MESSAGE_HISTORY: 1 << 16,
+ MENTION_EVERYONE: 1 << 17,
+ EXTERNAL_EMOJIS: 1 << 18,
+
+ CONNECT: 1 << 20,
+ SPEAK: 1 << 21,
+ MUTE_MEMBERS: 1 << 22,
+ DEAFEN_MEMBERS: 1 << 23,
+ MOVE_MEMBERS: 1 << 24,
+ USE_VAD: 1 << 25,
+
+ CHANGE_NICKNAME: 1 << 26,
+ MANAGE_NICKNAMES: 1 << 27,
+ MANAGE_ROLES_OR_PERMISSIONS: 1 << 28,
+};
+
+let _ALL_PERMISSIONS = 0;
+for (const key in PermissionFlags) _ALL_PERMISSIONS |= PermissionFlags[key];
+
+exports.ALL_PERMISSIONS = _ALL_PERMISSIONS;
+
+exports.DEFAULT_PERMISSIONS = 36953089;
diff --git a/src/util/MergeDefault.js b/src/util/MergeDefault.js
new file mode 100644
index 000000000..b09f9701b
--- /dev/null
+++ b/src/util/MergeDefault.js
@@ -0,0 +1,12 @@
+module.exports = function merge(def, given) {
+ if (!given) return def;
+ for (const key in def) {
+ if (!{}.hasOwnProperty.call(given, key)) {
+ given[key] = def[key];
+ } else if (given[key] === Object(given[key])) {
+ given[key] = merge(def[key], given[key]);
+ }
+ }
+
+ return given;
+};
diff --git a/test/random.js b/test/random.js
new file mode 100644
index 000000000..14c994917
--- /dev/null
+++ b/test/random.js
@@ -0,0 +1,154 @@
+'use strict';
+
+const Discord = require('../');
+const request = require('superagent');
+const fs = require('fs');
+
+const client = new Discord.Client({ fetch_all_members: false });
+
+const { email, password, token } = require('./auth.json');
+
+client.login(token).then(atoken => console.log('logged in with token ' + atoken)).catch(console.log);
+
+client.on('ready', () => {
+ console.log('ready!');
+});
+
+client.on('debug', console.log);
+
+client.on('message', message => {
+ if (true) {
+ if (message.content === 'makechann') {
+ if (message.channel.guild) {
+ message.channel.guild.createChannel('hi', 'text').then(console.log);
+ }
+ }
+
+ if (message.content === 'myperms?') {
+ message.channel.sendMessage('Your permissions are:\n' +
+ JSON.stringify(message.channel.permissionsFor(message.author).serialize(), null, 4));
+ }
+
+ if (message.content === 'delchann') {
+ message.channel.delete().then(chan => console.log('selfDelChann', chan.name));
+ }
+
+ if (message.content.startsWith('setname')) {
+ message.channel.setName(message.content.substr(8));
+ }
+
+ if (message.content.startsWith('botname')) {
+ client.user.setUsername(message.content.substr(8));
+ }
+
+ if (message.content.startsWith('botavatar')) {
+ request
+ .get('url')
+ .end((err, res) => {
+ client.user.setAvatar(res.body).catch(console.log)
+ .then(user => message.channel.sendMessage('Done!'));
+ });
+ }
+
+ if (message.content.startsWith('gn')) {
+ message.guild.setName(message.content.substr(3))
+ .then(guild => console.log('guild updated to', guild.name))
+ .catch(console.log);
+ }
+
+ if (message.content === 'leave') {
+ message.guild.leave().then(guild => console.log('left guild', guild.name)).catch(console.log);
+ }
+
+ if (message.content === 'stats') {
+ let m = '';
+ m += `I am aware of ${message.guild.channels.size} channels\n`;
+ m += `I am aware of ${message.guild.members.size} members\n`;
+ m += `I am aware of ${client.channels.size} channels overall\n`;
+ m += `I am aware of ${client.guilds.size} guilds overall\n`;
+ m += `I am aware of ${client.users.size} users overall\n`;
+ message.channel.sendMessage(m).then(msg => msg.edit('nah')).catch(console.log);
+ }
+
+ if (message.content === 'messageme!') {
+ message.author.sendMessage('oh, hi there!').catch(e => console.log(e.stack));
+ }
+
+ if (message.content === 'don\'t dm me') {
+ message.author.deleteDM();
+ }
+
+ if (message.content.startsWith('kick')) {
+ message.guild.member(message.mentions[0]).kick().then(member => {
+ console.log(member);
+ message.channel.sendMessage('Kicked!' + member.user.username);
+ }).catch(console.log);
+ }
+
+ if (message.content === 'ratelimittest') {
+ let i = 1;
+ while (i <= 20) {
+ message.channel.sendMessage(`Testing my rates, item ${i} of 20`);
+ i++;
+ }
+ }
+
+ if (message.content === 'makerole') {
+ message.guild.createRole().then(role => {
+ message.channel.sendMessage(`Made role ${role.name}`);
+ }).catch(console.log);
+ }
+ }
+});
+
+function nameLoop(user) {
+ // user.setUsername(user.username + 'a').then(nameLoop).catch(console.log);
+}
+
+function chanLoop(channel) {
+ channel.setName(channel.name + 'a').then(chanLoop).catch(console.log);
+}
+
+client.on('message', msg => {
+ if (msg.content.startsWith('?raw')) {
+ msg.channel.sendMessage('```' + msg.content + '```');
+ }
+
+ if (msg.content.startsWith('#eval') && msg.author.id === '66564597481480192') {
+ try {
+ const com = eval(msg.content.split(" ").slice(1).join(" "));
+ msg.channel.sendMessage('```\n' + com + '```');
+ } catch(e) {
+ msg.channel.sendMessage('```\n' + e + '```');
+ }
+ }
+});
+
+const ytdl = require('ytdl-core');
+
+let disp;
+
+client.on('message', msg => {
+ if (msg.content.startsWith('/join')) {
+ const chan = msg.content.split(' ').slice(1).join(' ');
+ msg.channel.guild.channels.get(chan).join()
+ .then(conn => {
+ msg.reply('done');
+ disp = conn.player.playStream(ytdl('https://www.youtube.com/watch?v=nbXgHAzUWB0', {filter : 'audioonly'}));
+ conn.player.on('debug', console.log);
+ conn.player.on('error', err => console.log(123, err));
+ const receiver = conn.createReceiver();
+ const out = fs.createWriteStream('C:/Users/Amish/Desktop/output.pcm');
+ conn.once('speaking', (user, speaking) => {
+ if (speaking) {
+ msg.reply(`${user.username} start`);
+ const str = receiver.createPCMStream(user);
+ str.pipe(out);
+ str.on('end', () => msg.reply(`${user.username} end`));
+ }
+ });
+ disp.on('error', err => console.log(123, err));
+ })
+ .catch(console.log);
+ }
+})
diff --git a/test/shard.js b/test/shard.js
new file mode 100644
index 000000000..8ae8633ba
--- /dev/null
+++ b/test/shard.js
@@ -0,0 +1,26 @@
+const Discord = require('../');
+const { token } = require('./auth.json');
+
+const client = new Discord.Client({
+ shard_id: process.argv[2],
+ shard_count: process.argv[3],
+});
+
+client.on('message', msg => {
+ if (msg.content.startsWith('?eval') && msg.author.id === '66564597481480192') {
+ try {
+ const com = eval(msg.content.split(' ').slice(1).join(' '));
+ msg.channel.sendMessage('```\n' + com + '```');
+ } catch (e) {
+ msg.channel.sendMessage('```\n' + e + '```');
+ }
+ }
+});
+
+process.send(123);
+
+client.on('ready', () => {
+ console.log('Ready');
+});
+
+client.login(token).catch(console.log);
diff --git a/test/sharder.js b/test/sharder.js
new file mode 100644
index 000000000..5a1340620
--- /dev/null
+++ b/test/sharder.js
@@ -0,0 +1,7 @@
+const Discord = require('../');
+
+const sharder = new Discord.ShardingManager(`${process.cwd()}/test/shard.js`);
+
+sharder.on('launch', id => console.log(`launched ${id}`));
+
+sharder.spawn(5);