mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +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