mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 11:33:30 +01:00
Merge branch 'indev-rewrite' into indev
This commit is contained in:
133
.eslintrc.json
Normal file
133
.eslintrc.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -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
|
||||||
10
.jscsrc
Normal file
10
.jscsrc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"preset": "airbnb",
|
||||||
|
"validateIndentation": "\t",
|
||||||
|
"maximumLineLength": 140,
|
||||||
|
"maxErrors": 5000,
|
||||||
|
"disallowMultipleVarDecl": false,
|
||||||
|
"disallowSpacesInsideObjectBrackets": false,
|
||||||
|
"disallowMixedSpacesAndTabs": false,
|
||||||
|
"excludeFiles": []
|
||||||
|
}
|
||||||
7
.travis.yml
Normal file
7
.travis.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- "6"
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- node_modules
|
||||||
|
install: npm install
|
||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -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.
|
||||||
6
README.md
Normal file
6
README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://hydrabolt.github.io/discord.js">
|
||||||
|
<img alt="discord.js" src="http://i.imgur.com/sPOLh9y.png" width="546"><br />
|
||||||
|
</a>
|
||||||
|
<div align="center"><h1><i>REWRITE</i></h1></div>
|
||||||
|
</p>
|
||||||
10
docs/custom/avatar.js
Normal file
10
docs/custom/avatar.js
Normal file
@@ -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')}
|
||||||
|
\`\`\``,
|
||||||
|
};
|
||||||
11
docs/custom/documents/updating.md
Normal file
11
docs/custom/documents/updating.md
Normal file
@@ -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.
|
||||||
16
docs/custom/documents/welcome.md
Normal file
16
docs/custom/documents/welcome.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://hydrabolt.github.io/discord.js">
|
||||||
|
<img alt="discord.js" src="http://i.imgur.com/sPOLh9y.png" width="546">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](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`
|
||||||
30
docs/custom/examples/avatar.js
Normal file
30
docs/custom/examples/avatar.js
Normal file
@@ -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);
|
||||||
30
docs/custom/examples/ping_pong.js
Normal file
30
docs/custom/examples/ping_pong.js
Normal file
@@ -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);
|
||||||
17
docs/custom/index.js
Normal file
17
docs/custom/index.js
Normal file
@@ -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;
|
||||||
10
docs/custom/ping_pong.js
Normal file
10
docs/custom/ping_pong.js
Normal file
@@ -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')}
|
||||||
|
\`\`\``,
|
||||||
|
};
|
||||||
7
docs/custom/updating.js
Normal file
7
docs/custom/updating.js
Normal file
@@ -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'),
|
||||||
|
};
|
||||||
7
docs/custom/welcome.js
Normal file
7
docs/custom/welcome.js
Normal file
@@ -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'),
|
||||||
|
};
|
||||||
1
docs/docs.json
Normal file
1
docs/docs.json
Normal file
File diff suppressed because one or more lines are too long
4
docs/generator/config.json
Normal file
4
docs/generator/config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"GEN_VERSION": 12,
|
||||||
|
"COMPRESS": false
|
||||||
|
}
|
||||||
24
docs/generator/doc-scanner.js
Normal file
24
docs/generator/doc-scanner.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
114
docs/generator/documentation.js
Normal file
114
docs/generator/documentation.js
Normal file
@@ -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;
|
||||||
30
docs/generator/generator.js
Normal file
30
docs/generator/generator.js
Normal file
@@ -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);
|
||||||
83
docs/generator/types/DocumentedClass.js
Normal file
83
docs/generator/types/DocumentedClass.js
Normal file
@@ -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;
|
||||||
46
docs/generator/types/DocumentedConstructor.js
Normal file
46
docs/generator/types/DocumentedConstructor.js
Normal file
@@ -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;
|
||||||
80
docs/generator/types/DocumentedEvent.js
Normal file
80
docs/generator/types/DocumentedEvent.js
Normal file
@@ -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.<Role>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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;
|
||||||
91
docs/generator/types/DocumentedFunction.js
Normal file
91
docs/generator/types/DocumentedFunction.js
Normal file
@@ -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.<Message>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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;
|
||||||
32
docs/generator/types/DocumentedInterface.js
Normal file
32
docs/generator/types/DocumentedInterface.js
Normal file
@@ -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;
|
||||||
17
docs/generator/types/DocumentedItem.js
Normal file
17
docs/generator/types/DocumentedItem.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class DocumentedItem {
|
||||||
|
constructor(parent, info) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.directData = {};
|
||||||
|
this.registerMetaInfo(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMetaInfo() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DocumentedItem;
|
||||||
29
docs/generator/types/DocumentedItemMeta.js
Normal file
29
docs/generator/types/DocumentedItemMeta.js
Normal file
@@ -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;
|
||||||
58
docs/generator/types/DocumentedMember.js
Normal file
58
docs/generator/types/DocumentedMember.js
Normal file
@@ -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;
|
||||||
36
docs/generator/types/DocumentedParam.js
Normal file
36
docs/generator/types/DocumentedParam.js
Normal file
@@ -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;
|
||||||
44
docs/generator/types/DocumentedTypeDef.js
Normal file
44
docs/generator/types/DocumentedTypeDef.js
Normal file
@@ -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;
|
||||||
50
docs/generator/types/DocumentedVarType.js
Normal file
50
docs/generator/types/DocumentedVarType.js
Normal file
@@ -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;
|
||||||
49
package.json
Normal file
49
package.json
Normal file
@@ -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 <amishshah.2k@gmail.com>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
213
src/client/Client.js
Normal file
213
src/client/Client.js
Normal file
@@ -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<string, User>}
|
||||||
|
*/
|
||||||
|
this.users = new Collection();
|
||||||
|
/**
|
||||||
|
* A Collection of the Client's stored guilds
|
||||||
|
* @type {Collection<string, Guild>}
|
||||||
|
*/
|
||||||
|
this.guilds = new Collection();
|
||||||
|
/**
|
||||||
|
* A Collection of the Client's stored channels
|
||||||
|
* @type {Collection<string, Channel>}
|
||||||
|
*/
|
||||||
|
this.channels = new Collection();
|
||||||
|
/**
|
||||||
|
* 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. <warn>If you're making a bot, it's
|
||||||
|
* much better to use a bot account rather than a user account.
|
||||||
|
* Bot accounts have higher rate limits and have access to some features user accounts don't have. User bots
|
||||||
|
* that are making a lot of API requests can even be banned.</warn>
|
||||||
|
* @param {string} 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<string>}
|
||||||
|
* @example
|
||||||
|
* // log the client in using a token
|
||||||
|
* const token = 'my token';
|
||||||
|
* client.login(token);
|
||||||
|
* @example
|
||||||
|
* // log the client in using email and password
|
||||||
|
* const email = 'user@email.com';
|
||||||
|
* const password = 'supersecret123';
|
||||||
|
* client.login(email, password);
|
||||||
|
*/
|
||||||
|
login(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<User>}
|
||||||
|
*/
|
||||||
|
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<string, VoiceConnection>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
98
src/client/ClientDataManager.js
Normal file
98
src/client/ClientDataManager.js
Normal file
@@ -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;
|
||||||
206
src/client/ClientDataResolver.js
Normal file
206
src/client/ClientDataResolver.js
Normal file
@@ -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<Buffer>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
63
src/client/ClientManager.js
Normal file
63
src/client/ClientManager.js
Normal file
@@ -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;
|
||||||
23
src/client/actions/Action.js
Normal file
23
src/client/actions/Action.js
Normal file
@@ -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;
|
||||||
30
src/client/actions/ActionsManager.js
Normal file
30
src/client/actions/ActionsManager.js
Normal file
@@ -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;
|
||||||
13
src/client/actions/ChannelCreate.js
Normal file
13
src/client/actions/ChannelCreate.js
Normal file
@@ -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;
|
||||||
31
src/client/actions/ChannelDelete.js
Normal file
31
src/client/actions/ChannelDelete.js
Normal file
@@ -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;
|
||||||
34
src/client/actions/ChannelUpdate.js
Normal file
34
src/client/actions/ChannelUpdate.js
Normal file
@@ -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;
|
||||||
13
src/client/actions/GuildBanRemove.js
Normal file
13
src/client/actions/GuildBanRemove.js
Normal file
@@ -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;
|
||||||
51
src/client/actions/GuildDelete.js
Normal file
51
src/client/actions/GuildDelete.js
Normal file
@@ -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;
|
||||||
50
src/client/actions/GuildMemberRemove.js
Normal file
50
src/client/actions/GuildMemberRemove.js
Normal file
@@ -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;
|
||||||
33
src/client/actions/GuildRoleCreate.js
Normal file
33
src/client/actions/GuildRoleCreate.js
Normal file
@@ -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;
|
||||||
47
src/client/actions/GuildRoleDelete.js
Normal file
47
src/client/actions/GuildRoleDelete.js
Normal file
@@ -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;
|
||||||
42
src/client/actions/GuildRoleUpdate.js
Normal file
42
src/client/actions/GuildRoleUpdate.js
Normal file
@@ -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;
|
||||||
31
src/client/actions/GuildSync.js
Normal file
31
src/client/actions/GuildSync.js
Normal file
@@ -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;
|
||||||
34
src/client/actions/GuildUpdate.js
Normal file
34
src/client/actions/GuildUpdate.js
Normal file
@@ -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;
|
||||||
22
src/client/actions/MessageCreate.js
Normal file
22
src/client/actions/MessageCreate.js
Normal file
@@ -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;
|
||||||
40
src/client/actions/MessageDelete.js
Normal file
40
src/client/actions/MessageDelete.js
Normal file
@@ -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;
|
||||||
24
src/client/actions/MessageDeleteBulk.js
Normal file
24
src/client/actions/MessageDeleteBulk.js
Normal file
@@ -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;
|
||||||
42
src/client/actions/MessageUpdate.js
Normal file
42
src/client/actions/MessageUpdate.js
Normal file
@@ -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;
|
||||||
13
src/client/actions/UserGet.js
Normal file
13
src/client/actions/UserGet.js
Normal file
@@ -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;
|
||||||
40
src/client/actions/UserUpdate.js
Normal file
40
src/client/actions/UserUpdate.js
Normal file
@@ -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;
|
||||||
40
src/client/rest/APIRequest.js
Normal file
40
src/client/rest/APIRequest.js
Normal file
@@ -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;
|
||||||
48
src/client/rest/RESTManager.js
Normal file
48
src/client/rest/RESTManager.js
Normal file
@@ -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;
|
||||||
443
src/client/rest/RESTMethods.js
Normal file
443
src/client/rest/RESTMethods.js
Normal file
@@ -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;
|
||||||
0
src/client/rest/RequestHandlers/Batch.js
Normal file
0
src/client/rest/RequestHandlers/Batch.js
Normal file
51
src/client/rest/RequestHandlers/RequestHandler.js
Normal file
51
src/client/rest/RequestHandlers/RequestHandler.js
Normal file
@@ -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;
|
||||||
103
src/client/rest/RequestHandlers/Sequential.js
Normal file
103
src/client/rest/RequestHandlers/Sequential.js
Normal file
@@ -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<?Object|Error>}
|
||||||
|
*/
|
||||||
|
execute(item) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
item.request.gen().end((err, res) => {
|
||||||
|
if (res && res.headers) {
|
||||||
|
this.requestLimit = res.headers['x-ratelimit-limit'];
|
||||||
|
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||||
|
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
|
||||||
|
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
if (err.status === 429) {
|
||||||
|
this.restManager.client.setTimeout(() => {
|
||||||
|
this.waiting = false;
|
||||||
|
this.globalLimit = false;
|
||||||
|
resolve();
|
||||||
|
}, Number(res.headers['retry-after']) + 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;
|
||||||
22
src/client/rest/UserAgentManager.js
Normal file
22
src/client/rest/UserAgentManager.js
Normal file
@@ -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;
|
||||||
124
src/client/voice/ClientVoiceManager.js
Normal file
124
src/client/voice/ClientVoiceManager.js
Normal file
@@ -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<string, VoiceConnection>}
|
||||||
|
*/
|
||||||
|
this.connections = new Collection();
|
||||||
|
/**
|
||||||
|
* Pending connection attempts, maps Guild ID to VoiceChannel
|
||||||
|
* @type {Collection<string, VoiceChannel>}
|
||||||
|
*/
|
||||||
|
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<VoiceConnection>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
259
src/client/voice/VoiceConnection.js
Normal file
259
src/client/voice/VoiceConnection.js
Normal file
@@ -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;
|
||||||
84
src/client/voice/VoiceConnectionUDPClient.js
Normal file
84
src/client/voice/VoiceConnectionUDPClient.js
Normal file
@@ -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;
|
||||||
113
src/client/voice/VoiceConnectionWebSocket.js
Normal file
113
src/client/voice/VoiceConnectionWebSocket.js
Normal file
@@ -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;
|
||||||
260
src/client/voice/dispatcher/StreamDispatcher.js
Normal file
260
src/client/voice/dispatcher/StreamDispatcher.js
Normal file
@@ -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;
|
||||||
15
src/client/voice/opus/BaseOpusEngine.js
Normal file
15
src/client/voice/opus/BaseOpusEngine.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class BaseOpus {
|
||||||
|
constructor(player) {
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
encode(buffer) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(buffer) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseOpus;
|
||||||
27
src/client/voice/opus/NodeOpusEngine.js
Normal file
27
src/client/voice/opus/NodeOpusEngine.js
Normal file
@@ -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;
|
||||||
24
src/client/voice/opus/OpusEngineList.js
Normal file
24
src/client/voice/opus/OpusEngineList.js
Normal file
@@ -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');
|
||||||
|
};
|
||||||
27
src/client/voice/opus/OpusScriptEngine.js
Normal file
27
src/client/voice/opus/OpusScriptEngine.js
Normal file
@@ -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;
|
||||||
14
src/client/voice/pcm/ConverterEngine.js
Normal file
14
src/client/voice/pcm/ConverterEngine.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
|
class ConverterEngine extends EventEmitter {
|
||||||
|
constructor(player) {
|
||||||
|
super();
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
createConvertStream() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConverterEngine;
|
||||||
1
src/client/voice/pcm/ConverterEngineList.js
Normal file
1
src/client/voice/pcm/ConverterEngineList.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
exports.fetch = () => require('./FfmpegConverterEngine');
|
||||||
40
src/client/voice/pcm/FfmpegConverterEngine.js
Normal file
40
src/client/voice/pcm/FfmpegConverterEngine.js
Normal file
@@ -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;
|
||||||
100
src/client/voice/player/BasePlayer.js
Normal file
100
src/client/voice/player/BasePlayer.js
Normal file
@@ -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;
|
||||||
17
src/client/voice/player/DefaultPlayer.js
Normal file
17
src/client/voice/player/DefaultPlayer.js
Normal file
@@ -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;
|
||||||
19
src/client/voice/receiver/VoiceReadable.js
Normal file
19
src/client/voice/receiver/VoiceReadable.js
Normal file
@@ -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;
|
||||||
123
src/client/voice/receiver/VoiceReceiver.js
Normal file
123
src/client/voice/receiver/VoiceReceiver.js
Normal file
@@ -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;
|
||||||
245
src/client/websocket/WebSocketManager.js
Normal file
245
src/client/websocket/WebSocketManager.js
Normal file
@@ -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;
|
||||||
98
src/client/websocket/packets/WebSocketPacketManager.js
Normal file
98
src/client/websocket/packets/WebSocketPacketManager.js
Normal file
@@ -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;
|
||||||
11
src/client/websocket/packets/handlers/AbstractHandler.js
Normal file
11
src/client/websocket/packets/handlers/AbstractHandler.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
class AbstractHandler {
|
||||||
|
constructor(packetManager) {
|
||||||
|
this.packetManager = packetManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle(packet) {
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AbstractHandler;
|
||||||
20
src/client/websocket/packets/handlers/ChannelCreate.js
Normal file
20
src/client/websocket/packets/handlers/ChannelCreate.js
Normal file
@@ -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;
|
||||||
20
src/client/websocket/packets/handlers/ChannelDelete.js
Normal file
20
src/client/websocket/packets/handlers/ChannelDelete.js
Normal file
@@ -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;
|
||||||
31
src/client/websocket/packets/handlers/ChannelPinsUpdate.js
Normal file
31
src/client/websocket/packets/handlers/ChannelPinsUpdate.js
Normal file
@@ -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;
|
||||||
11
src/client/websocket/packets/handlers/ChannelUpdate.js
Normal file
11
src/client/websocket/packets/handlers/ChannelUpdate.js
Normal file
@@ -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;
|
||||||
23
src/client/websocket/packets/handlers/GuildBanAdd.js
Normal file
23
src/client/websocket/packets/handlers/GuildBanAdd.js
Normal file
@@ -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;
|
||||||
20
src/client/websocket/packets/handlers/GuildBanRemove.js
Normal file
20
src/client/websocket/packets/handlers/GuildBanRemove.js
Normal file
@@ -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;
|
||||||
22
src/client/websocket/packets/handlers/GuildCreate.js
Normal file
22
src/client/websocket/packets/handlers/GuildCreate.js
Normal file
@@ -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;
|
||||||
19
src/client/websocket/packets/handlers/GuildDelete.js
Normal file
19
src/client/websocket/packets/handlers/GuildDelete.js
Normal file
@@ -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;
|
||||||
17
src/client/websocket/packets/handlers/GuildMemberAdd.js
Normal file
17
src/client/websocket/packets/handlers/GuildMemberAdd.js
Normal file
@@ -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;
|
||||||
13
src/client/websocket/packets/handlers/GuildMemberRemove.js
Normal file
13
src/client/websocket/packets/handlers/GuildMemberRemove.js
Normal file
@@ -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;
|
||||||
18
src/client/websocket/packets/handlers/GuildMemberUpdate.js
Normal file
18
src/client/websocket/packets/handlers/GuildMemberUpdate.js
Normal file
@@ -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;
|
||||||
29
src/client/websocket/packets/handlers/GuildMembersChunk.js
Normal file
29
src/client/websocket/packets/handlers/GuildMembersChunk.js
Normal file
@@ -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;
|
||||||
11
src/client/websocket/packets/handlers/GuildRoleCreate.js
Normal file
11
src/client/websocket/packets/handlers/GuildRoleCreate.js
Normal file
@@ -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;
|
||||||
11
src/client/websocket/packets/handlers/GuildRoleDelete.js
Normal file
11
src/client/websocket/packets/handlers/GuildRoleDelete.js
Normal file
@@ -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;
|
||||||
11
src/client/websocket/packets/handlers/GuildRoleUpdate.js
Normal file
11
src/client/websocket/packets/handlers/GuildRoleUpdate.js
Normal file
@@ -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;
|
||||||
11
src/client/websocket/packets/handlers/GuildSync.js
Normal file
11
src/client/websocket/packets/handlers/GuildSync.js
Normal file
@@ -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;
|
||||||
11
src/client/websocket/packets/handlers/GuildUpdate.js
Normal file
11
src/client/websocket/packets/handlers/GuildUpdate.js
Normal file
@@ -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;
|
||||||
19
src/client/websocket/packets/handlers/MessageCreate.js
Normal file
19
src/client/websocket/packets/handlers/MessageCreate.js
Normal file
@@ -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;
|
||||||
19
src/client/websocket/packets/handlers/MessageDelete.js
Normal file
19
src/client/websocket/packets/handlers/MessageDelete.js
Normal file
@@ -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;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user