chore: merge v12-dev master into stable

This commit is contained in:
Amish Shah
2020-03-01 17:57:30 +00:00
309 changed files with 17239 additions and 16659 deletions

View File

@@ -1,44 +1,65 @@
{
"extends": "eslint:recommended",
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": ["import"],
"parserOptions": {
"ecmaVersion": 6
"ecmaVersion": 2019
},
"env": {
"es6": true,
"node": true
},
"overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }],
"rules": {
"import/order": [
"error",
{
"groups": ["builtin", "external", "internal", "index", "sibling", "parent"],
"alphabetize": {
"order": "asc"
}
}
],
"prettier/prettier": [
2,
{
"printWidth": 120,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf"
}
],
"strict": ["error", "global"],
"no-await-in-loop": "warn",
"no-compare-neg-zero": "error",
"no-extra-parens": ["warn", "all", {
"nestedBinaryExpressions": false
}],
"no-template-curly-in-string": "error",
"no-unsafe-negation": "error",
"valid-jsdoc": ["error", {
"requireReturn": false,
"requireReturnDescription": false,
"prefer": {
"return": "returns",
"arg": "param"
},
"preferType": {
"String": "string",
"Number": "number",
"Boolean": "boolean",
"Symbol": "symbol",
"object": "Object",
"function": "Function",
"array": "Array",
"date": "Date",
"error": "Error",
"null": "void"
"valid-jsdoc": [
"error",
{
"requireReturn": false,
"requireReturnDescription": false,
"prefer": {
"return": "returns",
"arg": "param"
},
"preferType": {
"String": "string",
"Number": "number",
"Boolean": "boolean",
"Symbol": "symbol",
"object": "Object",
"function": "Function",
"array": "Array",
"date": "Date",
"error": "Error",
"null": "void"
}
}
}],
],
"accessor-pairs": "warn",
"array-callback-return": "error",
"complexity": "warn",
"consistent-return": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
@@ -96,7 +117,6 @@
"func-names": "error",
"func-name-matching": "error",
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"indent": ["error", 2, { "SwitchCase": 1 }],
"key-spacing": "error",
"keyword-spacing": "error",
"max-depth": "error",
@@ -108,7 +128,6 @@
"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",
@@ -118,14 +137,20 @@
"nonblock-statement-body-position": "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-before-function-paren": [
"error",
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",

View File

@@ -7,6 +7,7 @@ pull request. We use ESLint to enforce a consistent coding style, so having that
is a great boon to your development process.
## Setup
To get ready to work on the codebase, please do the following:
1. Fork & clone the repository, and make sure you're on the **master** branch

11
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# These are supported funding model platforms
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
# patreon: # Replace with a single Patreon username
# open_collective: # Replace with a single Open Collective username
# ko_fi: # Replace with a single Ko-fi username
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
# custom: # Replace with a single custom sponsorship URL
github: amishshah
patreon: discordjs

View File

@@ -1,26 +0,0 @@
<!--
If you need help with discord.js installation or usage, please go to the discord.js Discord server instead:
https://discord.gg/bRCvFy9
This issue tracker is only for bug reports and enhancement suggestions. You won't receive any basic help here.
-->
**Please describe the problem you are having in as much detail as possible:**
**Include a reproducible code sample here, if possible:**
```js
```
**Further details:**
- discord.js version:
- node.js version:
- Operating system:
- Priority this issue should have please be realistic and elaborate if possible:
<!--
Ideally you would also test whether the issue occurs on the latest master branch commit.
If you have, please check the following box and insert the hash of the commit you tested:
-->
- [ ] I have also tested the issue on latest master, commit hash:

39
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,39 @@
---
name: Bug report
about: Report incorrect or unexpected behaviour of discord.js
title: ''
labels: 's: unverified, type: bug'
assignees: ''
---
<!--
If you need help with discord.js installation or usage, please go to the discord.js Discord server instead:
https://discord.gg/bRCvFy9
This issue tracker is only for bug reports and enhancement suggestions.
You won't receive any basic help here.
-->
**Please describe the problem you are having in as much detail as possible:**
**Include a reproducible code sample here, if possible:**
```js
// Place your code here
```
**Further details:**
- discord.js version:
- Node.js version:
- Operating system:
- Priority this issue should have please be realistic and elaborate if possible:
<!--
If this applies to you, please check the respective checkbox: [ ] becomes [x].
You don't have to modify the text to suit your particular situation if you want to
elaborate, please do so in the description.
While it's not a requirement to test your issue on the master branch, it would make fixing
the problem a lot easier for us, so please do so if possible.
-->
- [ ] I have also tested the issue on latest master, commit hash:

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: discord.js discord server
url: https://discord.gg/bRCvFy9
about: Please use this Discord Server to ask questions and get support. We don't typically answer questions here and they will likely be closed and redirected to the Discord server.

View File

@@ -0,0 +1,26 @@
---
name: Feature request
about: Request a feature for the core discord.js library
title: ''
labels: 'type: enhancement'
assignees: ''
---
<!--
If you need help with discord.js installation or usage, please go to the discord.js Discord server instead:
https://discord.gg/bRCvFy9
This issue tracker is only for bug reports and enhancement suggestions.
You likely won't receive any basic help here.
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the ideal solution**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,7 +1,12 @@
**Please describe the changes this PR makes and why it should be merged:**
**Status**
- [ ] Code changes have been tested against the Discord API, or there are no code changes
- [ ] I know how to update typings and have done so, or typings don't need updating
**Semantic versioning classification:**
**Semantic versioning classification:**
- [ ] This PR changes the library's interface (methods or parameters added)
- [ ] This PR includes breaking changes (methods removed or renamed, parameters moved or removed)
- [ ] This PR **only** includes non-code changes, like changes to documentation, README, etc.

7
.github/SUPPORT.md vendored Normal file
View File

@@ -0,0 +1,7 @@
# Seeking support?
We're sorry, we only use this issue tracker for bugs in the library itself and feature requests for it. We are not able to provide general support or answser questions on the issue tracker.
Should you want to ask such questions, please post in one of our support channels in our Discord server: https://discord.gg/bRCvFy9
Any issues that don't directly involve a bug in the library or a feature request will likely be closed and redirected to the Discord server.

47
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Deployment
on:
push:
branches:
- '*'
- '!webpack'
- '!docs'
jobs:
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@master
- name: Install Node v12
uses: actions/setup-node@master
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Build and deploy documentation
uses: discordjs/action-docs@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
webpack:
name: webpack
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@master
- name: Install Node v12
uses: actions/setup-node@master
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Build and deploy webpack
uses: discordjs/action-webpack@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

58
.github/workflows/test-cron.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Testing Cron
on:
schedule:
- cron: '0 */12 * * *'
jobs:
lint:
name: ESLint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Run ESLint
uses: icrawl/action-eslint@v1
typings:
name: TSLint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Run TSLint
run: npm run lint:typings
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Test documentation
run: npm run docs:test

56
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Testing
on: [push, pull_request]
jobs:
lint:
name: ESLint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Run ESLint
uses: icrawl/action-eslint@v1
typings:
name: TSLint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Run TSLint
run: npm run lint:typings
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm install
- name: Test documentation
run: npm run docs:test

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Packages
node_modules/
yarn.lock
package-lock.json
# Log files
logs/

View File

@@ -14,8 +14,6 @@ deploy/
.vscode/
docs/
webpack/
# NPM ignore
.eslintrc.json
.gitattributes

2
.npmrc
View File

@@ -1 +1 @@
package-json=false
package-lock=false

View File

@@ -1,12 +1,8 @@
{
"ecmaVersion": 7,
"libs": [],
"loadEagerly": [
"./src/*.js"
],
"dontLoad": [
"node_modules/**"
],
"loadEagerly": ["./src/*.js"],
"dontLoad": ["node_modules/**"],
"plugins": {
"es_modules": {},
"node": {},
@@ -15,7 +11,7 @@
"strong": true
},
"webpack": {
"configPath": "./webpack.config.js",
"configPath": "./webpack.config.js"
}
}
}

View File

@@ -1,22 +0,0 @@
language: node_js
node_js:
- "6"
- "8"
- "10"
- "12"
cache:
directories:
- node_modules
install: npm install
script: bash ./deploy/test.sh
jobs:
include:
- stage: build
node_js: "6"
script: bash ./deploy/deploy.sh
env:
global:
- ENCRYPTION_LABEL: "af862fa96d3e"
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
dist: trusty
sudo: false

View File

@@ -175,7 +175,7 @@
END OF TERMS AND CONDITIONS
Copyright 2017 Amish Shah
Copyright 2015 - 2020 Amish Shah
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -8,17 +8,31 @@
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://travis-ci.org/discordjs/discord.js"><img src="https://travis-ci.org/discordjs/discord.js.svg" alt="Build status" /></a>
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/workflows/Testing/badge.svg" alt="Build status" /></a>
<a href="https://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
<a href="https://www.patreon.com/discordjs"><img src="https://img.shields.io/badge/donate-patreon-F96854.svg" alt="Patreon" /></a>
</p>
<p>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="npm installnfo" /></a>
</p>
</div>
## Table of contents
- [About](#about)
- [Installation](#installation)
- [Audio engines](#audio-engines)
- [Optional packages](#optional-packages)
- [Example Usage](#example-usage)
- [Links](#links)
- [Extensions](#extensions)
- [Contributing](#contributing)
- [Help](#help)
## About
discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the
[Discord API](https://discordapp.com/developers/docs/intro) very easily.
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
[Discord API](https://discordapp.com/developers/docs/intro).
- Object-oriented
- Predictable abstractions
@@ -26,7 +40,8 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
- 100% coverage of the Discord API
## Installation
**Node.js 6.0.0 or newer is required.**
**Node.js 12.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
@@ -34,18 +49,23 @@ With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/o
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
### Audio engines
The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack`)
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`)
- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`)
## Example usage
```js
const Discord = require('discord.js');
const client = new Discord.Client();
@@ -64,23 +84,28 @@ client.login('token');
```
## Links
* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
* [Documentation](https://discord.js.org/#/docs)
* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
* [Discord API Discord server](https://discord.gg/discord-api)
* [GitHub](https://github.com/discordjs/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
* [Related libraries](https://discordapi.com/unofficial/libs.html)
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Documentation](https://discord.js.org/#/docs/main/master/general/welcome)
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v12.html), including updated and removed items in the library.
- [Discord.js Discord server](https://discord.gg/bRCvFy9)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js)
- [NPM](https://www.npmjs.com/package/discord.js)
- [Related libraries](https://discordapi.com/unofficial/libs.html)
### Extensions
* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
- [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs).
See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).

View File

@@ -1,9 +0,0 @@
const browser = typeof window !== 'undefined';
const webpack = !!process.env.__DISCORD_WEBPACK__;
const Discord = require('./');
module.exports = Discord;
if (browser && webpack) window.Discord = Discord; // eslint-disable-line no-undef
// eslint-disable-next-line no-console
else if (!browser) console.warn('Warning: Attempting to use browser version of Discord.js in a non-browser environment!');

Binary file not shown.

View File

@@ -1,90 +0,0 @@
#!/bin/bash
# Adapted from https://gist.github.com/domenic/ec8b0fc8ab45f39403dd.
set -e
function build {
npm run docs
VERSIONED=false npm run webpack
}
# For revert branches, do nothing
if [[ "$TRAVIS_BRANCH" == revert-* ]]; then
echo -e "\e[36m\e[1mBuild triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing."
exit 0
fi
# For PRs, do nothing
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - doing nothing."
exit 0
fi
# Figure out the source of the build
if [ -n "$TRAVIS_TAG" ]; then
echo -e "\e[36m\e[1mBuild triggered for tag \"${TRAVIS_TAG}\"."
SOURCE=$TRAVIS_TAG
SOURCE_TYPE="tag"
else
echo -e "\e[36m\e[1mBuild triggered for branch \"${TRAVIS_BRANCH}\"."
SOURCE=$TRAVIS_BRANCH
SOURCE_TYPE="branch"
fi
# For Node != 6, do nothing
if [ "$TRAVIS_NODE_VERSION" != "6" ]; then
echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - doing nothing."
exit 0
fi
build
# Initialise some useful variables
REPO=`git config remote.origin.url`
SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:}
SHA=`git rev-parse --verify HEAD`
# Decrypt and add the ssh key
ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key"
ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv"
ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
ENCRYPTED_IV=${!ENCRYPTED_IV_VAR}
openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy/deploy-key.enc -out deploy-key -d
chmod 600 deploy-key
eval `ssh-agent -s`
ssh-add deploy-key
# Checkout the repo in the target branch so we can build docs and push to it
TARGET_BRANCH="docs"
git clone $REPO out -b $TARGET_BRANCH
# Move the generated JSON file to the newly-checked-out repo, to be committed and pushed
mv docs/docs.json out/$SOURCE.json
# Commit and push
cd out
git add .
git config user.name "Travis CI"
git config user.email "$COMMIT_AUTHOR_EMAIL"
git commit -m "Docs build for ${SOURCE_TYPE} ${SOURCE}: ${SHA}" || true
git push $SSH_REPO $TARGET_BRANCH
# Clean up...
cd ..
rm -rf out
# ...then do the same once more for the webpack
TARGET_BRANCH="webpack"
git clone $REPO out -b $TARGET_BRANCH
# Move the generated webpack over
mv webpack/discord.js out/discord.$SOURCE.js
mv webpack/discord.min.js out/discord.$SOURCE.min.js
# Commit and push
cd out
git add .
git config user.name "Travis CI"
git config user.email "$COMMIT_AUTHOR_EMAIL"
git commit -m "Webpack build for ${SOURCE_TYPE} ${SOURCE}: ${SHA}" || true
git push $SSH_REPO $TARGET_BRANCH

View File

@@ -1,34 +0,0 @@
#!/bin/bash
set -e
function tests {
npm run lint
npm run docs:test
exit 0
}
# For revert branches, do nothing
if [[ "$TRAVIS_BRANCH" == revert-* ]]; then
echo -e "\e[36m\e[1mTest triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing."
exit 0
fi
# For PRs
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
echo -e "\e[36m\e[1mTest triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running tests."
tests
fi
# Figure out the source of the test
if [ -n "$TRAVIS_TAG" ]; then
echo -e "\e[36m\e[1mTest triggered for tag \"${TRAVIS_TAG}\"."
else
echo -e "\e[36m\e[1mTest triggered for branch \"${TRAVIS_BRANCH}\"."
fi
# For Node != 6
if [ "$TRAVIS_NODE_VERSION" != "6" ]; then
echo -e "\e[36m\e[1mTest triggered with Node v${TRAVIS_NODE_VERSION} - only running tests."
tests
fi

View File

@@ -6,11 +6,11 @@ In here you'll see a few examples showing how you can send an attachment using d
There are a few ways you can do this, but we'll show you the easiest.
The following examples use [Attachment](/#/docs/main/stable/class/Attachment).
The following examples use [MessageAttachment](/#/docs/main/master/class/MessageAttachment).
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
const { Client, MessageAttachment } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
@@ -24,13 +24,13 @@ client.on('ready', () => {
});
client.on('message', message => {
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using Attachment
const attachment = new Attachment('https://i.imgur.com/w3duR07.png');
// Send the attachment in the message channel
message.channel.send(attachment);
}
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using MessageAttachment
const attachment = new MessageAttachment('https://i.imgur.com/w3duR07.png');
// Send the attachment in the message channel
message.channel.send(attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
@@ -41,11 +41,11 @@ And here is the result:
![Image showing the result](/static/attachment-example1.png)
But what if you want to send an attachment with a message content? Fear not, for it is easy to do that too! We'll recommend reading [the TextChannel's "send" function documentation](/#/docs/main/stable/class/TextChannel?scrollTo=send) to see what other options are available.
But what if you want to send an attachment with a message content? Fear not, for it is easy to do that too! We'll recommend reading [the TextChannel's "send" function documentation](/#/docs/main/master/class/TextChannel?scrollTo=send) to see what other options are available.
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
const { Client, MessageAttachment } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
@@ -59,13 +59,13 @@ client.on('ready', () => {
});
client.on('message', message => {
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using Attachment
const attachment = new Attachment('https://i.imgur.com/w3duR07.png');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author},`, attachment);
}
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using MessageAttachment
const attachment = new MessageAttachment('https://i.imgur.com/w3duR07.png');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author},`, attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
@@ -78,11 +78,11 @@ And here's the result of this one:
## Sending a local file or buffer
Sending a local file isn't hard either! We'll be using [Attachment](/#/docs/main/stable/class/Attachment) for these examples too.
Sending a local file isn't hard either! We'll be using [MessageAttachment](/#/docs/main/master/class/MessageAttachment) for these examples too.
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
const { Client, MessageAttachment } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
@@ -96,13 +96,13 @@ client.on('ready', () => {
});
client.on('message', message => {
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using Attachment
const attachment = new Attachment('./rip.png');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author},`, attachment);
}
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using MessageAttachment
const attachment = new MessageAttachment('./rip.png');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author},`, attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
@@ -120,7 +120,7 @@ You can use any buffer you want, and send it. Just make sure to overwrite the fi
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
const { Client, MessageAttachment } = require('discord.js');
// Import the native fs module
const fs = require('fs');
@@ -137,21 +137,21 @@ client.on('ready', () => {
});
client.on('message', message => {
// If the message is '!memes'
if (message.content === '!memes') {
// Get the buffer from the 'memes.txt', assuming that the file exists
const buffer = fs.readFileSync('./memes.txt');
// If the message is '!memes'
if (message.content === '!memes') {
// Get the buffer from the 'memes.txt', assuming that the file exists
const buffer = fs.readFileSync('./memes.txt');
/**
* Create the attachment using Attachment,
* overwritting the default file name to 'memes.txt'
* Read more about it over at
* http://discord.js.org/#/docs/main/stable/class/Attachment
*/
const attachment = new Attachment(buffer, 'memes.txt');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author}, here are your memes!`, attachment);
}
/**
* Create the attachment using MessageAttachment,
* overwritting the default file name to 'memes.txt'
* Read more about it over at
* http://discord.js.org/#/docs/main/master/class/MessageAttachment
*/
const attachment = new MessageAttachment(buffer, 'memes.txt');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author}, here are your memes!`, attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me

View File

@@ -1,3 +1,5 @@
'use strict';
/**
* Send a user a link to their avatar
*/
@@ -21,7 +23,7 @@ client.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);
message.reply(message.author.displayAvatarURL());
}
});

View File

@@ -1,9 +1,11 @@
'use strict';
/**
* An example of how you can send embeds
*/
// Extract the required classes from the discord.js module
const { Client, RichEmbed } = require('discord.js');
const { Client, MessageEmbed } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
@@ -21,12 +23,12 @@ client.on('message', message => {
if (message.content === 'how to embed') {
// We can create embeds using the MessageEmbed constructor
// Read more about all that you can do with the constructor
// over at https://discord.js.org/#/docs/main/stable/class/RichEmbed
const embed = new RichEmbed()
// over at https://discord.js.org/#/docs/main/master/class/MessageEmbed
const embed = new MessageEmbed()
// Set the title of the field
.setTitle('A slick little embed')
// Set the color of the embed
.setColor(0xFF0000)
.setColor(0xff0000)
// Set the main content of the embed
.setDescription('Hello, this is a slick embed!');
// Send the embed to the same channel as the message

View File

@@ -1,3 +1,5 @@
'use strict';
/**
* A bot that welcomes new guild members when they join
*/
@@ -19,7 +21,7 @@ client.on('ready', () => {
// Create an event listener for new guild members
client.on('guildMemberAdd', member => {
// Send the message to a designated channel on a server:
const channel = member.guild.channels.find(ch => ch.name === 'member-log');
const channel = member.guild.channels.cache.find(ch => ch.name === 'member-log');
// Do nothing if the channel wasn't found on this server
if (!channel) return;
// Send the message, mentioning the member

View File

@@ -4,7 +4,7 @@ In here, you'll see some basic examples for kicking and banning a member.
## Kicking a member
Let's say you have a member that you'd like to kick. Here is an example of how you *can* do it.
Let's say you have a member that you'd like to kick. Here is an example of how you _can_ do it.
```js
// Import the discord.js module
@@ -28,7 +28,7 @@ client.on('message', message => {
// If the message content starts with "!kick"
if (message.content.startsWith('!kick')) {
// Assuming we mention someone in the message, this will return the user
// Read more about mentions over at https://discord.js.org/#/docs/main/stable/class/MessageMentions
// Read more about mentions over at https://discord.js.org/#/docs/main/master/class/MessageMentions
const user = message.mentions.users.first();
// If we have a user mentioned
if (user) {
@@ -41,24 +41,27 @@ client.on('message', message => {
* Make sure you run this on a member, not a user!
* There are big differences between a user and a member
*/
member.kick('Optional reason that will display in the audit logs').then(() => {
// We let the message author know we were able to kick the person
message.reply(`Successfully kicked ${user.tag}`);
}).catch(err => {
// An error happened
// This is generally due to the bot not being able to kick the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to kick the member');
// Log the error
console.error(err);
});
member
.kick('Optional reason that will display in the audit logs')
.then(() => {
// We let the message author know we were able to kick the person
message.reply(`Successfully kicked ${user.tag}`);
})
.catch(err => {
// An error happened
// This is generally due to the bot not being able to kick the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to kick the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.reply('That user isn\'t in this guild!');
message.reply("That user isn't in this guild!");
}
// Otherwise, if no user was mentioned
// Otherwise, if no user was mentioned
} else {
message.reply('You didn\'t mention the user to kick!');
message.reply("You didn't mention the user to kick!");
}
}
});
@@ -97,7 +100,7 @@ client.on('message', message => {
// if the message content starts with "!ban"
if (message.content.startsWith('!ban')) {
// Assuming we mention someone in the message, this will return the user
// Read more about mentions over at https://discord.js.org/#/docs/main/stable/class/MessageMentions
// Read more about mentions over at https://discord.js.org/#/docs/main/master/class/MessageMentions
const user = message.mentions.users.first();
// If we have a user mentioned
if (user) {
@@ -110,28 +113,31 @@ client.on('message', message => {
* Make sure you run this on a member, not a user!
* There are big differences between a user and a member
* Read more about what ban options there are over at
* https://discord.js.org/#/docs/main/stable/class/GuildMember?scrollTo=ban
* https://discord.js.org/#/docs/main/master/class/GuildMember?scrollTo=ban
*/
member.ban({
reason: 'They were bad!',
}).then(() => {
// We let the message author know we were able to ban the person
message.reply(`Successfully banned ${user.tag}`);
}).catch(err => {
// An error happened
// This is generally due to the bot not being able to ban the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to ban the member');
// Log the error
console.error(err);
});
member
.ban({
reason: 'They were bad!',
})
.then(() => {
// We let the message author know we were able to ban the person
message.reply(`Successfully banned ${user.tag}`);
})
.catch(err => {
// An error happened
// This is generally due to the bot not being able to ban the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to ban the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.reply('That user isn\'t in this guild!');
message.reply("That user isn't in this guild!");
}
} else {
// Otherwise, if no user was mentioned
message.reply('You didn\'t mention the user to ban!');
// Otherwise, if no user was mentioned
message.reply("You didn't mention the user to ban!");
}
}
});

View File

@@ -1,3 +1,5 @@
'use strict';
/**
* A ping pong bot, whenever you send "ping", it replies "pong".
*/

View File

@@ -1,3 +1,5 @@
'use strict';
/**
* Send a message using a webhook
*/

View File

@@ -1,23 +1,30 @@
# Frequently Asked Questions
These are just questions that get asked frequently, that usually have a common resolution.
If you have issues not listed here, please ask in the [official Discord server](https://discord.gg/bRCvFy9).
Always make sure to read the documentation.
These questions are some of the most frequently asked.
## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽
Update to Node.js 6.0.0 or newer.
Update to Node.js 12.0.0 or newer.
## How do I get voice working?
- Install FFMPEG.
- Install either the `@discordjs/opus` package or the `opusscript` package.
@discordjs/opus is greatly preferred, due to it having significantly better performance.
## How do I install FFMPEG?
- **npm:** `npm install ffmpeg-binaries`
- **Ubuntu 16.04:** `sudo apt install ffmpeg`
- **Ubuntu 14.04:** `sudo apt-get install libav-tools`
- **Windows:** `npm install ffmpeg-binaries` or see the [FFMPEG section of AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md#download-ffmpeg).
## How do I set up @discordjs/opus?
- **Ubuntu:** Simply run `npm install @discordjs/opus`, and it's done. Congrats!
- **Windows:** Run `npm install --global --production windows-build-tools` in an admin command prompt or PowerShell.
Then, running `npm install @discordjs/opus` in your bot's directory should successfully build it. Woo!
Other questions can be found at the [official Discord.js guide](https://discordjs.guide/popular-topics/common-questions.html)
If you have issues not listed here or on the guide, feel free to ask in the [official Discord.js server](https://discord.gg/bRCvFy9).
Always make sure to read the [documentation](https://discord.js.org/#/docs/main/stable/general/welcome).

View File

@@ -1,92 +1,93 @@
# Version 11.6.0
v11.6.0 backports new features from the in-development v12, and fixes bugs in the v11.5.x releases.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.6.0) for a full list of changes, including information about deprecations.
# Version 12.0.0
# Version 11.5.0
v11.5.0 backports new features from the in-development v12, and fixes bugs in the v11.4.x releases.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.5.0) for a full list of changes, including information about deprecations.
# Version 11.4.0
v11.4.0 backports many new features such as Rich Presence and bugfixes from v11.3.0.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.4.0) for a full list of changes, including information about deprecations.
# Version 11.3.0
v11.3.0 backports many new features and bug fixes from the in-development v12.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.3.0) for a full list of changes, including information about deprecations.
# Version 11.2.0
v11.2.0 fixes a lot of bugs we encountered along the 11.1.0 release, as well as support for new features such as Message Attachments and UserGuildSettings.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.2.0) for a full list of changes, including information about deprecations.
v12.0.0 contains many new and improved features, optimisations, and bug fixes.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/12.0.0) for a full list of changes.
You can also visit [the guide](https://discordjs.guide/additional-info/changes-in-v12.html) for help with updating your v11 code to v12.
# Version 11.1.0
v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.1.0) for a full list of changes, including
information about deprecations.
# Version 11
Version 11 contains loads of new and improved features, optimisations, and bug fixes.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.0.0) for a full list of changes.
## Significant additions
* Message Reactions and Embeds (rich text)
* Support for uws and erlpack for better performance
* OAuthApplication support
* Web distributions
- Message Reactions and Embeds (rich text)
- Support for uws and erlpack for better performance
- OAuthApplication support
- Web distributions
## Breaking changes
### Client.login() no longer supports logging in with email + password
Logging in with an email and password has always been heavily discouraged since the advent of proper token support, but in v11 we have made the decision to completely remove the functionality, since Hammer & Chisel have [officially stated](https://github.com/hammerandchisel/discord-api-docs/issues/69#issuecomment-223886862) it simply shouldn't be done.
User accounts can still log in with tokens just like bot accounts. To obtain the token for a user account, you can log in to Discord with that account, and use Ctrl + Shift + I to open the developer tools. In the console tab, evaluating `localStorage.token` will give you the token for that account.
### ClientUser.setEmail()/setPassword() now require the current password, as well as setUsername() on user accounts
Since you can no longer log in with email and password, you must provide the current account password to the `setEmail()`, `setPassword()`, and `setUsername()` methods for user accounts (self-bots).
### Removed TextBasedChannel.sendTTSMessage()
This method was deemed to be an entirely pointless shortcut that virtually nobody even used.
The same results can be achieved by passing options to `send()` or `sendMessage()`.
Example:
```js
channel.send('Hi there', { tts: true });
```
### Using Collection.find()/exists() with IDs will throw an error
This is simply to help prevent a common mistake that is made frequently.
To find something or check its existence using an ID, you should use `.get()` and `.has()` which are part of the [ES6 Map class](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map), which Collection is an extension of.
# Version 10
Version 10's non-BC changes focus on cleaning up some inconsistencies that exist in previous versions.
Upgrading from v9 should be quick and painless.
## Client options
All client options have been converted to camelCase rather than snake_case, and `max_message_cache` was renamed to `messageCacheMaxSize`.
v9 code example:
```js
const client = new Discord.Client({
disable_everyone: true,
max_message_cache: 500,
message_cache_lifetime: 120,
message_sweep_interval: 60
message_sweep_interval: 60,
});
```
v10 code example:
```js
const client = new Discord.Client({
disableEveryone: true,
messageCacheMaxSize: 500,
messageCacheLifetime: 120,
messageSweepInterval: 60
messageSweepInterval: 60,
});
```
## Presences
Presences have been completely restructured.
Previous versions of discord.js assumed that users had the same presence amongst all guilds - with the introduction of sharding, however, this is no longer the case.
v9 discord.js code may look something like this:
```js
User.status; // the status of the user
User.game; // the game that the user is playing
@@ -101,6 +102,7 @@ Additionally, the introduction of the Presence class keeps all of the presence d
A user may have an entirely different presence between two different guilds.**
v10 code:
```js
MemberOrUser.presence.status; // the status of the member or user
MemberOrUser.presence.game; // the game that the member or user is playing
@@ -110,41 +112,46 @@ ClientUser.setPresence(fullPresence); // status and game combined
```
## Voice
Voice has been rewritten internally, but in a backwards-compatible manner.
There is only one breaking change here; the `disconnected` event was renamed to `disconnect`.
Several more events have been made available to a VoiceConnection, so see the documentation.
## Events
Many events have been renamed or had their arguments change.
### Client events
| Version 9 | Version 10 |
|------------------------------------------------------|-----------------------------------------------|
| guildMemberAdd(guild, member) | guildMemberAdd(member) |
| guildMemberAvailable(guild, member) | guildMemberAvailable(member) |
| guildMemberRemove(guild, member) | guildMemberRemove(member) |
| guildMembersChunk(guild, members) | guildMembersChunk(members) |
| guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) |
| guildRoleCreate(guild, role) | roleCreate(role) |
| guildRoleDelete(guild, role) | roleDelete(role) |
| guildRoleUpdate(guild, oldRole, newRole) | roleUpdate(oldRole, newRole) |
| Version 9 | Version 10 |
| ---------------------------------------------- | --------------------------------------- |
| guildMemberAdd(guild, member) | guildMemberAdd(member) |
| guildMemberAvailable(guild, member) | guildMemberAvailable(member) |
| guildMemberRemove(guild, member) | guildMemberRemove(member) |
| guildMembersChunk(guild, members) | guildMembersChunk(members) |
| guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) |
| guildRoleCreate(guild, role) | roleCreate(role) |
| guildRoleDelete(guild, role) | roleDelete(role) |
| guildRoleUpdate(guild, oldRole, newRole) | roleUpdate(oldRole, newRole) |
The guild parameter that has been dropped from the guild-related events can still be derived using `member.guild` or `role.guild`.
### VoiceConnection events
| Version 9 | Version 10 |
|--------------|------------|
| ------------ | ---------- |
| disconnected | disconnect |
## Dates and timestamps
All dates/timestamps on the structures have been refactored to have a consistent naming scheme and availability.
All of them are named similarly to this:
**Date:** `Message.createdAt`
**Timestamp:** `Message.createdTimestamp`
All of them are named similarly to this:
**Date:** `Message.createdAt`
**Timestamp:** `Message.createdTimestamp`
See the docs for each structure to see which date/timestamps are available on them.
# Version 9
The version 9 (v9) rewrite takes a much more object-oriented approach than previous versions,
which allows your code to be much more readable and manageable.
It's been rebuilt from the ground up and should be much more stable, fixing caching issues that affected
@@ -157,19 +164,22 @@ into other more relevant classes where they belong.
Because of this, you will need to convert most of your calls over to the new methods.
Here are a few examples of methods that have changed:
* `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)`
* `Client.sendMessage(user, message)` ==> `User.sendMessage(message)`
* `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")`
* `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})`
* `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object)
* `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()`
- `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)`
- `Client.sendMessage(user, message)` ==> `User.sendMessage(message)`
- `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")`
- `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})`
- `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object)
- `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()`
A couple more important details:
* `Client.loginWithToken("token")` ==> `client.login("token")`
* `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
- `Client.loginWithToken("token")` ==> `client.login("token")`
- `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
## No more callbacks!
Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
For example, the following code:
```js
@@ -179,7 +189,7 @@ client.getChannelLogs(channel, 100, function(messages) {
```
```js
channel.fetchMessages({limit: 100}).then(messages => {
channel.fetchMessages({ limit: 100 }).then(messages => {
console.log(`${messages.size} messages found`);
});
```

View File

@@ -1,7 +1,7 @@
<div align="center">
<br />
<p>
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
<a href="https://discord.js.org"><img src="/static/logo.svg" width="546" alt="discord.js" id="djs-logo" /></a>
</p>
<br />
<p>
@@ -18,15 +18,13 @@
</div>
# Welcome!
Welcome to the discord.js v11.6 documentation.
The v11.6 release contains bugfixes from v11.5 and backports features from the in-development v12.
v12 is still very much a work-in-progress, as we're aiming to make it the best it can possibly be before releasing.
If you are fond of living life on the bleeding-edge, check out the master branch.
Welcome to the discord.js v12 documentation.
## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to interact with the
[Discord API](https://discordapp.com/developers/docs/intro) very easily.
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
[Discord API](https://discordapp.com/developers/docs/intro).
- Object-oriented
- Predictable abstractions
@@ -34,7 +32,8 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
- 100% coverage of the Discord API
## Installation
**Node.js 6.0.0 or newer is required.**
**Node.js 12.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
@@ -42,19 +41,23 @@ With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/o
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
### Audio engines
The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack`)
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`)
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`)
- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`)
## Example usage
```js
const Discord = require('discord.js');
const client = new Discord.Client();
@@ -73,23 +76,28 @@ client.login('token');
```
## Links
* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
* [Documentation](https://discord.js.org/#/docs)
* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
* [Discord API Discord server](https://discord.gg/discord-api)
* [GitHub](https://github.com/discordjs/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
* [Related libraries](https://discordapi.com/unofficial/libs.html)
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Documentation](https://discord.js.org/#/docs/main/master/general/welcome)
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable
See also the WIP [Update Guide](https://discordjs.guide/additional-info/changes-in-v12.html) also including updated and removed items in the library.
- [Discord.js Discord server](https://discord.gg/bRCvFy9)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js)
- [NPM](https://www.npmjs.com/package/discord.js)
- [Related libraries](https://discordapi.com/unofficial/libs.html)
### Extensions
* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
- [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs).
See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).

View File

@@ -12,6 +12,8 @@
path: voice.md
- name: Web builds
path: web.md
- name: Partials
path: partials.md
- name: Examples
files:
- name: Ping

65
docs/topics/partials.md Normal file
View File

@@ -0,0 +1,65 @@
# Partials
Partials allow you to receive events that contain uncached instances, providing structures that contain very minimal
data. For example, if you were to receive a `messageDelete` event with an uncached message, normally Discord.js would
discard the event. With partials, you're able to receive the event, with a Message object that contains just an ID.
## Opting in
Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](/#/docs/main/master/typedef/PartialType):
```js
// Accept partial messages, DM channels, and reactions when emitting events
new Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] });
```
## Usage & warnings
<warn>The only guaranteed data a partial structure can store is its ID. All other properties/methods should be
considered invalid/defunct while accessing a partial structure.</warn>
After opting-in with the above, you begin to allow partial messages and channels in your caches, so it's important
to check whether they're safe to access whenever you encounter them, whether it be in events or through normal cache
usage.
All instance of structures that you opted-in for will have a `partial` property. As you'd expect, this value is `true`
when the instance is partial. Partial structures are only guaranteed to contain an ID, any other properties and methods
no longer carry their normal type guarantees.
This means you have to take time to consider possible parts of your program that might need checks put in place to
prevent accessing partial data:
```js
client.on('messageDelete', message => {
console.log(`${message.id} was deleted!`);
// Partial messages do not contain any content so skip them
if (!message.partial) {
console.log(`It had content: "${message.content}"`);
}
});
// You can also try to upgrade partials to full instances:
client.on('messageReactionAdd', async (reaction, user) => {
// If a message gains a reaction and it is uncached, fetch and cache the message
// You should account for any errors while fetching, it could return API errors if the resource is missing
if (reaction.message.partial) await reaction.message.fetch();
// Now the message has been cached and is fully available:
console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`);
// Fetches and caches the reaction itself, updating resources that were possibly defunct.
if (reaction.partial) await reaction.fetch();
// Now the reaction is fully available and the properties will be reflected accurately:
console.log(`${reaction.count} user(s) have given the same reaction to this message!`);
});
```
<info>If a message is deleted and both the message and channel are uncached, you must enable both 'MESSAGE' and
'CHANNEL' in the client options to receive the messageDelete event.</info>
## Why?
This allows developers to listen to events that contain uncached data, which is useful if you're running a moderation
bot or any bot that relies on still receiving updates to resources you don't have cached -- message reactions are a
good example.
Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should
not be cached.

View File

@@ -1,20 +1,23 @@
# Introduction to Voice
Voice in discord.js can be used for many things, such as music bots, recording or relaying audio.
In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `VoiceConnection`, where you can start streaming and receiving audio.
To get started, make sure you have:
* FFmpeg - `npm install ffmpeg-binaries`
* an opus encoder, choose one from below:
* `npm install opusscript`
* `npm install @discordjs/opus`
* a good network connection
- FFmpeg - `npm install ffmpeg-static`
- an opus encoder, choose one from below:
- `npm install @discordjs/opus` (better performance)
- `npm install opusscript`
- a good network connection
The preferred opus engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.
## Joining a voice channel
The example below reacts to a message and joins the sender's voice channel, catching any errors. This is important
as it allows us to obtain a `VoiceConnection` that we can start to stream audio with.
@@ -24,19 +27,15 @@ const client = new Discord.Client();
client.login('token here');
client.on('message', message => {
client.on('message', async message => {
// Voice only works in guilds, if the message does not come from a guild,
// we ignore it
if (!message.guild) return;
if (message.content === '/join') {
// Only try to join the sender's voice channel if they are in one themselves
if (message.member.voiceChannel) {
message.member.voiceChannel.join()
.then(connection => { // Connection is an instance of VoiceConnection
message.reply('I have successfully connected to the channel!');
})
.catch(console.log);
if (message.member.voice.channel) {
const connection = await message.member.voice.channel.join();
} else {
message.reply('You need to join a voice channel first!');
}
@@ -45,69 +44,97 @@ client.on('message', message => {
```
## Streaming to a Voice Channel
In the previous example, we looked at how to join a voice channel in order to obtain a `VoiceConnection`. Now that we
have obtained a voice connection, we can start streaming audio to it. The following example shows how to stream an mp3
file:
have obtained a voice connection, we can start streaming audio to it.
### Introduction to playing on voice connections
The most basic example of playing audio over a connection would be playing a local file:
**Playing a file:**
```js
// To play a file, we need to give an absolute path to it
const dispatcher = connection.playFile('C:/Users/Discord/Desktop/myfile.mp3');
const dispatcher = connection.play('/home/discord/audio.mp3');
```
Your file doesn't have to be just an mp3; ffmpeg can convert videos and audios of many formats.
The `dispatcher` variable is an instance of a `StreamDispatcher`, which manages streaming a specific resource to a voice
channel. We can do many things with the dispatcher, such as finding out when the stream ends or changing the volume:
The `dispatcher` in this case is a `StreamDispatcher` - here you can control the volume and playback of the stream:
```js
dispatcher.on('end', () => {
// The song has finished
dispatcher.pause();
dispatcher.resume();
dispatcher.setVolume(0.5); // half the volume
dispatcher.on('finish', () => {
console.log('Finished playing!');
});
dispatcher.on('error', e => {
// Catch any errors that may arise
console.log(e);
dispatcher.destroy(); // end the stream
```
We can also pass in options when we first play the stream:
```js
const dispatcher = connection.play('/home/discord/audio.mp3', {
volume: 0.5,
});
```
### What can I play?
Discord.js allows you to play a lot of things:
```js
// ReadableStreams, in this example YouTube audio
const ytdl = require('ytdl-core');
connection.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' }));
// Files on the internet
connection.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
// Local files
connection.play('/home/discord/audio.mp3');
```
New to v12 is the ability to play OggOpus and WebmOpus streams with much better performance by skipping out Ffmpeg. Note this comes at the cost of no longer having volume control over the stream:
```js
connection.play(fs.createReadStream('./media.webm'), {
type: 'webm/opus',
});
dispatcher.setVolume(0.5); // Set the volume to 50%
dispatcher.setVolume(1); // Set the volume back to 100%
console.log(dispatcher.time); // The time in milliseconds that the stream dispatcher has been playing for
dispatcher.pause(); // Pause the stream
dispatcher.resume(); // Carry on playing
dispatcher.end(); // End the dispatcher, emits 'end' event
connection.play(fs.createReadStream('./media.ogg'), {
type: 'ogg/opus',
});
```
If you have an existing [ReadableStream](https://nodejs.org/api/stream.html#stream_readable_streams),
this can also be used:
Make sure to consult the documentation for a full list of what you can play - there's too much to cover here!
**Playing a ReadableStream:**
```js
connection.playStream(myReadableStream);
## Voice Broadcasts
// If you don't want to use absolute paths, you can use
// fs.createReadStream to circumvent it
const fs = require('fs');
const stream = fs.createReadStream('./test.mp3');
connection.playStream(stream);
```
It's important to note that creating a readable stream to a file is less efficient than simply using `connection.playFile()`.
**Playing anything else:**
For anything else, such as a URL to a file, you can use `connection.playArbitraryInput()`. You should consult the [ffmpeg protocol documentation](https://ffmpeg.org/ffmpeg-protocols.html) to see what you can use this for.
A voice broadcast is very useful for "radio" bots, that play the same audio across multiple channels. It means audio is only transcoded once, and is much better on performance.
```js
// Play an mp3 from a URL
connection.playArbitraryInput('http://mysite.com/sound.mp3');
const broadcast = client.voice.createBroadcast();
broadcast.on('subscribe', dispatcher => {
console.log('New broadcast subscriber!');
});
broadcast.on('unsubscribe', dispatcher => {
console.log('Channel unsubscribed from broadcast :(');
});
```
Again, playing a file from a URL like this is more performant than creating a ReadableStream to the file.
`broadcast` is an instance of `VoiceBroadcast`, which has the same `play` method you are used to with regular VoiceConnections:
## Advanced Topics
soon:tm:
```js
const dispatcher = broadcast.play('./audio.mp3');
connection.play(broadcast);
```
It's important to note that the `dispatcher` stored above is a `BroadcastDispatcher` - it controls all the dispatcher subscribed to the broadcast, e.g. setting the volume of this dispatcher affects the volume of all subscribers.
## Voice Receive
coming soon&trade;

View File

@@ -1,13 +1,32 @@
# Web builds
In addition to your usual Node applications, discord.js has special distributions available that are capable of running in web browsers.
This is useful for client-side web apps that need to interact with the Discord API.
[Webpack 3](https://webpack.js.org/) is used to build these.
## Usage
## Restrictions
- Any voice-related functionality is unavailable, as there is currently no audio encoding/decoding capabilities without external native libraries,
which web browsers do not support.
- The ShardingManager cannot be used, since it relies on being able to spawn child processes for shards.
- None of the native optional packages are usable.
### Require Library
If you are making your own webpack project, you can require `discord.js/browser` wherever you need to use discord.js, like so:
```js
const Discord = require('discord.js/browser');
// do something with Discord like you normally would
```
### Webpack File
You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/discordjs/discord.js/tree/webpack) of the GitHub repository.
There is a file for each branch and version of the library, and the ones ending in `.min.js` are minified to substantially reduce the size of the source code.
Include the file on the page just as you would any other JS library, like so:
```html
<script type="text/javascript" src="discord.VERSION.min.js"></script>
```
@@ -15,15 +34,10 @@ Include the file on the page just as you would any other JS library, like so:
Rather than importing discord.js with `require('discord.js')`, the entire `Discord` object is available as a global (on the `window`) object.
The usage of the API isn't any different from using it in Node.js.
## Restrictions
- Any voice-related functionality is unavailable, as there is currently no audio encoding/decoding capabilities without external native libraries,
which web browsers do not support.
- The ShardingManager cannot be used, since it relies on being able to spawn child processes for shards.
- None of the optional packages are usable, since they're native libraries.
#### Example
## Example
```html
<script type="text/javascript" src="discord.11.5.0.min.js"></script>
<script type="text/javascript" src="discord.11.1.0.min.js"></script>
<script type="text/javascript">
const client = new Discord.Client();

3
jsdoc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"plugins": ["node_modules/jsdoc-strip-async-await"]
}

View File

@@ -1,17 +1,19 @@
{
"name": "discord.js",
"version": "11.6.1",
"version": "12.0.0-dev",
"description": "A powerful library for interacting with the Discord API",
"main": "./src/index",
"types": "./typings/index.d.ts",
"scripts": {
"test": "npm run lint && npm run docs:test",
"test": "npm run lint && npm run docs:test && npm run lint:typings",
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml",
"lint": "eslint src",
"lint:fix": "eslint --fix src",
"lint:typings": "tslint typings/index.d.ts typings/discord.js-test.ts",
"webpack": "parallel-webpack"
"lint:fix": "eslint src --fix",
"lint:typings": "tslint typings/index.d.ts",
"prettier": "prettier --write --single-quote --print-width 120 --trailing-comma all --end-of-line lf src/**/*.js typings/**/*.ts",
"build:browser": "webpack",
"prepublishOnly": "npm run test && NODE_ENV=production npm run build:browser"
},
"repository": {
"type": "git",
@@ -32,22 +34,24 @@
},
"homepage": "https://github.com/discordjs/discord.js#readme",
"runkitExampleFilename": "./docs/examples/ping.js",
"unpkg": "./webpack/discord.min.js",
"dependencies": {
"long": "^4.0.0",
"prism-media": "^0.0.4",
"snekfetch": "^3.6.4",
"tweetnacl": "^1.0.0",
"ws": "^6.0.0"
"@discordjs/collection": "^0.1.5",
"abort-controller": "^3.0.0",
"form-data": "^3.0.0",
"node-fetch": "^2.6.0",
"prism-media": "^1.2.0",
"setimmediate": "^1.0.5",
"tweetnacl": "^1.0.3",
"ws": "^7.2.1"
},
"peerDependencies": {
"@discordjs/uws": "^10.149.0",
"bufferutil": "^4.0.0",
"bufferutil": "^4.0.1",
"erlpack": "discordapp/erlpack",
"libsodium-wrappers": "^0.7.3",
"@discordjs/opus": "^0.1.0",
"node-opus": "^0.2.7",
"opusscript": "^0.0.6",
"sodium": "^2.0.3"
"libsodium-wrappers": "^0.7.6",
"sodium": "^3.0.2",
"utf-8-validate": "^5.0.2",
"zlib-sync": "^0.1.6"
},
"peerDependenciesMeta": {
"bufferutil": {
@@ -56,72 +60,108 @@
"erlpack": {
"optional": true
},
"@discordjs/opus": {
"optional": true
},
"node-opus": {
"optional": true
},
"opusscript": {
"libsodium-wrappers": {
"optional": true
},
"sodium": {
"optional": true
},
"libsodium-wrappers": {
"utf-8-validate": {
"optional": true
},
"uws": {
"zlib-sync": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^9.4.6",
"@commitlint/cli": "^8.3.5",
"@commitlint/config-angular": "^8.3.4",
"@types/node": "^10.12.24",
"@types/ws": "^7.2.1",
"discord.js-docgen": "discordjs/docgen",
"eslint": "^5.4.0",
"parallel-webpack": "^2.3.0",
"tslint": "^3.15.1",
"tslint-config-typings": "^0.2.4",
"typescript": "^3.0.1",
"uglifyjs-webpack-plugin": "^1.3.0",
"webpack": "^4.17.0"
"dtslint": "^3.0.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.2",
"husky": "^4.2.3",
"jest": "^25.1.0",
"json-filter-loader": "^1.0.0",
"lint-staged": "^10.0.8",
"prettier": "^1.19.1",
"terser-webpack-plugin": "^1.2.2",
"tslint": "^6.0.0",
"typescript": "^3.8.2",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11"
},
"engines": {
"node": ">=6.0.0"
"node": ">=12.0.0"
},
"browser": {
"@discordjs/opus": false,
"https": false,
"ws": false,
"uws": false,
"@discordjs/uws": false,
"erlpack": false,
"prism-media": false,
"opusscript": false,
"node-opus": false,
"@discordjs/opus": false,
"tweetnacl": false,
"sodium": false,
"worker_threads": false,
"zlib-sync": false,
"src/sharding/Shard.js": false,
"src/sharding/ShardClientUtil.js": false,
"src/sharding/ShardingManager.js": false,
"src/client/voice/dispatcher/StreamDispatcher.js": false,
"src/client/voice/opus/BaseOpusEngine.js": false,
"src/client/voice/opus/NodeOpusEngine.js": false,
"src/client/voice/opus/DiscordJsOpusEngine.js": false,
"src/client/voice/opus/OpusEngineList.js": false,
"src/client/voice/opus/OpusScriptEngine.js": false,
"src/client/voice/pcm/ConverterEngine.js": false,
"src/client/voice/pcm/ConverterEngineList.js": false,
"src/client/voice/pcm/FfmpegConverterEngine.js": false,
"src/client/voice/player/AudioPlayer.js": false,
"src/client/voice/receiver/VoiceReadable.js": false,
"src/client/voice/receiver/VoiceReceiver.js": false,
"src/client/voice/util/Secretbox.js": false,
"src/client/voice/util/SecretKey.js": false,
"src/client/voice/util/VolumeInterface.js": false,
"src/client/voice/ClientVoiceManager.js": false,
"src/client/voice/VoiceBroadcast.js": false,
"src/client/voice/VoiceConnection.js": false,
"src/client/voice/VoiceUDPClient.js": false,
"src/client/voice/VoiceWebSocket.js": false
"src/client/voice/dispatcher/BroadcastDispatcher.js": false,
"src/client/voice/dispatcher/StreamDispatcher.js": false,
"src/client/voice/networking/VoiceUDPClient.js": false,
"src/client/voice/networking/VoiceWebSocket.js": false,
"src/client/voice/player/AudioPlayer.js": false,
"src/client/voice/player/BasePlayer.js": false,
"src/client/voice/player/BroadcastAudioPlayer.js": false,
"src/client/voice/receiver/PacketHandler.js": false,
"src/client/voice/receiver/Receiver.js": false,
"src/client/voice/util/PlayInterface.js": false,
"src/client/voice/util/Secretbox.js": false,
"src/client/voice/util/Silence.js": false,
"src/client/voice/util/VolumeInterface.js": false
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": "eslint --fix",
"*.ts": "prettier --write --single-quote --print-width 120 --trailing-comma all --end-of-line lf"
},
"commitlint": {
"extends": [
"@commitlint/config-angular"
],
"rules": {
"type-enum": [
2,
"always",
[
"chore",
"build",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test"
]
]
}
}
}

49
src/WebSocket.js Normal file
View File

@@ -0,0 +1,49 @@
'use strict';
const { browser } = require('./util/Constants');
let erlpack;
try {
erlpack = require('erlpack');
if (!erlpack.pack) erlpack = null;
} catch {} // eslint-disable-line no-empty
let TextDecoder;
if (browser) {
TextDecoder = window.TextDecoder; // eslint-disable-line no-undef
exports.WebSocket = window.WebSocket; // eslint-disable-line no-undef
} else {
TextDecoder = require('util').TextDecoder;
exports.WebSocket = require('ws');
}
const ab = new TextDecoder();
exports.encoding = erlpack ? 'etf' : 'json';
exports.pack = erlpack ? erlpack.pack : JSON.stringify;
exports.unpack = (data, type) => {
if (exports.encoding === 'json' || type === 'json') {
if (typeof data !== 'string') {
data = ab.decode(data);
}
return JSON.parse(data);
}
if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data));
return erlpack.unpack(data);
};
exports.create = (gateway, query = {}, ...args) => {
const [g, q] = gateway.split('?');
query.encoding = exports.encoding;
query = new URLSearchParams(query);
if (q) new URLSearchParams(q).forEach((v, k) => query.set(k, v));
const ws = new exports.WebSocket(`${g}?${query}`, ...args);
if (browser) ws.binaryType = 'arraybuffer';
return ws;
};
for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) exports[state] = exports.WebSocket[state];

147
src/client/BaseClient.js Normal file
View File

@@ -0,0 +1,147 @@
'use strict';
require('setimmediate');
const EventEmitter = require('events');
const RESTManager = require('../rest/RESTManager');
const { DefaultOptions } = require('../util/Constants');
const Util = require('../util/Util');
/**
* The base class for all clients.
* @extends {EventEmitter}
*/
class BaseClient extends EventEmitter {
constructor(options = {}) {
super();
/**
* Timeouts set by {@link BaseClient#setTimeout} that are still active
* @type {Set<Timeout>}
* @private
*/
this._timeouts = new Set();
/**
* Intervals set by {@link BaseClient#setInterval} that are still active
* @type {Set<Timeout>}
* @private
*/
this._intervals = new Set();
/**
* Intervals set by {@link BaseClient#setImmediate} that are still active
* @type {Set<Immediate>}
* @private
*/
this._immediates = new Set();
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(DefaultOptions, options);
/**
* The REST manager of the client
* @type {RESTManager}
* @private
*/
this.rest = new RESTManager(this, options._tokenType);
}
/**
* API shortcut
* @type {Object}
* @readonly
* @private
*/
get api() {
return this.rest.api;
}
/**
* Destroys all assets used by the base client.
*/
destroy() {
for (const t of this._timeouts) this.clearTimeout(t);
for (const i of this._intervals) this.clearInterval(i);
for (const i of this._immediates) this.clearImmediate(i);
this._timeouts.clear();
this._intervals.clear();
this._immediates.clear();
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait between executions (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
/**
* Sets an immediate that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {...*} args Arguments for the function
* @returns {Immediate}
*/
setImmediate(fn, ...args) {
const immediate = setImmediate(fn, ...args);
this._immediates.add(immediate);
return immediate;
}
/**
* Clears an immediate.
* @param {Immediate} immediate Immediate to cancel
*/
clearImmediate(immediate) {
clearImmediate(immediate);
this._immediates.delete(immediate);
}
toJSON(...props) {
return Util.flatten(this, { domain: false }, ...props);
}
}
module.exports = BaseClient;

View File

@@ -1,76 +1,84 @@
const EventEmitter = require('events');
const Constants = require('../util/Constants');
const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
const RESTManager = require('./rest/RESTManager');
const ClientDataManager = require('./ClientDataManager');
const ClientManager = require('./ClientManager');
const ClientDataResolver = require('./ClientDataResolver');
'use strict';
const BaseClient = require('./BaseClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
const ActionsManager = require('./actions/ActionsManager');
const Collection = require('../util/Collection');
const Presence = require('../structures/Presence').Presence;
const { Error, TypeError, RangeError } = require('../errors');
const ChannelManager = require('../managers/ChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildManager = require('../managers/GuildManager');
const UserManager = require('../managers/UserManager');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const VoiceBroadcast = require('./voice/VoiceBroadcast');
const ClientApplication = require('../structures/ClientApplication');
const Invite = require('../structures/Invite');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
const Collection = require('../util/Collection');
const { Events, browser, DefaultOptions } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Intents = require('../util/Intents');
const Permissions = require('../util/Permissions');
const Structures = require('../util/Structures');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
* @extends {EventEmitter}
* @extends {BaseClient}
*/
class Client extends EventEmitter {
class Client extends BaseClient {
/**
* @param {ClientOptions} [options] Options for the client
*/
constructor(options = {}) {
super();
super(Object.assign({ _tokenType: 'Bot' }, options));
// Obtain shard details from environment
if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID);
if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT);
// Obtain shard details from environment or if present, worker threads
let data = process.env;
try {
// Test if worker threads module is present and used
data = require('worker_threads').workerData || data;
} catch {
// Do nothing
}
if (this.options.shards === DefaultOptions.shards) {
if ('SHARDS' in data) {
this.options.shards = JSON.parse(data.SHARDS);
}
}
if (this.options.shardCount === DefaultOptions.shardCount) {
if ('SHARD_COUNT' in data) {
this.options.shardCount = Number(data.SHARD_COUNT);
} else if (Array.isArray(this.options.shards)) {
this.options.shardCount = this.options.shards.length;
}
}
const typeofShards = typeof this.options.shards;
if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') {
this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i);
}
if (typeofShards === 'number') this.options.shards = [this.options.shards];
if (Array.isArray(this.options.shards)) {
this.options.shards = [
...new Set(
this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)),
),
];
}
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
this._validateOptions();
/**
* The REST manager of the client
* @type {RESTManager}
* @private
*/
this.rest = new RESTManager(this);
/**
* The data manager of the client
* @type {ClientDataManager}
* @private
*/
this.dataManager = new ClientDataManager(this);
/**
* The manager of the client
* @type {ClientManager}
* @private
*/
this.manager = new ClientManager(this);
/**
* The WebSocket manager of the client
* @type {WebSocketManager}
* @private
*/
this.ws = new WebSocketManager(this);
/**
* The data resolver of the client
* @type {ClientDataResolver}
* @private
*/
this.resolver = new ClientDataResolver(this);
/**
* The action manager of the client
* @type {ActionsManager}
@@ -81,53 +89,56 @@ class Client extends EventEmitter {
/**
* The voice manager of the client (`null` in browsers)
* @type {?ClientVoiceManager}
* @private
*/
this.voice = !this.browser ? new ClientVoiceManager(this) : null;
this.voice = !browser ? new ClientVoiceManager(this) : null;
/**
* The shard helpers for the client
* (only if the process was spawned as a child, such as from a {@link ShardingManager})
* Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
* @type {?ShardClientUtil}
*/
this.shard = process.send ? ShardClientUtil.singleton(this) : null;
this.shard =
!browser && process.env.SHARDING_MANAGER
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
: null;
/**
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {Collection<Snowflake, User>}
* @type {UserManager}
*/
this.users = new Collection();
this.users = new UserManager(this);
/**
* All of the guilds the client is currently handling, mapped by their IDs -
* as long as sharding isn't being used, this will be *every* guild the bot is a member of
* @type {Collection<Snowflake, Guild>}
* @type {GuildManager}
*/
this.guilds = new Collection();
this.guilds = new GuildManager(this);
/**
* All of the {@link Channel}s that the client is currently handling, mapped by their IDs -
* as long as sharding isn't being used, this will be *every* channel in *every* guild, and all DM channels
* @type {Collection<Snowflake, Channel>}
* as long as sharding isn't being used, this will be *every* channel in *every* guild the bot
* is a member of. Note that DM channels will not be initially cached, and thus not be present
* in the Manager without their explicit fetching or use.
* @type {ChannelManager}
*/
this.channels = new Collection();
this.channels = new ChannelManager(this);
const ClientPresence = Structures.get('ClientPresence');
/**
* Presences that have been received for the client user's friends, mapped by user IDs
* <warn>This is only filled when using a user account.</warn>
* @type {Collection<Snowflake, Presence>}
* @deprecated
* The presence of the Client
* @private
* @type {ClientPresence}
*/
this.presences = new Collection();
this.presence = new ClientPresence(this);
Object.defineProperty(this, 'token', { writable: true });
if (!this.token && 'CLIENT_TOKEN' in process.env) {
if (!browser && !this.token && 'DISCORD_TOKEN' in process.env) {
/**
* Authorization token for the logged in user/bot
* Authorization token for the logged in bot
* <warn>This should be kept private at all times.</warn>
* @type {?string}
*/
this.token = process.env.CLIENT_TOKEN;
this.token = process.env.DISCORD_TOKEN;
} else {
this.token = null;
}
@@ -145,92 +156,20 @@ class Client extends EventEmitter {
*/
this.readyAt = null;
/**
* Active voice broadcasts that have been created
* @type {VoiceBroadcast[]}
*/
this.broadcasts = [];
/**
* Previous heartbeat pings of the websocket (most recent first, limited to three elements)
* @type {number[]}
*/
this.pings = [];
/**
* Timeouts set by {@link Client#setTimeout} that are still active
* @type {Set<Timeout>}
* @private
*/
this._timeouts = new Set();
/**
* Intervals set by {@link Client#setInterval} that are still active
* @type {Set<Timeout>}
* @private
*/
this._intervals = new Set();
if (this.options.messageSweepInterval > 0) {
this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000);
}
}
/**
* Timestamp of the latest ping's start time
* @type {number}
* @private
*/
get _pingTimestamp() {
return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0;
}
/**
* Current status of the client's connection to Discord
* @type {Status}
* @readonly
*/
get status() {
return this.ws.connection ? this.ws.connection.status : Constants.Status.IDLE;
}
/**
* How long it has been since the client last entered the `READY` state in milliseconds
* @type {?number}
* @readonly
*/
get uptime() {
return this.readyAt ? Date.now() - this.readyAt : null;
}
/**
* Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property
* @type {number}
* @readonly
*/
get ping() {
return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length;
}
/**
* All active voice connections that have been established, mapped by guild ID
* @type {Collection<Snowflake, VoiceConnection>}
* @readonly
*/
get voiceConnections() {
if (this.browser) return new Collection();
return this.voice.connections;
}
/**
* All custom emojis that the client has access to, mapped by their IDs
* @type {Collection<Snowflake, Emoji>}
* @type {GuildEmojiManager}
* @readonly
*/
get emojis() {
const emojis = new Collection();
for (const guild of this.guilds.values()) {
for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji);
const emojis = new GuildEmojiManager({ client: this });
for (const guild of this.guilds.cache.values()) {
if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji);
}
return emojis;
}
@@ -245,78 +184,55 @@ class Client extends EventEmitter {
}
/**
* Whether the client is in a browser environment
* @type {boolean}
* How long it has been since the client last entered the `READY` state in milliseconds
* @type {?number}
* @readonly
*/
get browser() {
return typeof window !== 'undefined';
}
/**
* Creates a voice broadcast.
* @returns {VoiceBroadcast}
*/
createVoiceBroadcast() {
const broadcast = new VoiceBroadcast(this);
this.broadcasts.push(broadcast);
return broadcast;
get uptime() {
return this.readyAt ? Date.now() - this.readyAt : null;
}
/**
* Logs the client in, establishing a websocket connection to Discord.
* <info>Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever
* possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts.
* Bot accounts also have access to many features that user accounts cannot utilise. Automating a user account is
* considered a violation of Discord's ToS.</info>
* @param {string} token Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token')
* .then(console.log)
* .catch(console.error);
* client.login('my token');
*/
login(token = this.token) {
return this.rest.methods.login(token);
async login(token = this.token) {
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
this.emit(
Events.DEBUG,
`Provided token: ${token
.split('.')
.map((val, i) => (i > 1 ? val.replace(/./g, '*') : val))
.join('.')}`,
);
if (this.options.presence) {
this.options.ws.presence = await this.presence._parse(this.options.presence);
}
this.emit(Events.DEBUG, 'Preparing to connect to the gateway...');
try {
await this.ws.connect();
return this.token;
} catch (error) {
this.destroy();
throw error;
}
}
/**
* Logs out, terminates the connection to Discord, and destroys the client.
* @returns {Promise}
* @returns {void}
*/
destroy() {
for (const t of this._timeouts) clearTimeout(t);
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
return this.manager.destroy();
}
/**
* Requests a sync of guild data with Discord.
* <info>This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}.</info>
* <warn>This is only available when using a user account.</warn>
* @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
* @deprecated
*/
syncGuilds(guilds = this.guilds) {
if (this.user.bot) return;
this.ws.send({
op: 12,
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
});
}
/**
* Obtains a user from Discord, or the user cache if it's already available.
* <warn>This is only available when using a bot account.</warn>
* @param {Snowflake} id ID of the user
* @param {boolean} [cache=true] Whether to cache the new user object if it isn't already
* @returns {Promise<User>}
*/
fetchUser(id, cache = true) {
if (this.users.has(id)) return Promise.resolve(this.users.get(id));
return this.rest.methods.getUser(id, cache);
super.destroy();
this.ws.destroy();
this.token = null;
}
/**
@@ -329,8 +245,11 @@ class Client extends EventEmitter {
* .catch(console.error);
*/
fetchInvite(invite) {
const code = this.resolver.resolveInviteCode(invite);
return this.rest.methods.getInvite(code);
const code = DataResolver.resolveInviteCode(invite);
return this.api
.invites(code)
.get({ query: { with_counts: true } })
.then(data => new Invite(this, data));
}
/**
@@ -344,7 +263,10 @@ class Client extends EventEmitter {
* .catch(console.error);
*/
fetchWebhook(id, token) {
return this.rest.methods.getWebhook(id, token);
return this.api
.webhooks(id, token)
.get()
.then(data => new Webhook(this, data));
}
/**
@@ -356,7 +278,11 @@ class Client extends EventEmitter {
* .catch(console.error);
*/
fetchVoiceRegions() {
return this.rest.methods.fetchVoiceRegions();
return this.api.voice.regions.get().then(res => {
const regions = new Collection();
for (const region of res) regions.set(region.id, new VoiceRegion(region));
return regions;
});
}
/**
@@ -366,11 +292,17 @@ class Client extends EventEmitter {
* will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime}
* @returns {number} Amount of messages that were removed from the caches,
* or -1 if the message cache lifetime is unlimited
* @example
* // Remove all messages older than 1800 seconds from the messages cache
* const amount = client.sweepMessages(1800);
* console.log(`Successfully removed ${amount} messages from the cache.`);
*/
sweepMessages(lifetime = this.options.messageCacheLifetime) {
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.');
if (typeof lifetime !== 'number' || isNaN(lifetime)) {
throw new TypeError('INVALID_TYPE', 'lifetime', 'number');
}
if (lifetime <= 0) {
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
this.emit(Events.DEBUG, "Didn't sweep messages - lifetime is unlimited");
return -1;
}
@@ -379,37 +311,35 @@ class Client extends EventEmitter {
let channels = 0;
let messages = 0;
for (const channel of this.channels.values()) {
for (const channel of this.channels.cache.values()) {
if (!channel.messages) continue;
channels++;
messages += channel.messages.sweep(
message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs
messages += channel.messages.cache.sweep(
message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs,
);
}
this.emit('debug', `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`);
this.emit(
Events.DEBUG,
`Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`,
);
return messages;
}
/**
* Obtains the OAuth Application of the bot from Discord.
* <warn>Bots can only fetch their own profile.</warn>
* @param {Snowflake} [id='@me'] ID of application to fetch
* @returns {Promise<OAuth2Application>}
* @example
* client.fetchApplication()
* .then(application => console.log(`Obtained application with name: ${application.name}`))
* .catch(console.error);
* Obtains the OAuth Application of this bot from Discord.
* @returns {Promise<ClientApplication>}
*/
fetchApplication(id = '@me') {
if (id !== '@me') process.emitWarning('fetchApplication: use "@me" as an argument', 'DeprecationWarning');
return this.rest.methods.getApplication(id);
fetchApplication() {
return this.api.oauth2
.applications('@me')
.get()
.then(app => new ClientApplication(this, app));
}
/**
* Generates a link that can be used to invite the bot to a guild.
* <warn>This is only available when using a bot account.</warn>
* @param {PermissionResolvable} [permissions] Permissions to request
* @returns {Promise<string>}
* @example
@@ -417,83 +347,22 @@ class Client extends EventEmitter {
* .then(link => console.log(`Generated bot invite link: ${link}`))
* .catch(console.error);
*/
generateInvite(permissions) {
async generateInvite(permissions) {
permissions = Permissions.resolve(permissions);
return this.fetchApplication().then(application =>
`https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot`
);
const application = await this.fetchApplication();
const query = new URLSearchParams({
client_id: application.id,
permissions: permissions,
scope: 'bot',
});
return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
/**
* Adds a ping to {@link Client#pings}.
* @param {number} startTime Starting time of the ping
* @private
*/
_pong(startTime) {
this.pings.unshift(Date.now() - startTime);
if (this.pings.length > 3) this.pings.length = 3;
this.ws.lastHeartbeatAck = true;
}
/**
* Adds/updates a friend's presence in {@link Client#presences}.
* @param {Snowflake} id ID of the user
* @param {Object} presence Raw presence object from Discord
* @private
*/
_setPresence(id, presence) {
if (this.presences.has(id)) {
this.presences.get(id).update(presence);
return;
}
this.presences.set(id, new Presence(presence, this));
toJSON() {
return super.toJSON({
readyAt: false,
presences: false,
});
}
/**
@@ -512,39 +381,46 @@ class Client extends EventEmitter {
* @param {ClientOptions} [options=this.options] Options to validate
* @private
*/
_validateOptions(options = this.options) { // eslint-disable-line complexity
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
throw new TypeError('The shardCount option must be a number.');
_validateOptions(options = this.options) {
if (typeof options.ws.intents !== 'undefined') {
options.ws.intents = Intents.resolve(options.ws.intents);
}
if (typeof options.shardId !== 'number' || isNaN(options.shardId)) {
throw new TypeError('The shardId option must be a number.');
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) {
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1');
}
if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.');
if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.');
if (options.shardId !== 0 && options.shardId >= options.shardCount) {
throw new RangeError('The shardId option must be less than shardCount.');
if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) {
throw new TypeError('CLIENT_INVALID_OPTION', 'shards', "'auto', a number or array of numbers");
}
if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS');
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
throw new TypeError('The messageCacheMaxSize option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number');
}
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
throw new TypeError('The messageCacheLifetime option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number');
}
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('The messageSweepInterval option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number');
}
if (typeof options.fetchAllMembers !== 'boolean') {
throw new TypeError('The fetchAllMembers option must be a boolean.');
throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean');
}
if (typeof options.disableEveryone !== 'boolean') {
throw new TypeError('The disableEveryone option must be a boolean.');
if (typeof options.disableMentions !== 'string') {
throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a string');
}
if (!Array.isArray(options.partials)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
}
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
throw new TypeError('The restWsBridgeTimeout option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number');
}
if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number');
}
if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number');
}
if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.');
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
throw new TypeError('The retryLimit options must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
}
}
}

View File

@@ -1,149 +0,0 @@
const Constants = require('../util/Constants');
const Util = require('../util/Util');
const Guild = require('../structures/Guild');
const User = require('../structures/User');
const Emoji = require('../structures/Emoji');
const GuildChannel = require('../structures/GuildChannel');
const TextChannel = require('../structures/TextChannel');
const VoiceChannel = require('../structures/VoiceChannel');
const CategoryChannel = require('../structures/CategoryChannel');
const NewsChannel = require('../structures/NewsChannel');
const StoreChannel = require('../structures/StoreChannel');
const DMChannel = require('../structures/DMChannel');
const GroupDMChannel = require('../structures/GroupDMChannel');
class ClientDataManager {
constructor(client) {
this.client = client;
}
get pastReady() {
return this.client.ws.connection.status === Constants.Status.READY;
}
newGuild(data) {
const already = this.client.guilds.has(data.id);
const guild = new Guild(this.client, data);
this.client.guilds.set(guild.id, guild);
if (this.pastReady && !already) {
/**
* Emitted whenever the client joins a guild.
* @event Client#guildCreate
* @param {Guild} guild The created guild
*/
if (this.client.options.fetchAllMembers) {
guild.fetchMembers().then(() => { this.client.emit(Constants.Events.GUILD_CREATE, guild); });
} else {
this.client.emit(Constants.Events.GUILD_CREATE, guild);
}
}
return guild;
}
newUser(data, cache = true) {
if (this.client.users.has(data.id)) return this.client.users.get(data.id);
const user = new User(this.client, data);
if (cache) this.client.users.set(user.id, user);
return user;
}
newChannel(data, guild) {
const already = this.client.channels.has(data.id);
let channel;
if (data.type === Constants.ChannelTypes.DM) {
channel = new DMChannel(this.client, data);
} else if (data.type === Constants.ChannelTypes.GROUP_DM) {
channel = new GroupDMChannel(this.client, data);
} else {
guild = guild || this.client.guilds.get(data.guild_id);
if (already) {
channel = this.client.channels.get(data.id);
} else if (guild) {
switch (data.type) {
case Constants.ChannelTypes.TEXT:
channel = new TextChannel(guild, data);
break;
case Constants.ChannelTypes.VOICE:
channel = new VoiceChannel(guild, data);
break;
case Constants.ChannelTypes.CATEGORY:
channel = new CategoryChannel(guild, data);
break;
case Constants.ChannelTypes.NEWS:
channel = new NewsChannel(guild, data);
break;
case Constants.ChannelTypes.STORE:
channel = new StoreChannel(guild, data);
break;
}
guild.channels.set(channel.id, channel);
}
}
if (channel && !already) {
if (this.pastReady) this.client.emit(Constants.Events.CHANNEL_CREATE, channel);
this.client.channels.set(channel.id, channel);
return channel;
} else if (already) {
return channel;
}
return null;
}
newEmoji(data, guild) {
const already = guild.emojis.has(data.id);
if (data && !already) {
let emoji = new Emoji(guild, data);
this.client.emit(Constants.Events.GUILD_EMOJI_CREATE, emoji);
guild.emojis.set(emoji.id, emoji);
return emoji;
} else if (already) {
return guild.emojis.get(data.id);
}
return null;
}
killEmoji(emoji) {
if (!(emoji instanceof Emoji && emoji.guild)) return;
this.client.emit(Constants.Events.GUILD_EMOJI_DELETE, emoji);
emoji.guild.emojis.delete(emoji.id);
}
killGuild(guild) {
const already = this.client.guilds.has(guild.id);
this.client.guilds.delete(guild.id);
if (already && this.pastReady) this.client.emit(Constants.Events.GUILD_DELETE, guild);
}
killUser(user) {
this.client.users.delete(user.id);
}
killChannel(channel) {
this.client.channels.delete(channel.id);
if (channel instanceof GuildChannel) channel.guild.channels.delete(channel.id);
}
updateGuild(currentGuild, newData) {
const oldGuild = Util.cloneObject(currentGuild);
currentGuild.setup(newData);
if (this.pastReady) this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild);
}
updateChannel(currentChannel, newData) {
currentChannel.setup(newData);
}
updateEmoji(currentEmoji, newData) {
const oldEmoji = Util.cloneObject(currentEmoji);
currentEmoji.setup(newData);
this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji);
return currentEmoji;
}
}
module.exports = ClientDataManager;

View File

@@ -1,376 +0,0 @@
const path = require('path');
const fs = require('fs');
const snekfetch = require('snekfetch');
const Constants = require('../util/Constants');
const convertToBuffer = require('../util/Util').convertToBuffer;
const User = require('../structures/User');
const Message = require('../structures/Message');
const Guild = require('../structures/Guild');
const Channel = require('../structures/Channel');
const GuildMember = require('../structures/GuildMember');
const Emoji = require('../structures/Emoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const Role = require('../structures/Role');
/**
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
* extracting a User from a Message object.
* @private
*/
class ClientDataResolver {
/**
* @param {Client} client The client the resolver is for
*/
constructor(client) {
this.client = client;
}
/**
* Data that resolves to give a User object. This can be:
* * A User object
* * A Snowflake
* * A Message object (resolves to the message author)
* * A Guild object (owner of the guild)
* * A GuildMember object
* @typedef {User|Snowflake|Message|Guild|GuildMember} UserResolvable
*/
/**
* Resolves a UserResolvable to a User object.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?User}
*/
resolveUser(user) {
if (user instanceof User) return user;
if (typeof user === 'string') return this.client.users.get(user) || null;
if (user instanceof GuildMember) return user.user;
if (user instanceof Message) return user.author;
if (user instanceof Guild) return this.resolveUser(user.ownerID);
return null;
}
/**
* Resolves a UserResolvable to a user ID string.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?Snowflake}
*/
resolveUserID(user) {
if (user instanceof User || user instanceof GuildMember) return user.id;
if (typeof user === 'string') return user || null;
if (user instanceof Message) return user.author.id;
if (user instanceof Guild) return user.ownerID;
return null;
}
/**
* Data that resolves to give a Guild object. This can be:
* * A Guild object
* * A Snowflake
* @typedef {Guild|Snowflake} GuildResolvable
*/
/**
* Resolves a GuildResolvable to a Guild object.
* @param {GuildResolvable} guild The GuildResolvable to identify
* @returns {?Guild}
*/
resolveGuild(guild) {
if (guild instanceof Guild) return guild;
if (typeof guild === 'string') return this.client.guilds.get(guild) || null;
return null;
}
/**
* Data that resolves to give a GuildMember object. This can be:
* * A GuildMember object
* * A User object
* @typedef {GuildMember|User} GuildMemberResolvable
*/
/**
* Resolves a GuildMemberResolvable to a GuildMember object.
* @param {GuildResolvable} guild The guild that the member is part of
* @param {UserResolvable} user The user that is part of the guild
* @returns {?GuildMember}
*/
resolveGuildMember(guild, user) {
if (user instanceof GuildMember) return user;
guild = this.resolveGuild(guild);
user = this.resolveUser(user);
if (!guild || !user) return null;
return guild.members.get(user.id) || null;
}
/**
* Data that can be resolved to a Role object. This can be:
* * A Role
* * A Snowflake
* @typedef {Role|Snowflake} RoleResolvable
*/
/**
* Resolves a RoleResolvable to a Role object.
* @param {GuildResolvable} guild The guild that this role is part of
* @param {RoleResolvable} role The role resolvable to resolve
* @returns {?Role}
*/
resolveRole(guild, role) {
if (role instanceof Role) return role;
guild = this.resolveGuild(guild);
if (!guild) return null;
if (typeof role === 'string') return guild.roles.get(role);
return null;
}
/**
* Data that can be resolved to give a Channel object. This can be:
* * A Channel object
* * A Message object (the channel the message was sent in)
* * A Guild object (the #general channel)
* * A Snowflake
* @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable
*/
/**
* Resolves a ChannelResolvable to a Channel object.
* @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?Channel}
*/
resolveChannel(channel) {
if (channel instanceof Channel) return channel;
if (typeof channel === 'string') return this.client.channels.get(channel) || null;
if (channel instanceof Message) return channel.channel;
if (channel instanceof Guild) return channel.channels.get(channel.id) || null;
return null;
}
/**
* Resolves a ChannelResolvable to a channel ID.
* @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?Snowflake}
*/
resolveChannelID(channel) {
if (channel instanceof Channel) return channel.id;
if (typeof channel === 'string') return channel;
if (channel instanceof Message) return channel.channel.id;
if (channel instanceof Guild) return channel.defaultChannel.id;
return null;
}
/**
* Data that can be resolved to give an invite code. This can be:
* * An invite code
* * An invite URL
* @typedef {string} InviteResolvable
*/
/**
* Resolves InviteResolvable to an invite code.
* @param {InviteResolvable} data The invite resolvable to resolve
* @returns {string}
*/
resolveInviteCode(data) {
const inviteRegex = /discord(?:app\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/i;
const match = inviteRegex.exec(data);
if (match && match[1]) return match[1];
return data;
}
/**
* Data that can be resolved to give a string. This can be:
* * A string
* * An array (joined with a new line delimiter to give a string)
* * Any value
* @typedef {string|Array|*} StringResolvable
*/
/**
* Resolves a StringResolvable to a string.
* @param {StringResolvable} data The string resolvable to resolve
* @returns {string}
*/
resolveString(data) {
if (typeof data === 'string') return data;
if (data instanceof Array) return data.join('\n');
return String(data);
}
/**
* Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image.
* @param {BufferResolvable|Base64Resolvable} image The image to be resolved
* @returns {Promise<?string>}
*/
resolveImage(image) {
if (!image) return Promise.resolve(null);
if (typeof image === 'string' && image.startsWith('data:')) {
return Promise.resolve(image);
}
return this.resolveFile(image).then(this.resolveBase64);
}
/**
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
* * A Buffer
* * A base64 string
* @typedef {Buffer|string} Base64Resolvable
*/
/**
* Resolves a Base64Resolvable to a Base 64 image.
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
* @returns {?string}
*/
resolveBase64(data) {
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
return data;
}
/**
* Data that can be resolved to give a Buffer. This can be:
* * A Buffer
* * The path to a local file
* * A URL
* * A Stream
* @typedef {string|Buffer} BufferResolvable
*/
/**
* @external Stream
* @see {@link https://nodejs.org/api/stream.html}
*/
/**
* Resolves a BufferResolvable to a Buffer.
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
* @returns {Promise<Buffer>}
*/
resolveFile(resource) {
if (resource instanceof Buffer) return Promise.resolve(resource);
if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource));
if (typeof resource === 'string') {
if (/^https?:\/\//.test(resource)) {
return snekfetch.get(resource).then(res => res.body instanceof Buffer ? res.body : Buffer.from(res.text));
}
return new Promise((resolve, reject) => {
const file = path.resolve(resource);
fs.stat(file, (err, stats) => {
if (err) return reject(err);
if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`));
fs.readFile(file, (err2, data) => {
if (err2) reject(err2);
else resolve(data);
});
return null;
});
});
} else if (resource && resource.pipe && typeof resource.pipe === 'function') {
return new Promise((resolve, reject) => {
const buffers = [];
resource.once('error', reject);
resource.on('data', data => buffers.push(data));
resource.once('end', () => resolve(Buffer.concat(buffers)));
});
}
return Promise.reject(new TypeError('The resource must be a string or Buffer.'));
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * A custom emoji ID
* * An Emoji object
* * A ReactionEmoji object
* @typedef {string|Emoji|ReactionEmoji} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveEmojiIdentifier(emoji) {
if (emoji instanceof Emoji || emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
if (this.client.emojis.has(emoji)) return this.client.emojis.get(emoji).identifier;
else if (!emoji.includes('%')) return encodeURIComponent(emoji);
else return emoji;
}
return null;
}
/**
* Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following
* ```
* [
* 'DEFAULT',
* 'WHITE',
* 'AQUA',
* 'GREEN',
* 'BLUE',
* 'PURPLE',
* 'LUMINOUS_VIVID_PINK',
* 'GOLD',
* 'ORANGE',
* 'RED',
* 'GREY',
* 'DARKER_GREY',
* 'NAVY',
* 'DARK_AQUA',
* 'DARK_GREEN',
* 'DARK_BLUE',
* 'DARK_PURPLE',
* 'DARK_VIVID_PINK',
* 'DARK_GOLD',
* 'DARK_ORANGE',
* 'DARK_RED',
* 'DARK_GREY',
* 'LIGHT_GREY',
* 'DARK_NAVY',
* 'RANDOM',
* ]
* ```
* or something like
* ```
* [255, 0, 255]
* ```
* for purple
* @typedef {string|number|Array} ColorResolvable
*/
/**
* Resolves a ColorResolvable into a color number.
* @param {ColorResolvable} color Color to resolve
* @returns {number} A color
*/
static resolveColor(color) {
if (typeof color === 'string') {
if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1));
if (color === 'DEFAULT') return 0;
color = Constants.Colors[color] || parseInt(color.replace('#', ''), 16);
} else if (color instanceof Array) {
color = (color[0] << 16) + (color[1] << 8) + color[2];
}
if (color < 0 || color > 0xFFFFFF) {
throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).');
} else if (color && isNaN(color)) {
throw new TypeError('Unable to convert color to a number.');
}
return color;
}
/**
* @param {ColorResolvable} color Color to resolve
* @returns {number} A color
*/
resolveColor(color) {
return this.constructor.resolveColor(color);
}
}
module.exports = ClientDataResolver;

View File

@@ -1,74 +0,0 @@
const Constants = require('../util/Constants');
const WebSocketConnection = require('./websocket/WebSocketConnection');
/**
* Manages the state and background tasks of the client.
* @private
*/
class ClientManager {
constructor(client) {
/**
* The client that instantiated this Manager
* @type {Client}
*/
this.client = client;
/**
* The heartbeat interval
* @type {?number}
*/
this.heartbeatInterval = null;
}
/**
* The status of the client
* @type {number}
*/
get status() {
return this.connection ? this.connection.status : Constants.Status.IDLE;
}
/**
* Connects the client to the WebSocket.
* @param {string} token The authorization token
* @param {Function} resolve Function to run when connection is successful
* @param {Function} reject Function to run when connection fails
*/
connectToWebSocket(token, resolve, reject) {
this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`);
this.client.token = token;
const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300);
this.client.rest.methods.getGateway().then(res => {
const protocolVersion = Constants.DefaultOptions.ws.version;
const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`;
this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`);
this.client.ws.connect(gateway);
this.client.ws.connection.once('error', reject);
this.client.ws.connection.once('close', event => {
if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN));
if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD));
if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED));
});
this.client.once(Constants.Events.READY, () => {
resolve(token);
this.client.clearTimeout(timeout);
});
}, reject);
}
destroy() {
this.client.ws.destroy();
this.client.rest.destroy();
if (!this.client.user) return Promise.resolve();
if (this.client.user.bot) {
this.client.token = null;
return Promise.resolve();
} else {
return this.client.rest.methods.logout().then(() => {
this.client.token = null;
});
}
}
}
module.exports = ClientManager;

View File

@@ -1,14 +1,14 @@
'use strict';
const BaseClient = require('./BaseClient');
const Webhook = require('../structures/Webhook');
const RESTManager = require('./rest/RESTManager');
const ClientDataResolver = require('./ClientDataResolver');
const Constants = require('../util/Constants');
const Util = require('../util/Util');
/**
* The webhook client.
* @extends {Webhook}
* @implements {Webhook}
* @extends {BaseClient}
*/
class WebhookClient extends Webhook {
class WebhookClient extends BaseClient {
/**
* @param {Snowflake} id ID of the webhook
* @param {string} token Token of the webhook
@@ -16,103 +16,16 @@ class WebhookClient extends Webhook {
* @example
* // Create a new webhook and send a message
* const hook = new Discord.WebhookClient('1234', 'abcdef');
* hook.sendMessage('This will send a message').catch(console.error);
* hook.send('This will send a message').catch(console.error);
*/
constructor(id, token, options) {
super(null, id, token);
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
/**
* The REST manager of the client
* @type {RESTManager}
* @private
*/
this.rest = new RESTManager(this);
/**
* The data resolver of the client
* @type {ClientDataResolver}
* @private
*/
this.resolver = new ClientDataResolver(this);
/**
* Timeouts set by {@link WebhookClient#setTimeout} that are still active
* @type {Set<Timeout>}
* @private
*/
this._timeouts = new Set();
/**
* Intervals set by {@link WebhookClient#setInterval} that are still active
* @type {Set<Timeout>}
* @private
*/
this._intervals = new Set();
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
/**
* Destroys the client.
*/
destroy() {
for (const t of this._timeouts) clearTimeout(t);
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
super(options);
Object.defineProperty(this, 'client', { value: this });
this.id = id;
Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true });
}
}
Webhook.applyToClass(WebhookClient);
module.exports = WebhookClient;

View File

@@ -1,3 +1,7 @@
'use strict';
const { PartialTypes } = require('../../util/Constants');
/*
ABOUT ACTIONS
@@ -18,6 +22,82 @@ class GenericAction {
handle(data) {
return data;
}
getPayload(data, manager, id, partialType, cache) {
const existing = manager.cache.get(id);
if (!existing && this.client.options.partials.includes(partialType)) {
return manager.add(data, cache);
}
return existing;
}
getChannel(data) {
const id = data.channel_id || data.id;
return (
data.channel ||
this.getPayload(
{
id,
guild_id: data.guild_id,
recipients: [data.author || { id: data.user_id }],
},
this.client.channels,
id,
PartialTypes.CHANNEL,
)
);
}
getMessage(data, channel, cache) {
const id = data.message_id || data.id;
return (
data.message ||
this.getPayload(
{
id,
channel_id: channel.id,
guild_id: data.guild_id || (channel.guild ? channel.guild.id : null),
},
channel.messages,
id,
PartialTypes.MESSAGE,
cache,
)
);
}
getReaction(data, message, user) {
const id = data.emoji.id || decodeURIComponent(data.emoji.name);
return this.getPayload(
{
emoji: data.emoji,
count: message.partial ? null : 0,
me: user ? user.id === this.client.user.id : false,
},
message.reactions,
id,
PartialTypes.REACTION,
);
}
getMember(data, guild) {
const id = data.user.id;
return this.getPayload(
{
user: {
id,
},
},
guild.members,
id,
PartialTypes.GUILD_MEMBER,
);
}
getUser(data) {
const id = data.user_id;
return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER);
}
}
module.exports = GenericAction;

View File

@@ -1,3 +1,5 @@
'use strict';
class ActionsManager {
constructor(client) {
this.client = client;
@@ -8,31 +10,31 @@ class ActionsManager {
this.register(require('./MessageUpdate'));
this.register(require('./MessageReactionAdd'));
this.register(require('./MessageReactionRemove'));
this.register(require('./MessageReactionRemoveEmoji'));
this.register(require('./MessageReactionRemoveAll'));
this.register(require('./MessageReactionRemoveEmoji'));
this.register(require('./ChannelCreate'));
this.register(require('./ChannelDelete'));
this.register(require('./ChannelUpdate'));
this.register(require('./GuildDelete'));
this.register(require('./GuildUpdate'));
this.register(require('./GuildMemberGet'));
this.register(require('./InviteCreate'));
this.register(require('./InviteDelete'));
this.register(require('./GuildMemberRemove'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
this.register(require('./GuildRoleUpdate'));
this.register(require('./InviteCreate'));
this.register(require('./InviteDelete'));
this.register(require('./UserGet'));
this.register(require('./PresenceUpdate'));
this.register(require('./UserUpdate'));
this.register(require('./UserNoteUpdate'));
this.register(require('./GuildSync'));
this.register(require('./VoiceStateUpdate'));
this.register(require('./GuildEmojiCreate'));
this.register(require('./GuildEmojiDelete'));
this.register(require('./GuildEmojiUpdate'));
this.register(require('./GuildEmojisUpdate'));
this.register(require('./GuildRolesPositionUpdate'));
this.register(require('./GuildChannelsPositionUpdate'));
this.register(require('./GuildIntegrationsUpdate'));
this.register(require('./WebhooksUpdate'));
}
register(Action) {

View File

@@ -1,9 +1,21 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class ChannelCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.dataManager.newChannel(data);
const existing = client.channels.cache.has(data.id);
const channel = client.channels.add(data);
if (!existing && channel) {
/**
* Emitted whenever a channel is created.
* @event Client#channelCreate
* @param {DMChannel|GuildChannel} channel The channel that was created
*/
client.emit(Events.CHANNEL_CREATE, channel);
}
return { channel };
}
}

View File

@@ -1,5 +1,8 @@
'use strict';
const Action = require('./Action');
const DMChannel = require('../../structures/DMChannel');
const { Events } = require('../../util/Constants');
class ChannelDeleteAction extends Action {
constructor(client) {
@@ -9,30 +12,26 @@ class ChannelDeleteAction extends Action {
handle(data) {
const client = this.client;
let channel = client.channels.cache.get(data.id);
let channel = client.channels.get(data.id);
if (channel) {
client.dataManager.killChannel(channel);
this.deleted.set(channel.id, channel);
this.scheduleForDeletion(channel.id);
} else {
channel = this.deleted.get(data.id) || null;
}
if (channel) {
client.channels.remove(channel.id);
channel.deleted = true;
if (channel.messages && !(channel instanceof DMChannel)) {
for (const message of channel.messages.values()) {
for (const message of channel.messages.cache.values()) {
message.deleted = true;
}
}
channel.deleted = true;
/**
* Emitted whenever a channel is deleted.
* @event Client#channelDelete
* @param {DMChannel|GuildChannel} channel The channel that was deleted
*/
client.emit(Events.CHANNEL_DELETE, channel);
}
return { channel };
}
scheduleForDeletion(id) {
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout);
}
}
module.exports = ChannelDeleteAction;

View File

@@ -1,74 +1,33 @@
'use strict';
const Action = require('./Action');
const TextChannel = require('../../structures/TextChannel');
const VoiceChannel = require('../../structures/VoiceChannel');
const CategoryChannel = require('../../structures/CategoryChannel');
const NewsChannel = require('../../structures/NewsChannel');
const StoreChannel = require('../../structures/StoreChannel');
const Constants = require('../../util/Constants');
const ChannelTypes = Constants.ChannelTypes;
const Util = require('../../util/Util');
const Channel = require('../../structures/Channel');
const { ChannelTypes } = require('../../util/Constants');
class ChannelUpdateAction extends Action {
handle(data) {
const client = this.client;
let channel = client.channels.get(data.id);
let channel = client.channels.cache.get(data.id);
if (channel) {
const oldChannel = Util.cloneObject(channel);
const old = channel._update(data);
// If the channel is changing types, we need to follow a different process
if (ChannelTypes[channel.type.toUpperCase()] !== data.type) {
// Determine which channel class we're changing to
let channelClass;
switch (data.type) {
case ChannelTypes.TEXT:
channelClass = TextChannel;
break;
case ChannelTypes.VOICE:
channelClass = VoiceChannel;
break;
case ChannelTypes.CATEGORY:
channelClass = CategoryChannel;
break;
case ChannelTypes.NEWS:
channelClass = NewsChannel;
break;
case ChannelTypes.STORE:
channelClass = StoreChannel;
break;
}
// Create the new channel instance and copy over cached data
const newChannel = new channelClass(channel.guild, data);
if (channel.messages && newChannel.messages) {
for (const [id, message] of channel.messages) newChannel.messages.set(id, message);
}
const newChannel = Channel.create(this.client, data, channel.guild);
for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message);
newChannel._typing = new Map(channel._typing);
channel = newChannel;
this.client.channels.set(channel.id, channel);
} else {
channel.setup(data);
this.client.channels.cache.set(channel.id, channel);
}
client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
return {
old: oldChannel,
old,
updated: channel,
};
}
return {
old: null,
updated: null,
};
return {};
}
}
/**
* 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;

View File

@@ -1,12 +1,20 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events } = 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);
const guild = client.guilds.cache.get(data.guild_id);
const user = client.users.add(data.user);
/**
* 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
*/
if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user);
}
}

View File

@@ -1,14 +1,16 @@
'use strict';
const Action = require('./Action');
class GuildChannelsPositionUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
for (const partialChannel of data.channels) {
const channel = guild.channels.get(partialChannel.id);
if (channel) channel.position = partialChannel.position;
const channel = guild.channels.cache.get(partialChannel.id);
if (channel) channel.rawPosition = partialChannel.position;
}
}

View File

@@ -1,5 +1,7 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events } = require('../../util/Constants');
class GuildDeleteAction extends Action {
constructor(client) {
@@ -10,16 +12,22 @@ class GuildDeleteAction extends Action {
handle(data) {
const client = this.client;
let guild = client.guilds.get(data.id);
let guild = client.guilds.cache.get(data.id);
if (guild) {
for (const channel of guild.channels.values()) {
for (const channel of guild.channels.cache.values()) {
if (channel.type === 'text') channel.stopTyping(true);
}
if (guild.available && data.unavailable) {
// Guild is unavailable
guild.available = false;
client.emit(Constants.Events.GUILD_UNAVAILABLE, guild);
/**
* Emitted whenever a guild becomes unavailable, likely due to a server outage.
* @event Client#guildUnavailable
* @param {Guild} guild The guild that has become unavailable
*/
client.emit(Events.GUILD_UNAVAILABLE, guild);
// Stops the GuildDelete packet thinking a guild was actually deleted,
// handles emitting of event itself
@@ -28,17 +36,25 @@ class GuildDeleteAction extends Action {
};
}
for (const channel of guild.channels.values()) this.client.channels.delete(channel.id);
if (guild.voiceConnection) guild.voiceConnection.disconnect();
for (const channel of guild.channels.cache.values()) this.client.channels.remove(channel.id);
if (guild.voice && guild.voice.connection) guild.voice.connection.disconnect();
// Delete guild
client.guilds.delete(guild.id);
client.guilds.cache.delete(guild.id);
guild.deleted = true;
/**
* Emitted whenever a guild kicks the client or the guild is deleted/left.
* @event Client#guildDelete
* @param {Guild} guild The guild that was deleted
*/
client.emit(Events.GUILD_DELETE, guild);
this.deleted.set(guild.id, guild);
this.scheduleForDeletion(guild.id);
} else {
guild = this.deleted.get(data.id) || null;
}
if (guild) guild.deleted = true;
return { guild };
}
@@ -48,10 +64,4 @@ class GuildDeleteAction extends Action {
}
}
/**
* 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;

View File

@@ -1,17 +1,19 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildEmojiCreateAction extends Action {
handle(guild, createdEmoji) {
const client = this.client;
const emoji = client.dataManager.newEmoji(createdEmoji, guild);
const emoji = guild.emojis.add(createdEmoji);
/**
* Emitted whenever a custom emoji is created in a guild.
* @event Client#emojiCreate
* @param {GuildEmoji} emoji The emoji that was created
*/
this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
return { emoji };
}
}
/**
* Emitted whenever a custom emoji is created in a guild.
* @event Client#emojiCreate
* @param {Emoji} emoji The emoji that was created
*/
module.exports = GuildEmojiCreateAction;

View File

@@ -1,18 +1,20 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildEmojiDeleteAction extends Action {
handle(emoji) {
const client = this.client;
client.dataManager.killEmoji(emoji);
emoji.guild.emojis.cache.delete(emoji.id);
emoji.deleted = true;
/**
* Emitted whenever a custom emoji is deleted in a guild.
* @event Client#emojiDelete
* @param {GuildEmoji} emoji The emoji that was deleted
*/
this.client.emit(Events.GUILD_EMOJI_DELETE, emoji);
return { emoji };
}
}
/**
* Emitted whenever a custom guild emoji is deleted.
* @event Client#emojiDelete
* @param {Emoji} emoji The emoji that was deleted
*/
module.exports = GuildEmojiDeleteAction;

View File

@@ -1,17 +1,20 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildEmojiUpdateAction extends Action {
handle(oldEmoji, newEmoji) {
const emoji = this.client.dataManager.updateEmoji(oldEmoji, newEmoji);
return { emoji };
handle(current, data) {
const old = current._update(data);
/**
* Emitted whenever a custom emoji is updated in a guild.
* @event Client#emojiUpdate
* @param {GuildEmoji} oldEmoji The old emoji
* @param {GuildEmoji} newEmoji The new emoji
*/
this.client.emit(Events.GUILD_EMOJI_UPDATE, old, current);
return { emoji: current };
}
}
/**
* Emitted whenever a custom guild emoji is updated.
* @event Client#emojiUpdate
* @param {Emoji} oldEmoji The old emoji
* @param {Emoji} newEmoji The new emoji
*/
module.exports = GuildEmojiUpdateAction;

View File

@@ -1,24 +1,20 @@
const Action = require('./Action');
'use strict';
function mappify(iterable) {
const map = new Map();
for (const x of iterable) map.set(...x);
return map;
}
const Action = require('./Action');
class GuildEmojisUpdateAction extends Action {
handle(data) {
const guild = this.client.guilds.get(data.guild_id);
const guild = this.client.guilds.cache.get(data.guild_id);
if (!guild || !guild.emojis) return;
const deletions = mappify(guild.emojis.entries());
const deletions = new Map(guild.emojis.cache);
for (const emoji of data.emojis) {
// Determine type of emoji event
const cachedEmoji = guild.emojis.get(emoji.id);
const cachedEmoji = guild.emojis.cache.get(emoji.id);
if (cachedEmoji) {
deletions.delete(emoji.id);
if (!cachedEmoji.equals(emoji, true)) {
if (!cachedEmoji.equals(emoji)) {
// Emoji updated
this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji);
}

View File

@@ -0,0 +1,19 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildIntegrationsUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
/**
* Emitted whenever a guild integration is updated
* @event Client#guildIntegrationsUpdate
* @param {Guild} guild The guild whose integrations were updated
*/
if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild);
}
}
module.exports = GuildIntegrationsUpdate;

View File

@@ -1,10 +0,0 @@
const Action = require('./Action');
class GuildMemberGetAction extends Action {
handle(guild, data) {
const member = guild._addMember(data, false);
return { member };
}
}
module.exports = GuildMemberGetAction;

View File

@@ -1,41 +1,30 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events, Status } = require('../../util/Constants');
class GuildMemberRemoveAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
handle(data, shard) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const guild = client.guilds.cache.get(data.guild_id);
let member = null;
if (guild) {
member = guild.members.get(data.user.id);
member = this.getMember(data, guild);
guild.memberCount--;
if (member) {
guild._removeMember(member);
this.deleted.set(guild.id + data.user.id, member);
if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, member);
this.scheduleForDeletion(guild.id, data.user.id);
} else {
member = this.deleted.get(guild.id + data.user.id) || null;
member.deleted = true;
guild.members.cache.delete(member.id);
/**
* Emitted whenever a member leaves a guild, or is kicked.
* @event Client#guildMemberRemove
* @param {GuildMember} member The member that has left/been kicked from the guild
*/
if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member);
}
if (member) member.deleted = true;
guild.voiceStates.cache.delete(data.user.id);
}
return { guild, member };
}
scheduleForDeletion(guildID, userID) {
this.client.setTimeout(() => this.deleted.delete(guildID + userID), this.client.options.restWsBridgeTimeout);
}
}
/**
* Emitted whenever a member leaves a guild, or is kicked.
* @event Client#guildMemberRemove
* @param {GuildMember} member The member that has left/been kicked from the guild
*/
module.exports = GuildMemberRemoveAction;

View File

@@ -1,26 +1,25 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Role = require('../../structures/Role');
const { Events } = require('../../util/Constants');
class GuildRoleCreate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const guild = client.guilds.cache.get(data.guild_id);
let role;
if (guild) {
const already = guild.roles.has(data.role.id);
role = new Role(guild, data.role);
guild.roles.set(role.id, role);
if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, role);
const already = guild.roles.cache.has(data.role.id);
role = guild.roles.add(data.role);
/**
* Emitted whenever a role is created.
* @event Client#roleCreate
* @param {Role} role The role that was created
*/
if (!already) client.emit(Events.GUILD_ROLE_CREATE, role);
}
return { role };
}
}
/**
* Emitted whenever a role is created.
* @event Client#roleCreate
* @param {Role} role The role that was created
*/
module.exports = GuildRoleCreate;

View File

@@ -1,42 +1,30 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events } = 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);
const guild = client.guilds.cache.get(data.guild_id);
let role;
if (guild) {
role = guild.roles.get(data.role_id);
role = guild.roles.cache.get(data.role_id);
if (role) {
guild.roles.delete(data.role_id);
this.deleted.set(guild.id + data.role_id, role);
this.scheduleForDeletion(guild.id, data.role_id);
client.emit(Constants.Events.GUILD_ROLE_DELETE, role);
} else {
role = this.deleted.get(guild.id + data.role_id) || null;
guild.roles.cache.delete(data.role_id);
role.deleted = true;
/**
* Emitted whenever a guild role is deleted.
* @event Client#roleDelete
* @param {Role} role The role that was deleted
*/
client.emit(Events.GUILD_ROLE_DELETE, role);
}
if (role) role.deleted = true;
}
return { role };
}
scheduleForDeletion(guildID, roleID) {
this.client.setTimeout(() => this.deleted.delete(guildID + roleID), this.client.options.restWsBridgeTimeout);
}
}
/**
* Emitted whenever a guild role is deleted.
* @event Client#roleDelete
* @param {Role} role The role that was deleted
*/
module.exports = GuildRoleDeleteAction;

View File

@@ -1,25 +1,30 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Util = require('../../util/Util');
const { Events } = require('../../util/Constants');
class GuildRoleUpdateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const roleData = data.role;
let oldRole = null;
let old = null;
const role = guild.roles.get(roleData.id);
const role = guild.roles.cache.get(data.role.id);
if (role) {
oldRole = Util.cloneObject(role);
role.setup(data.role);
client.emit(Constants.Events.GUILD_ROLE_UPDATE, oldRole, role);
old = role._update(data.role);
/**
* Emitted whenever a guild role is updated.
* @event Client#roleUpdate
* @param {Role} oldRole The role before the update
* @param {Role} newRole The role after the update
*/
client.emit(Events.GUILD_ROLE_UPDATE, old, role);
}
return {
old: oldRole,
old,
updated: role,
};
}
@@ -31,11 +36,4 @@ class GuildRoleUpdateAction extends Action {
}
}
/**
* Emitted whenever a guild role is updated.
* @event Client#roleUpdate
* @param {Role} oldRole The role before the update
* @param {Role} newRole The role after the update
*/
module.exports = GuildRoleUpdateAction;

View File

@@ -1,14 +1,16 @@
'use strict';
const Action = require('./Action');
class GuildRolesPositionUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
for (const partialRole of data.roles) {
const role = guild.roles.get(partialRole.id);
if (role) role.position = partialRole.position;
const role = guild.roles.cache.get(partialRole.id);
if (role) role.rawPosition = partialRole.position;
}
}

View File

@@ -1,29 +0,0 @@
const Action = require('./Action');
class GuildSync extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.id);
if (guild) {
if (data.presences) {
for (const presence of data.presences) guild._setPresence(presence.user.id, presence);
}
if (data.members) {
for (const syncMember of data.members) {
const member = guild.members.get(syncMember.user.id);
if (member) {
guild._updateMember(member, syncMember);
} else {
guild._addMember(syncMember, false);
}
}
}
if ('large' in data) guild.large = data.large;
}
}
}
module.exports = GuildSync;

View File

@@ -1,18 +1,24 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Util = require('../../util/Util');
const { Events } = require('../../util/Constants');
class GuildUpdateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.id);
const guild = client.guilds.cache.get(data.id);
if (guild) {
const oldGuild = Util.cloneObject(guild);
guild.setup(data);
client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild);
const old = guild._update(data);
/**
* 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
*/
client.emit(Events.GUILD_UPDATE, old, guild);
return {
old: oldGuild,
old,
updated: guild,
};
}
@@ -24,11 +30,4 @@ class GuildUpdateAction extends Action {
}
}
/**
* 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;

View File

@@ -7,22 +7,21 @@ const { Events } = require('../../util/Constants');
class InviteCreateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const channel = client.channels.get(data.channel_id);
if (guild && channel) {
const inviteData = Object.assign(data, { guild, channel });
const invite = new Invite(client, inviteData);
/**
* Emitted when an invite is created.
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
* or `MANAGE_CHANNEL` permissions for the channel.</info>
* @event Client#inviteCreate
* @param {Invite} invite The invite that was created
*/
client.emit(Events.INVITE_CREATE, invite);
return { invite };
}
return { invite: null };
const channel = client.channels.cache.get(data.channel_id);
const guild = client.guilds.cache.get(data.guild_id);
if (!channel && !guild) return false;
const inviteData = Object.assign(data, { channel, guild });
const invite = new Invite(client, inviteData);
/**
* Emitted when an invite is created.
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
* or `MANAGE_CHANNEL` permissions for the channel.</info>
* @event Client#inviteCreate
* @param {Invite} invite The invite that was created
*/
client.emit(Events.INVITE_CREATE, invite);
return { invite };
}
}

View File

@@ -1,3 +1,5 @@
'use strict';
const Action = require('./Action');
const Invite = require('../../structures/Invite');
const { Events } = require('../../util/Constants');
@@ -5,20 +7,22 @@ const { Events } = require('../../util/Constants');
class InviteDeleteAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const channel = client.channels.get(data.channel_id);
if (guild && channel) {
const inviteData = Object.assign(data, { guild, channel });
const invite = new Invite(client, inviteData);
/**
* Emitted when an invite is deleted.
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
* or `MANAGE_CHANNEL` permissions for the channel.</info>
* @event Client#inviteDelete
* @param {Invite} invite The invite that was deleted
*/
client.emit(Events.INVITE_DELETE, invite);
}
const channel = client.channels.cache.get(data.channel_id);
const guild = client.guilds.cache.get(data.guild_id);
if (!channel && !guild) return false;
const inviteData = Object.assign(data, { channel, guild });
const invite = new Invite(client, inviteData);
/**
* Emitted when an invite is deleted.
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
* or `MANAGE_CHANNEL` permissions for the channel.</info>
* @event Client#inviteDelete
* @param {Invite} invite The invite that was deleted
*/
client.emit(Events.INVITE_DELETE, invite);
return { invite };
}
}

View File

@@ -1,52 +1,38 @@
'use strict';
const Action = require('./Action');
const Message = require('../../structures/Message');
const { Events } = require('../../util/Constants');
class MessageCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get((data instanceof Array ? data[0] : data).channel_id);
const user = client.users.get((data instanceof Array ? data[0] : data).author.id);
const channel = client.channels.cache.get(data.channel_id);
if (channel) {
const member = channel.guild ? channel.guild.member(user) : null;
if (data instanceof Array) {
const messages = new Array(data.length);
for (let i = 0; i < data.length; i++) {
messages[i] = channel._cacheMessage(new Message(channel, data[i], client));
}
const lastMessage = messages[messages.length - 1];
channel.lastMessageID = lastMessage.id;
if (user) {
user.lastMessageID = lastMessage.id;
user.lastMessage = lastMessage;
}
if (member) {
member.lastMessageID = lastMessage.id;
member.lastMessage = lastMessage;
}
return {
messages,
};
} else {
const message = channel._cacheMessage(new Message(channel, data, client));
channel.lastMessageID = data.id;
if (user) {
user.lastMessageID = data.id;
user.lastMessage = message;
}
if (member) {
member.lastMessageID = data.id;
member.lastMessage = message;
}
return {
message,
};
const existing = channel.messages.cache.get(data.id);
if (existing) return { message: existing };
const message = channel.messages.add(data);
const user = message.author;
let member = message.member;
channel.lastMessageID = data.id;
if (user) {
user.lastMessageID = data.id;
user.lastMessageChannelID = channel.id;
}
if (member) {
member.lastMessageID = data.id;
member.lastMessageChannelID = channel.id;
}
/**
* Emitted whenever a message is created.
* @event Client#message
* @param {Message} message The created message
*/
client.emit(Events.MESSAGE_CREATE, message);
return { message };
}
return {
message: null,
};
return {};
}
}

View File

@@ -1,35 +1,29 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
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);
const channel = this.getChannel(data);
let message;
if (channel) {
message = channel.messages.get(data.id);
message = this.getMessage(data, channel);
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;
channel.messages.cache.delete(message.id);
message.deleted = true;
/**
* Emitted whenever a message is deleted.
* @event Client#messageDelete
* @param {Message} message The deleted message
*/
client.emit(Events.MESSAGE_DELETE, message);
}
if (message) message.deleted = true;
}
return { message };
}
scheduleForDeletion(channelID, messageID) {
this.client.setTimeout(() => this.deleted.delete(channelID + messageID),
this.client.options.restWsBridgeTimeout);
}
}
module.exports = MessageDeleteAction;

View File

@@ -1,25 +1,42 @@
'use strict';
const Action = require('./Action');
const Collection = require('../../util/Collection');
const Constants = require('../../util/Constants');
const { Events } = require('../../util/Constants');
class MessageDeleteBulkAction extends Action {
handle(data) {
const messages = new Collection();
const channel = this.client.channels.get(data.channel_id);
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
if (channel) {
for (const id of data.ids) {
const message = channel.messages.get(id);
const ids = data.ids;
const messages = new Collection();
for (const id of ids) {
const message = this.getMessage(
{
id,
guild_id: data.guild_id,
},
channel,
false,
);
if (message) {
message.deleted = true;
messages.set(message.id, message);
channel.messages.delete(id);
channel.messages.cache.delete(id);
}
}
}
if (messages.size > 0) this.client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages);
return { messages };
/**
* Emitted whenever messages are deleted in bulk.
* @event Client#messageDeleteBulk
* @param {Collection<Snowflake, Message>} messages The deleted messages, mapped by their ID
*/
if (messages.size > 0) client.emit(Events.MESSAGE_BULK_DELETE, messages);
return { messages };
}
return {};
}
}

View File

@@ -1,5 +1,8 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events } = require('../../util/Constants');
const { PartialTypes } = require('../../util/Constants');
/*
{ user_id: 'id',
@@ -10,28 +13,38 @@ const Constants = require('../../util/Constants');
class MessageReactionAdd extends Action {
handle(data) {
const user = this.client.users.get(data.user_id);
if (!user) return false;
// Verify channel
const channel = this.client.channels.get(data.channel_id);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = channel.messages.get(data.message_id);
if (!message) return false;
if (!data.emoji) return false;
const user = this.getUser(data);
if (!user) return false;
// Verify channel
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
// Verify reaction
const reaction = message._addReaction(data.emoji, user);
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_ADD, reaction, user);
if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false;
const reaction = message.reactions.add({
emoji: data.emoji,
count: message.partial ? null : 0,
me: user.id === this.client.user.id,
});
if (!reaction) return false;
reaction._add(user);
/**
* Emitted whenever a reaction is added to a cached message.
* @event Client#messageReactionAdd
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user that applied the guild or reaction emoji
*/
this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user);
return { message, reaction, user };
}
}
/**
* Emitted whenever a reaction is added to a cached message.
* @event Client#messageReactionAdd
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user that applied the emoji or reaction emoji
*/
module.exports = MessageReactionAdd;

View File

@@ -1,5 +1,7 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events } = require('../../util/Constants');
/*
{ user_id: 'id',
@@ -10,28 +12,33 @@ const Constants = require('../../util/Constants');
class MessageReactionRemove extends Action {
handle(data) {
const user = this.client.users.get(data.user_id);
if (!user) return false;
// Verify channel
const channel = this.client.channels.get(data.channel_id);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = channel.messages.get(data.message_id);
if (!message) return false;
if (!data.emoji) return false;
const user = this.getUser(data);
if (!user) return false;
// Verify channel
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
// Verify reaction
const reaction = message._removeReaction(data.emoji, user);
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE, reaction, user);
const reaction = this.getReaction(data, message, user);
if (!reaction) return false;
reaction._remove(user);
/**
* Emitted whenever a reaction is removed from a cached message.
* @event Client#messageReactionRemove
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user whose emoji or reaction emoji was removed
*/
this.client.emit(Events.MESSAGE_REACTION_REMOVE, reaction, user);
return { message, reaction, user };
}
}
/**
* Emitted whenever a reaction is removed from a cached message.
* @event Client#messageReactionRemove
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user whose emoji or reaction emoji was removed
*/
module.exports = MessageReactionRemove;

View File

@@ -1,16 +1,20 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events } = require('../../util/Constants');
class MessageReactionRemoveAll extends Action {
handle(data) {
const channel = this.client.channels.get(data.channel_id);
// Verify channel
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
const message = channel.messages.get(data.message_id);
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
message._clearReactions();
this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_ALL, message);
message.reactions.cache.clear();
this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message);
return { message };
}

View File

@@ -1,27 +1,28 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const { Events } = require('../../util/Constants');
class MessageReactionRemoveEmoji extends Action {
handle(data) {
// Verify channel
const channel = this.client.channels.get(data.channel_id);
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = channel.messages.get(data.message_id);
if (!message) return false;
if (!data.emoji) return false;
// Verify reaction
const reaction = message._removeReaction(data.emoji);
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction);
return { message, reaction };
const message = this.getMessage(data, channel);
if (!message) return false;
const reaction = this.getReaction(data, message);
if (!reaction) return false;
if (!message.partial) message.reactions.cache.delete(reaction.emoji.id || reaction.emoji.name);
/**
* Emitted when a bot removes an emoji reaction from a cached message.
* @event Client#messageReactionRemoveEmoji
* @param {MessageReaction} reaction The reaction that was removed
*/
this.client.emit(Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction);
return { reaction };
}
}
/**
* Emitted whenever a reaction emoji is removed from a cached message.
* @event Client#messageReactionRemoveEmoji
* @param {MessageReaction} messageReaction The reaction object
*/
module.exports = MessageReactionRemoveEmoji;

View File

@@ -1,40 +1,24 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
class MessageUpdateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get(data.channel_id);
const channel = this.getChannel(data);
if (channel) {
const message = channel.messages.get(data.id);
const { id, channel_id, guild_id, author, timestamp, type } = data;
const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel);
if (message) {
message.patch(data);
client.emit(Constants.Events.MESSAGE_UPDATE, message._edits[0], message);
return {
old: message._edits[0],
updated: message,
};
}
return {
old: message,
updated: message,
};
}
return {
old: null,
updated: null,
};
return {};
}
}
/**
* 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;

View File

@@ -0,0 +1,44 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class PresenceUpdateAction extends Action {
handle(data) {
let user = this.client.users.cache.get(data.user.id);
if (!user && data.user.username) user = this.client.users.add(data.user);
if (!user) return;
if (data.user && data.user.username) {
if (!user.equals(data.user)) this.client.actions.UserUpdate.handle(data.user);
}
const guild = this.client.guilds.cache.get(data.guild_id);
if (!guild) return;
let oldPresence = guild.presences.cache.get(user.id);
if (oldPresence) oldPresence = oldPresence._clone();
let member = guild.members.cache.get(user.id);
if (!member && data.status !== 'offline') {
member = guild.members.add({
user,
roles: data.roles,
deaf: false,
mute: false,
});
this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member);
}
guild.presences.add(Object.assign(data, { guild }));
if (member && this.client.listenerCount(Events.PRESENCE_UPDATE)) {
/**
* Emitted whenever a guild member's presence (e.g. status, activity) is changed.
* @event Client#presenceUpdate
* @param {?Presence} oldPresence The presence before the update, if one at all
* @param {Presence} newPresence The presence after the update
*/
this.client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence);
}
}
}
module.exports = PresenceUpdateAction;

View File

@@ -1,11 +0,0 @@
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;

View File

@@ -1,30 +0,0 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class UserNoteUpdateAction extends Action {
handle(data) {
const client = this.client;
const oldNote = client.user.notes.get(data.id);
const note = data.note.length ? data.note : null;
client.user.notes.set(data.id, note);
client.emit(Constants.Events.USER_NOTE_UPDATE, data.id, oldNote, note);
return {
old: oldNote,
updated: note,
};
}
}
/**
* Emitted whenever a note is updated.
* @event Client#userNoteUpdate
* @param {User} user The user the note belongs to
* @param {string} oldNote The note content before the update
* @param {string} newNote The note content after the update
*/
module.exports = UserNoteUpdateAction;

View File

@@ -1,25 +1,26 @@
'use strict';
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Util = require('../../util/Util');
const { Events } = require('../../util/Constants');
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 newUser = client.users.cache.get(data.id);
const oldUser = newUser._update(data);
const oldUser = Util.cloneObject(client.user);
client.user.patch(data);
client.emit(Constants.Events.USER_UPDATE, oldUser, client.user);
if (!oldUser.equals(newUser)) {
/**
* Emitted whenever a user's details (e.g. username) are changed.
* @event Client#userUpdate
* @param {User} oldUser The user before the update
* @param {User} newUser The user after the update
*/
client.emit(Events.USER_UPDATE, oldUser, newUser);
return {
old: oldUser,
updated: client.user,
updated: newUser,
};
}

View File

@@ -0,0 +1,44 @@
'use strict';
const Action = require('./Action');
const VoiceState = require('../../structures/VoiceState');
const { Events } = require('../../util/Constants');
class VoiceStateUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
// Update the state
const oldState = guild.voiceStates.cache.has(data.user_id)
? guild.voiceStates.cache.get(data.user_id)._clone()
: new VoiceState(guild, { user_id: data.user_id });
const newState = guild.voiceStates.add(data);
// Get the member
let member = guild.members.cache.get(data.user_id);
if (member && data.member) {
member._patch(data.member);
} else if (data.member && data.member.user && data.member.joined_at) {
member = guild.members.add(data.member);
}
// Emit event
if (member && member.user.id === client.user.id) {
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
client.voice.onVoiceStateUpdate(data);
}
/**
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
* @event Client#voiceStateUpdate
* @param {VoiceState} oldState The voice state before the update
* @param {VoiceState} newState The voice state after the update
*/
client.emit(Events.VOICE_STATE_UPDATE, oldState, newState);
}
}
}
module.exports = VoiceStateUpdate;

View File

@@ -0,0 +1,19 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class WebhooksUpdate extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
/**
* Emitted whenever a guild text channel has its webhooks changed.
* @event Client#webhookUpdate
* @param {TextChannel} channel The channel that had a webhook update
*/
if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel);
}
}
module.exports = WebhooksUpdate;

View File

@@ -1,52 +0,0 @@
const snekfetch = require('snekfetch');
const Constants = require('../../util/Constants');
class APIRequest {
constructor(rest, method, path, auth, data, files, reason) {
this.rest = rest;
this.client = rest.client;
this.method = method;
this.path = path.toString();
this.auth = auth;
this.data = data;
this.files = files;
this.route = this.getRoute(this.path);
this.reason = reason;
}
getRoute(url) {
let route = url.split('?')[0];
if (route.includes('/channels/') || route.includes('/guilds/')) {
const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/');
const majorID = route.substring(startInd).split('/')[2];
route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID);
}
return route;
}
getAuth() {
if (this.client.token && this.client.user && this.client.user.bot) {
return `Bot ${this.client.token}`;
} else if (this.client.token) {
return this.client.token;
}
throw new Error(Constants.Errors.NO_TOKEN);
}
gen() {
const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`;
const request = snekfetch[this.method](`${API}${this.path}`);
if (this.auth) request.set('Authorization', this.getAuth());
if (this.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.reason));
if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent);
if (this.files) {
for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name);
if (typeof this.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.data));
} else if (this.data) {
request.send(this.data);
}
return request;
}
}
module.exports = APIRequest;

View File

@@ -1,58 +0,0 @@
const UserAgentManager = require('./UserAgentManager');
const RESTMethods = require('./RESTMethods');
const SequentialRequestHandler = require('./RequestHandlers/Sequential');
const BurstRequestHandler = require('./RequestHandlers/Burst');
const APIRequest = require('./APIRequest');
const Constants = require('../../util/Constants');
class RESTManager {
constructor(client) {
this.client = client;
this.handlers = {};
this.userAgentManager = new UserAgentManager(this);
this.methods = new RESTMethods(this);
this.rateLimitedEndpoints = {};
this.globallyRateLimited = false;
}
destroy() {
for (const handlerKey of Object.keys(this.handlers)) {
const handler = this.handlers[handlerKey];
if (handler.destroy) handler.destroy();
}
}
push(handler, apiRequest) {
return new Promise((resolve, reject) => {
handler.push({
request: apiRequest,
resolve,
reject,
retries: 0,
});
});
}
getRequestHandler() {
switch (this.client.options.apiRequestMethod) {
case 'sequential':
return SequentialRequestHandler;
case 'burst':
return BurstRequestHandler;
default:
throw new Error(Constants.Errors.INVALID_RATE_LIMIT_METHOD);
}
}
makeRequest(method, url, auth, data, file, reason) {
const apiRequest = new APIRequest(this, method, url, auth, data, file, reason);
if (!this.handlers[apiRequest.route]) {
const RequestHandlerType = this.getRequestHandler();
this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route);
}
return this.push(this.handlers[apiRequest.route], apiRequest);
}
}
module.exports = RESTManager;

File diff suppressed because it is too large Load Diff

View File

@@ -1,90 +0,0 @@
const RequestHandler = require('./RequestHandler');
const DiscordAPIError = require('../DiscordAPIError');
const { Events: { RATE_LIMIT } } = require('../../../util/Constants');
class BurstRequestHandler extends RequestHandler {
constructor(restManager, endpoint) {
super(restManager, endpoint);
this.client = restManager.client;
this.limit = Infinity;
this.resetTime = null;
this.remaining = 1;
this.timeDifference = 0;
this.resetTimeout = null;
}
push(request) {
super.push(request);
this.handle();
}
execute(item) {
if (!item) return;
item.request.gen().end((err, res) => {
if (res && res.headers) {
this.limit = Number(res.headers['x-ratelimit-limit']);
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.remaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
}
if (err) {
if (err.status === 429) {
this.queue.unshift(item);
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
if (this.resetTimeout) return;
this.resetTimeout = this.client.setTimeout(() => {
this.remaining = this.limit;
this.globalLimit = false;
this.handle();
this.resetTimeout = null;
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
} else if (err.status >= 500 && err.status < 600) {
if (item.retries === this.client.options.retryLimit) {
item.reject(err);
this.handle();
} else {
item.retries++;
this.queue.unshift(item);
this.resetTimeout = this.client.setTimeout(() => {
this.handle();
this.resetTimeout = null;
}, 1e3 + this.client.options.restTimeOffset);
}
} else {
item.reject(err.status >= 400 && err.status < 500 ?
new DiscordAPIError(res.request.path, res.body, res.request.method) : err);
this.handle();
}
} else {
if (this.remaining === 0) {
if (this.client.listenerCount(RATE_LIMIT)) {
this.client.emit(RATE_LIMIT, {
limit: this.limit,
timeDifference: this.timeDifference,
path: item.request.path,
method: item.request.method,
});
}
}
this.globalLimit = false;
const data = res && res.body ? res.body : {};
item.resolve(data);
this.handle();
}
});
}
handle() {
super.handle();
if (this.queue.length === 0) return;
if ((this.remaining <= 0 || this.globalLimit) && Date.now() - this.timeDifference < this.resetTime) return;
this.execute(this.queue.shift());
this.remaining--;
this.handle();
}
}
module.exports = BurstRequestHandler;

View File

@@ -1,54 +0,0 @@
/**
* A base class for different types of rate limiting handlers for the REST API.
* @private
*/
class RequestHandler {
/**
* @param {RESTManager} restManager The REST manager to use
*/
constructor(restManager) {
/**
* The RESTManager that instantiated this RequestHandler
* @type {RESTManager}
*/
this.restManager = restManager;
/**
* A list of requests that have yet to be processed
* @type {APIRequest[]}
*/
this.queue = [];
}
/**
* Whether or not the client is being rate limited on every endpoint
* @type {boolean}
* @readonly
*/
get globalLimit() {
return this.restManager.globallyRateLimited;
}
set globalLimit(value) {
this.restManager.globallyRateLimited = value;
}
/**
* Push a new API request into this bucket.
* @param {APIRequest} request The new request to push into the queue
*/
push(request) {
this.queue.push(request);
}
/**
* Attempts to get this RequestHandler to process its current queue.
*/
handle() {} // eslint-disable-line no-empty-function
destroy() {
this.queue = [];
}
}
module.exports = RequestHandler;

View File

@@ -1,132 +0,0 @@
const RequestHandler = require('./RequestHandler');
const DiscordAPIError = require('../DiscordAPIError');
const { Events: { RATE_LIMIT } } = require('../../../util/Constants');
/**
* Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto
* the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account,
* but it can be slower.
* @extends {RequestHandler}
* @private
*/
class SequentialRequestHandler extends RequestHandler {
/**
* @param {RESTManager} restManager The REST manager to use
* @param {string} endpoint The endpoint to handle
*/
constructor(restManager, endpoint) {
super(restManager, endpoint);
/**
* The client that instantiated this handler
* @type {Client}
*/
this.client = restManager.client;
/**
* The endpoint that this handler is handling
* @type {string}
*/
this.endpoint = endpoint;
/**
* The time difference between Discord's Dates and the local computer's Dates. A positive number means the local
* computer's time is ahead of Discord's
* @type {number}
*/
this.timeDifference = 0;
/**
* Whether the queue is being processed or not
* @type {boolean}
*/
this.busy = false;
}
push(request) {
super.push(request);
this.handle();
}
/**
* Performs a request then resolves a promise to indicate its readiness for a new request.
* @param {APIRequest} item The item to execute
* @returns {Promise<?Object|Error>}
*/
execute(item) {
this.busy = true;
return new Promise(resolve => {
item.request.gen().end((err, res) => {
if (res && res.headers) {
this.requestLimit = Number(res.headers['x-ratelimit-limit']);
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
}
if (err) {
if (err.status === 429) {
this.queue.unshift(item);
this.client.setTimeout(() => {
this.globalLimit = false;
resolve();
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
} else if (err.status >= 500 && err.status < 600) {
if (item.retries === this.client.options.retryLimit) {
item.reject(err);
resolve();
} else {
item.retries++;
this.queue.unshift(item);
this.client.setTimeout(resolve, 1e3 + this.client.options.restTimeOffset);
}
} else {
item.reject(err.status >= 400 && err.status < 500 ?
new DiscordAPIError(res.request.path, res.body, res.request.method) : err);
resolve(err);
}
} else {
this.globalLimit = false;
const data = res && res.body ? res.body : {};
item.resolve(data);
if (this.requestRemaining === 0) {
if (this.client.listenerCount(RATE_LIMIT)) {
/**
* Emitted when the client hits a rate limit while making a request
* @event Client#rateLimit
* @param {Object} rateLimitInfo Object containing the rate limit info
* @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint
* @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers
* @param {string} rateLimitInfo.path Path used for request that triggered this event
* @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
*/
this.client.emit(RATE_LIMIT, {
limit: this.requestLimit,
timeDifference: this.timeDifference,
path: item.request.path,
method: item.request.method,
});
}
this.client.setTimeout(
() => resolve(data),
this.requestResetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset
);
} else {
resolve(data);
}
}
});
});
}
handle() {
super.handle();
if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return;
this.execute(this.queue.shift()).then(() => {
this.busy = false;
this.handle();
});
}
}
module.exports = SequentialRequestHandler;

View File

@@ -1,25 +0,0 @@
const Constants = require('../../util/Constants');
class UserAgentManager {
constructor() {
this.build(this.constructor.DEFAULT);
}
set({ url, version } = {}) {
this.build({
url: url || this.constructor.DFEAULT.url,
version: version || this.constructor.DEFAULT.version,
});
}
build(ua) {
this.userAgent = `DiscordBot (${ua.url}, ${ua.version}) Node.js/${process.version}`;
}
}
UserAgentManager.DEFAULT = {
url: Constants.Package.homepage.split('#')[0],
version: Constants.Package.version,
};
module.exports = UserAgentManager;

View File

@@ -1,17 +1,22 @@
const Collection = require('../../util/Collection');
'use strict';
const VoiceBroadcast = require('./VoiceBroadcast');
const VoiceConnection = require('./VoiceConnection');
const { Error } = require('../../errors');
const Collection = require('../../util/Collection');
/**
* Manages all the voice stuff for the client.
* @private
* Manages voice connections for the client
*/
class ClientVoiceManager {
constructor(client) {
/**
* The client that instantiated this voice manager
* @type {Client}
* @readonly
* @name ClientVoiceManager#client
*/
this.client = client;
Object.defineProperty(this, 'client', { value: client });
/**
* A collection mapping connection IDs to the Connection objects
@@ -19,25 +24,39 @@ class ClientVoiceManager {
*/
this.connections = new Collection();
this.client.on('self.voiceServer', this.onVoiceServer.bind(this));
this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this));
/**
* Active voice broadcasts that have been created
* @type {VoiceBroadcast[]}
*/
this.broadcasts = [];
}
/**
* Creates a voice broadcast.
* @returns {VoiceBroadcast}
*/
createBroadcast() {
const broadcast = new VoiceBroadcast(this.client);
this.broadcasts.push(broadcast);
return broadcast;
}
onVoiceServer({ guild_id, token, endpoint }) {
this.client.emit('debug', `[VOICE] voiceServer guild: ${guild_id} token: ${token} endpoint: ${endpoint}`);
const connection = this.connections.get(guild_id);
if (connection) connection.setTokenAndEndpoint(token, endpoint);
}
onVoiceStateUpdate({ guild_id, session_id, channel_id }) {
const connection = this.connections.get(guild_id);
this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`);
if (!connection) return;
if (!channel_id) {
connection._disconnect();
this.connections.delete(guild_id);
return;
}
connection.channel = this.client.channels.get(channel_id);
connection.channel = this.client.channels.cache.get(channel_id);
connection.setSessionID(session_id);
}
@@ -45,15 +64,12 @@ class ClientVoiceManager {
* Sets up a request to join a voice channel.
* @param {VoiceChannel} channel The voice channel to join
* @returns {Promise<VoiceConnection>}
* @private
*/
joinChannel(channel) {
return new Promise((resolve, reject) => {
if (!channel.joinable) {
if (channel.full) {
throw new Error('You do not have permission to join this voice channel; it is full.');
} else {
throw new Error('You do not have permission to join this voice channel.');
}
throw new Error('VOICE_JOIN_CHANNEL', channel.full);
}
let connection = this.connections.get(channel.guild.id);
@@ -66,6 +82,10 @@ class ClientVoiceManager {
return;
} else {
connection = new VoiceConnection(this, channel);
connection.on('debug', msg =>
this.client.emit('debug', `[VOICE (${channel.guild.id}:${connection.status})]: ${msg}`),
);
connection.authenticate();
this.connections.set(channel.guild.id, connection);
}
@@ -74,9 +94,13 @@ class ClientVoiceManager {
reject(reason);
});
connection.on('error', reject);
connection.once('authenticated', () => {
connection.once('ready', () => resolve(connection));
connection.once('error', reject);
connection.once('ready', () => {
resolve(connection);
connection.removeListener('error', reject);
});
connection.once('disconnect', () => this.connections.delete(channel.guild.id));
});
});

View File

@@ -1,31 +1,25 @@
const VolumeInterface = require('./util/VolumeInterface');
const Prism = require('prism-media');
const OpusEncoders = require('./opus/OpusEngineList');
const Collection = require('../../util/Collection');
'use strict';
const ffmpegArguments = [
'-analyzeduration', '0',
'-loglevel', '0',
'-f', 's16le',
'-ar', '48000',
'-ac', '2',
];
const EventEmitter = require('events');
const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer');
const PlayInterface = require('./util/PlayInterface');
const { Events } = require('../../util/Constants');
/**
* A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency.
*
* Example usage:
* ```js
* const broadcast = client.createVoiceBroadcast();
* broadcast.playFile('./music.mp3');
* const broadcast = client.voice.createBroadcast();
* broadcast.play('./music.mp3');
* // Play "music.mp3" in all voice connections that the client is in
* for (const connection of client.voiceConnections.values()) {
* connection.playBroadcast(broadcast);
* for (const connection of client.voice.connections.values()) {
* connection.play(broadcast);
* }
* ```
* @implements {VolumeInterface}
* @implements {PlayInterface}
*/
class VoiceBroadcast extends VolumeInterface {
class VoiceBroadcast extends EventEmitter {
constructor(client) {
super();
/**
@@ -33,334 +27,84 @@ class VoiceBroadcast extends VolumeInterface {
* @type {Client}
*/
this.client = client;
this._dispatchers = new Collection();
this._encoders = new Collection();
/**
* The audio transcoder that this broadcast uses
* @type {Prism}
* The subscribed StreamDispatchers of this broadcast
* @type {StreamDispatcher[]}
*/
this.prism = new Prism();
/**
* The current audio transcoder that is being used
* @type {Object}
*/
this.currentTranscoder = null;
this.tickInterval = null;
this._volume = 1;
this.subscribers = [];
this.player = new BroadcastAudioPlayer(this);
}
/**
* An array of subscribed dispatchers
* @type {StreamDispatcher[]}
* The current master dispatcher, if any. This dispatcher controls all that is played by subscribed dispatchers.
* @type {?BroadcastDispatcher}
* @readonly
*/
get dispatchers() {
let d = [];
for (const container of this._dispatchers.values()) {
d = d.concat(Array.from(container.values()));
}
return d;
get dispatcher() {
return this.player.dispatcher;
}
get _playableStream() {
const currentTranscoder = this.currentTranscoder;
if (!currentTranscoder) return null;
const transcoder = currentTranscoder.transcoder;
const options = currentTranscoder.options;
return (transcoder && transcoder.output) || options.stream;
/**
* Play an audio resource.
* @param {ReadableStream|string} resource The resource to play.
* @param {StreamOptions} [options] The options to play.
* @example
* // Play a local audio file
* broadcast.play('/home/hydrabolt/audio.mp3', { volume: 0.5 });
* @example
* // Play a ReadableStream
* broadcast.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' }));
* @example
* // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html
* broadcast.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
* @returns {BroadcastDispatcher}
*/
play() {
return null;
}
unregisterDispatcher(dispatcher, old) {
const volume = old || dispatcher.volume;
/**
* Emitted whenever a stream dispatcher unsubscribes from the broadcast.
* @event VoiceBroadcast#unsubscribe
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
*/
this.emit('unsubscribe', dispatcher);
for (const container of this._dispatchers.values()) {
container.delete(dispatcher);
if (!container.size) {
this._encoders.get(volume).destroy();
this._dispatchers.delete(volume);
this._encoders.delete(volume);
}
}
/**
* Ends the broadcast, unsubscribing all subscribed channels and deleting the broadcast
*/
end() {
for (const dispatcher of this.subscribers) this.delete(dispatcher);
const index = this.client.voice.broadcasts.indexOf(this);
if (index !== -1) this.client.voice.broadcasts.splice(index, 1);
}
registerDispatcher(dispatcher) {
if (!this._dispatchers.has(dispatcher.volume)) {
this._dispatchers.set(dispatcher.volume, new Set());
this._encoders.set(dispatcher.volume, OpusEncoders.fetch());
}
const container = this._dispatchers.get(dispatcher.volume);
if (!container.has(dispatcher)) {
container.add(dispatcher);
dispatcher.once('end', () => this.unregisterDispatcher(dispatcher));
dispatcher.on('volumeChange', (o, n) => {
this.unregisterDispatcher(dispatcher, o);
if (!this._dispatchers.has(n)) {
this._dispatchers.set(n, new Set());
this._encoders.set(n, OpusEncoders.fetch());
}
this._dispatchers.get(n).add(dispatcher);
});
add(dispatcher) {
const index = this.subscribers.indexOf(dispatcher);
if (index === -1) {
this.subscribers.push(dispatcher);
/**
* Emitted whenever a stream dispatcher subscribes to the broadcast.
* @event VoiceBroadcast#subscribe
* @param {StreamDispatcher} dispatcher The subscribed dispatcher
* @param {StreamDispatcher} subscriber The subscribed dispatcher
*/
this.emit('subscribe', dispatcher);
this.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher);
return true;
} else {
return false;
}
}
killCurrentTranscoder() {
if (this.currentTranscoder) {
if (this.currentTranscoder.transcoder) this.currentTranscoder.transcoder.kill();
this.currentTranscoder = null;
this.emit('end');
}
}
/**
* Plays any audio stream across the broadcast.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
* @example
* // Play streams using ytdl-core
* const ytdl = require('ytdl-core');
* const streamOptions = { seek: 0, volume: 1 };
* const broadcast = client.createVoiceBroadcast();
*
* voiceChannel.join()
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* broadcast.playStream(stream);
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
*/
playStream(stream, options = {}) {
this.setVolume(options.volume || 1);
return this._playTranscodable(stream, options);
}
/**
* Play the given file in the voice connection.
* @param {string} file The absolute path to the file
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play files natively
* const broadcast = client.createVoiceBroadcast();
*
* voiceChannel.join()
* .then(connection => {
* broadcast.playFile('C:/Users/Discord/Desktop/music.mp3');
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
*/
playFile(file, options = {}) {
this.setVolume(options.volume || 1);
return this._playTranscodable(`file:${file}`, options);
}
_playTranscodable(media, options) {
this.killCurrentTranscoder();
const transcoder = this.prism.transcode({
type: 'ffmpeg',
media,
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
});
/**
* Emitted whenever an error occurs.
* @event VoiceBroadcast#error
* @param {Error} error The error that occurred
*/
transcoder.once('error', e => {
if (this.listenerCount('error') > 0) this.emit('error', e);
delete(dispatcher) {
const index = this.subscribers.indexOf(dispatcher);
if (index !== -1) {
this.subscribers.splice(index, 1);
dispatcher.destroy();
/**
* Emitted whenever the VoiceBroadcast has any warnings.
* @event VoiceBroadcast#warn
* @param {string|Error} warning The warning that was raised
* Emitted whenever a stream dispatcher unsubscribes to the broadcast.
* @event VoiceBroadcast#unsubscribe
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
*/
else this.emit('warn', e);
});
/**
* Emitted once the broadcast (the audio stream) ends.
* @event VoiceBroadcast#end
*/
transcoder.once('end', () => this.killCurrentTranscoder());
this.currentTranscoder = {
transcoder,
options,
};
transcoder.output.once('readable', () => this._startPlaying());
return this;
}
/**
* Plays a stream of 16-bit signed stereo PCM.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
*/
playConvertedStream(stream, options = {}) {
this.killCurrentTranscoder();
this.setVolume(options.volume || 1);
this.currentTranscoder = { options: { stream } };
stream.once('readable', () => this._startPlaying());
return this;
}
/**
* Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playOpusStream(stream) {
this.currentTranscoder = { options: { stream }, opus: true };
stream.once('readable', () => this._startPlaying());
return this;
}
/**
* Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
* @param {string} input The arbitrary input
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
*/
playArbitraryInput(input, options = {}) {
this.setVolume(options.volume || 1);
options.input = input;
return this._playTranscodable(input, options);
}
/**
* Pauses the entire broadcast - all dispatchers also pause.
*/
pause() {
this.paused = true;
for (const container of this._dispatchers.values()) {
for (const dispatcher of container.values()) {
dispatcher.pause();
}
}
}
/**
* Resumes the entire broadcast - all dispatchers also resume.
*/
resume() {
this.paused = false;
for (const container of this._dispatchers.values()) {
for (const dispatcher of container.values()) {
dispatcher.resume();
}
}
}
_startPlaying() {
if (this.tickInterval) clearInterval(this.tickInterval);
// Old code?
// this.tickInterval = this.client.setInterval(this.tick.bind(this), 20);
this._startTime = Date.now();
this._count = 0;
this._pausedTime = 0;
this._missed = 0;
this.tick();
}
tick() {
if (!this._playableStream) return;
if (this.paused) {
this._pausedTime += 20;
setTimeout(() => this.tick(), 20);
return;
}
const opus = this.currentTranscoder.opus;
const buffer = this.readStreamBuffer();
if (!buffer) {
this._missed++;
if (this._missed < 5) {
this._pausedTime += 200;
setTimeout(() => this.tick(), 200);
} else {
this.killCurrentTranscoder();
}
return;
}
this._missed = 0;
let packetMatrix = {};
const getOpusPacket = volume => {
if (packetMatrix[volume]) return packetMatrix[volume];
const opusEncoder = this._encoders.get(volume);
const opusPacket = opusEncoder.encode(this.applyVolume(buffer, this._volume * volume));
packetMatrix[volume] = opusPacket;
return opusPacket;
};
for (const dispatcher of this.dispatchers) {
if (opus) {
dispatcher.processPacket(buffer);
continue;
}
const volume = dispatcher.volume;
dispatcher.processPacket(getOpusPacket(volume));
}
const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now());
this._count++;
setTimeout(() => this.tick(), next);
}
readStreamBuffer() {
const opus = this.currentTranscoder.opus;
const bufferLength = (opus ? 80 : 1920) * 2;
let buffer = this._playableStream.read(bufferLength);
if (opus) return buffer;
if (!buffer) return null;
if (buffer.length !== bufferLength) {
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer);
buffer = newBuffer;
}
return buffer;
}
/**
* Stop the current stream from playing without unsubscribing dispatchers.
*/
end() {
this.killCurrentTranscoder();
}
/**
* End the current broadcast, all subscribed dispatchers will also end.
*/
destroy() {
this.end();
for (const container of this._dispatchers.values()) {
for (const dispatcher of container.values()) {
dispatcher.destroy('end', 'broadcast ended');
}
this.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher);
return true;
}
return false;
}
}
PlayInterface.applyToClass(VoiceBroadcast);
module.exports = VoiceBroadcast;

View File

@@ -1,16 +1,26 @@
const VoiceWebSocket = require('./VoiceWebSocket');
const VoiceUDP = require('./VoiceUDPClient');
const Util = require('../../util/Util');
const Constants = require('../../util/Constants');
const AudioPlayer = require('./player/AudioPlayer');
const VoiceReceiver = require('./receiver/VoiceReceiver');
const SingleSilence = require('./util/SingleSilence');
const EventEmitter = require('events').EventEmitter;
const Prism = require('prism-media');
'use strict';
// The delay between packets when a user is considered to have stopped speaking
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
const DISCORD_SPEAKING_DELAY = 250;
const EventEmitter = require('events');
const VoiceUDP = require('./networking/VoiceUDPClient');
const VoiceWebSocket = require('./networking/VoiceWebSocket');
const AudioPlayer = require('./player/AudioPlayer');
const VoiceReceiver = require('./receiver/Receiver');
const PlayInterface = require('./util/PlayInterface');
const Silence = require('./util/Silence');
const { Error } = require('../../errors');
const { OPCodes, VoiceOPCodes, VoiceStatus, Events } = require('../../util/Constants');
const Speaking = require('../../util/Speaking');
const Util = require('../../util/Util');
// Workaround for Discord now requiring silence to be sent before being able to receive audio
class SingleSilence extends Silence {
_read() {
super._read();
this.push(null);
}
}
const SUPPORTED_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'];
/**
* Represents a connection to a guild's voice server.
@@ -22,6 +32,7 @@ const DISCORD_SPEAKING_DELAY = 250;
* });
* ```
* @extends {EventEmitter}
* @implements {PlayInterface}
*/
class VoiceConnection extends EventEmitter {
constructor(voiceManager, channel) {
@@ -33,23 +44,6 @@ class VoiceConnection extends EventEmitter {
*/
this.voiceManager = voiceManager;
/**
* The client that instantiated this connection
* @type {Client}
*/
this.client = voiceManager.client;
/**
* @external Prism
* @see {@link https://github.com/hydrabolt/prism-media}
*/
/**
* The audio transcoder for this connection
* @type {Prism}
*/
this.prism = new Prism();
/**
* The voice channel this connection is currently serving
* @type {VoiceChannel}
@@ -60,19 +54,13 @@ class VoiceConnection extends EventEmitter {
* The current status of the voice connection
* @type {VoiceStatus}
*/
this.status = Constants.VoiceStatus.AUTHENTICATING;
this.status = VoiceStatus.AUTHENTICATING;
/**
* Whether we're currently transmitting audio
* @type {boolean}
* Our current speaking state
* @type {Readonly<Speaking>}
*/
this.speaking = false;
/**
* An array of Voice Receivers that have been created for this connection
* @type {VoiceReceiver[]}
*/
this.receivers = [];
this.speaking = new Speaking().freeze();
/**
* The authentication data needed to connect to the voice server
@@ -105,19 +93,21 @@ class VoiceConnection extends EventEmitter {
this.emit('warn', e);
});
this.once('closing', () => this.player.destroy());
/**
* Map SSRC to user id
* Map SSRC values to user IDs
* @type {Map<number, Snowflake>}
* @private
*/
this.ssrcMap = new Map();
/**
* Map user id to speaking timeout
* @type {Map<Snowflake, Timeout>}
* Tracks which users are talking
* @type {Map<Snowflake, Readonly<Speaking>>}
* @private
*/
this.speakingTimeouts = new Map();
this._speaking = new Map();
/**
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
@@ -126,7 +116,20 @@ class VoiceConnection extends EventEmitter {
*/
this.sockets = {};
this.authenticate();
/**
* The voice receiver of this connection
* @type {VoiceReceiver}
*/
this.receiver = new VoiceReceiver(this);
}
/**
* The client that instantiated this connection
* @type {Client}
* @readonly
*/
get client() {
return this.voiceManager.client;
}
/**
@@ -139,68 +142,92 @@ class VoiceConnection extends EventEmitter {
}
/**
* Sets whether the voice connection should display as "speaking" or not.
* @param {boolean} value Whether or not to speak
* Sets whether the voice connection should display as "speaking", "soundshare" or "none".
* @param {BitFieldResolvable} value The new speaking state
* @private
*/
setSpeaking(value) {
if (this.speaking === value) return;
if (this.status !== Constants.VoiceStatus.CONNECTED) return;
this.speaking = value;
this.sockets.ws.sendPacket({
op: Constants.VoiceOPCodes.SPEAKING,
d: {
speaking: true,
delay: 0,
},
}).catch(e => {
this.emit('debug', e);
});
if (this.speaking.equals(value)) return;
if (this.status !== VoiceStatus.CONNECTED) return;
this.speaking = new Speaking(value).freeze();
this.sockets.ws
.sendPacket({
op: VoiceOPCodes.SPEAKING,
d: {
speaking: this.speaking.bitfield,
delay: 0,
ssrc: this.authentication.ssrc,
},
})
.catch(e => {
this.emit('debug', e);
});
}
/**
* The voice state of this connection
* @type {VoiceState}
*/
get voice() {
return this.channel.guild.voice;
}
/**
* Sends a request to the main gateway to join a voice channel.
* @param {Object} [options] The options to provide
* @returns {Promise<Shard>}
* @private
*/
sendVoiceStateUpdate(options = {}) {
options = Util.mergeDefault({
guild_id: this.channel.guild.id,
channel_id: this.channel.id,
self_mute: false,
self_deaf: false,
}, options);
options = Util.mergeDefault(
{
guild_id: this.channel.guild.id,
channel_id: this.channel.id,
self_mute: this.voice ? this.voice.selfMute : false,
self_deaf: this.voice ? this.voice.selfDeaf : false,
},
options,
);
this.client.ws.send({
op: Constants.OPCodes.VOICE_STATE_UPDATE,
d: options,
});
this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`);
return this.channel.guild.shard.send(
{
op: OPCodes.VOICE_STATE_UPDATE,
d: options,
},
true,
);
}
/**
* Set the token and endpoint required to connect to the voice servers.
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @private
* @returns {void}
*/
setTokenAndEndpoint(token, endpoint) {
this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`);
if (!endpoint) {
// Signifies awaiting endpoint stage
return;
}
if (!token) {
this.authenticateFailed('Token not provided from voice server packet.');
this.authenticateFailed('VOICE_TOKEN_ABSENT');
return;
}
endpoint = endpoint.match(/([^:]*)/)[0];
this.emit('debug', `Endpoint resolved as ${endpoint}`);
if (!endpoint) {
this.authenticateFailed('Invalid endpoint received.');
this.authenticateFailed('VOICE_INVALID_ENDPOINT');
return;
}
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
if (this.status === VoiceStatus.AUTHENTICATING) {
this.authentication.token = token;
this.authentication.endpoint = endpoint;
this.checkAuthenticated();
@@ -212,14 +239,16 @@ class VoiceConnection extends EventEmitter {
/**
* Sets the Session ID for the connection.
* @param {string} sessionID The voice session ID
* @private
*/
setSessionID(sessionID) {
this.emit('debug', `Setting sessionID ${sessionID} (stored as "${this.authentication.sessionID}")`);
if (!sessionID) {
this.authenticateFailed('Session ID not supplied.');
this.authenticateFailed('VOICE_SESSION_ABSENT');
return;
}
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
if (this.status === VoiceStatus.AUTHENTICATING) {
this.authentication.sessionID = sessionID;
this.checkAuthenticated();
} else if (sessionID !== this.authentication.sessionID) {
@@ -239,10 +268,9 @@ class VoiceConnection extends EventEmitter {
*/
checkAuthenticated() {
const { token, endpoint, sessionID } = this.authentication;
this.emit('debug', `Authenticated with sessionID ${sessionID}`);
if (token && endpoint && sessionID) {
this.client.clearTimeout(this.connectTimeout);
this.status = Constants.VoiceStatus.CONNECTING;
this.status = VoiceStatus.CONNECTING;
/**
* Emitted when we successfully initiate a voice connection.
* @event VoiceConnection#authenticated
@@ -259,7 +287,8 @@ class VoiceConnection extends EventEmitter {
*/
authenticateFailed(reason) {
this.client.clearTimeout(this.connectTimeout);
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
this.emit('debug', `Authenticate failed - ${reason}`);
if (this.status === VoiceStatus.AUTHENTICATING) {
/**
* Emitted when we fail to initiate a voice connection.
* @event VoiceConnection#failed
@@ -274,7 +303,7 @@ class VoiceConnection extends EventEmitter {
*/
this.emit('error', new Error(reason));
}
this.status = Constants.VoiceStatus.DISCONNECTED;
this.status = VoiceStatus.DISCONNECTED;
}
/**
@@ -293,8 +322,7 @@ class VoiceConnection extends EventEmitter {
*/
authenticate() {
this.sendVoiceStateUpdate();
this.connectTimeout = this.client.setTimeout(
() => this.authenticateFailed(new Error('Connection not established within 15 seconds.')), 15000);
this.connectTimeout = this.client.setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15000);
}
/**
@@ -306,8 +334,9 @@ class VoiceConnection extends EventEmitter {
reconnect(token, endpoint) {
this.authentication.token = token;
this.authentication.endpoint = endpoint;
this.status = Constants.VoiceStatus.RECONNECTING;
this.speaking = new Speaking().freeze();
this.status = VoiceStatus.RECONNECTING;
this.emit('debug', `Reconnecting to ${endpoint}`);
/**
* Emitted when the voice connection is reconnecting (typically after a region change).
* @event VoiceConnection#reconnecting
@@ -317,14 +346,17 @@ class VoiceConnection extends EventEmitter {
}
/**
* Disconnect the voice connection, causing a disconnect and closing event to be emitted.
* Disconnects the voice connection, causing a disconnect and closing event to be emitted.
*/
disconnect() {
this.emit('closing');
this.emit('debug', 'disconnect() triggered');
this.client.clearTimeout(this.connectTimeout);
const conn = this.voiceManager.connections.get(this.channel.guild.id);
if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id);
this.sendVoiceStateUpdate({
channel_id: null,
});
this._disconnect();
}
@@ -333,9 +365,8 @@ class VoiceConnection extends EventEmitter {
* @private
*/
_disconnect() {
this.player.destroy();
this.cleanup();
this.status = Constants.VoiceStatus.DISCONNECTED;
this.status = VoiceStatus.DISCONNECTED;
/**
* Emitted when the voice connection disconnects.
* @event VoiceConnection#disconnect
@@ -348,13 +379,17 @@ class VoiceConnection extends EventEmitter {
* @private
*/
cleanup() {
this.player.destroy();
this.speaking = new Speaking().freeze();
const { ws, udp } = this.sockets;
this.emit('debug', 'Connection clean up');
if (ws) {
ws.removeAllListeners('error');
ws.removeAllListeners('ready');
ws.removeAllListeners('sessionDescription');
ws.removeAllListeners('startSpeaking');
ws.removeAllListeners('speaking');
ws.shutdown();
}
@@ -369,9 +404,10 @@ class VoiceConnection extends EventEmitter {
* @private
*/
connect() {
if (this.status !== Constants.VoiceStatus.RECONNECTING) {
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
this.emit('debug', `Connect triggered`);
if (this.status !== VoiceStatus.RECONNECTING) {
if (this.sockets.ws) throw new Error('WS_CONNECTION_EXISTS');
if (this.sockets.udp) throw new Error('UDP_CONNECTION_EXISTS');
}
if (this.sockets.ws) this.sockets.ws.shutdown();
@@ -382,11 +418,15 @@ class VoiceConnection extends EventEmitter {
const { ws, udp } = this.sockets;
ws.on('debug', msg => this.emit('debug', msg));
udp.on('debug', msg => this.emit('debug', msg));
ws.on('error', err => this.emit('error', err));
udp.on('error', err => this.emit('error', err));
ws.on('ready', this.onReady.bind(this));
ws.on('sessionDescription', this.onSessionDescription.bind(this));
ws.on('startSpeaking', this.onStartSpeaking.bind(this));
this.sockets.ws.connect();
}
/**
@@ -394,25 +434,29 @@ class VoiceConnection extends EventEmitter {
* @param {Object} data The received data
* @private
*/
onReady({ port, ssrc, ip }) {
this.authentication.port = port;
this.authentication.ssrc = ssrc;
this.sockets.udp.createUDPSocket(ip);
this.sockets.udp.socket.on('message', this.onUDPMessage.bind(this));
onReady(data) {
Object.assign(this.authentication, data);
for (let mode of data.modes) {
if (SUPPORTED_MODES.includes(mode)) {
this.authentication.mode = mode;
this.emit('debug', `Selecting the ${mode} mode`);
break;
}
}
this.sockets.udp.createUDPSocket(data.ip);
}
/**
* Invoked when a session description is received.
* @param {string} mode The encryption mode
* @param {string} secret The secret key
* @param {Object} data The received data
* @private
*/
onSessionDescription(mode, secret) {
this.authentication.encryptionMode = mode;
this.authentication.secretKey = secret;
this.status = Constants.VoiceStatus.CONNECTED;
onSessionDescription(data) {
Object.assign(this.authentication, data);
this.status = VoiceStatus.CONNECTED;
const ready = () => {
this.client.clearTimeout(this.connectTimeout);
this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`);
/**
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
* the connection will already be ready.
@@ -424,17 +468,13 @@ class VoiceConnection extends EventEmitter {
ready();
} else {
// This serves to provide support for voice receive, sending audio is required to receive it.
this.playOpusStream(new SingleSilence()).once('end', ready);
const dispatcher = this.play(new SingleSilence(), { type: 'opus' });
dispatcher.once('finish', ready);
}
}
/**
* Invoked whenever a user initially starts speaking.
* @param {Object} data The speaking data
* @private
*/
onStartSpeaking({ user_id, ssrc }) {
this.ssrcMap.set(+ssrc, user_id);
onStartSpeaking({ user_id, ssrc, speaking }) {
this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking });
}
/**
@@ -443,156 +483,41 @@ class VoiceConnection extends EventEmitter {
* @private
*/
onSpeaking({ user_id, speaking }) {
speaking = new Speaking(speaking).freeze();
const guild = this.channel.guild;
const user = this.client.users.get(user_id);
if (!speaking) {
for (const receiver of this.receivers) {
receiver.stoppedSpeaking(user);
}
}
const user = this.client.users.cache.get(user_id);
const old = this._speaking.get(user_id);
this._speaking.set(user_id, speaking);
/**
* Emitted whenever a user starts/stops speaking.
* Emitted whenever a user changes speaking state.
* @event VoiceConnection#speaking
* @param {User} user The user that has started/stopped speaking
* @param {boolean} speaking Whether or not the user is speaking
* @param {User} user The user that has changed speaking state
* @param {Readonly<Speaking>} speaking The speaking state of the user
*/
if (this.status === Constants.VoiceStatus.CONNECTED) this.emit('speaking', user, speaking);
guild._memberSpeakUpdate(user_id, speaking);
}
/**
* Handles synthesizing of the speaking event.
* @param {Buffer} buffer Received packet from the UDP socket
* @private
*/
onUDPMessage(buffer) {
const ssrc = +buffer.readUInt32BE(8).toString(10);
const user = this.client.users.get(this.ssrcMap.get(ssrc));
if (!user) return;
let speakingTimeout = this.speakingTimeouts.get(ssrc);
if (typeof speakingTimeout === 'undefined') {
this.onSpeaking({ user_id: user.id, ssrc, speaking: true });
} else {
this.client.clearTimeout(speakingTimeout);
if (this.status === VoiceStatus.CONNECTED) {
this.emit('speaking', user, speaking);
if (!speaking.has(Speaking.FLAGS.SPEAKING)) {
this.receiver.packets._stoppedSpeaking(user_id);
}
}
speakingTimeout = this.client.setTimeout(() => {
try {
this.onSpeaking({ user_id: user.id, ssrc, speaking: false });
this.client.clearTimeout(speakingTimeout);
this.speakingTimeouts.delete(ssrc);
} catch (ex) {
// Connection already closed, ignore
if (guild && user && !speaking.equals(old)) {
const member = guild.member(user);
if (member) {
/**
* Emitted once a guild member changes speaking state.
* @event Client#guildMemberSpeaking
* @param {GuildMember} member The member that started/stopped speaking
* @param {Readonly<Speaking>} speaking The speaking state of the member
*/
this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking);
}
}, DISCORD_SPEAKING_DELAY);
this.speakingTimeouts.set(ssrc, speakingTimeout);
}
}
/**
* Options that can be passed to stream-playing methods:
* @typedef {Object} StreamOptions
* @property {number} [seek=0] The time to seek to
* @property {number} [volume=1] The volume to play at
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
* @property {number|string} [bitrate=48000] The bitrate (quality) of the audio.
* If set to 'auto', the voice channel's bitrate will be used
*/
/**
* Play the given file in the voice connection.
* @param {string} file The absolute path to the file
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play files natively
* voiceChannel.join()
* .then(connection => {
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
* })
* .catch(console.error);
*/
playFile(file, options) {
return this.player.playUnknownStream(`file:${file}`, options);
}
/**
* Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
* @param {string} input the arbitrary input
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playArbitraryInput(input, options) {
return this.player.playUnknownStream(input, options);
}
/**
* Plays and converts an audio stream in the voice connection.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play streams using ytdl-core
* const ytdl = require('ytdl-core');
* const streamOptions = { seek: 0, volume: 1 };
* voiceChannel.join()
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* const dispatcher = connection.playStream(stream, streamOptions);
* })
* .catch(console.error);
*/
playStream(stream, options) {
return this.player.playUnknownStream(stream, options);
}
/**
* Plays a stream of 16-bit signed stereo PCM.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playConvertedStream(stream, options) {
return this.player.playPCMStream(stream, options);
}
/**
* Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playOpusStream(stream, options) {
return this.player.playOpusStream(stream, options);
}
/**
* Plays a voice broadcast.
* @param {VoiceBroadcast} broadcast The broadcast to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play a broadcast
* const broadcast = client
* .createVoiceBroadcast()
* .playFile('./test.mp3');
* const dispatcher = voiceConnection.playBroadcast(broadcast);
*/
playBroadcast(broadcast, options) {
return this.player.playBroadcast(broadcast, options);
}
/**
* Creates a VoiceReceiver so you can start listening to voice data.
* It's recommended to only create one of these.
* @returns {VoiceReceiver}
*/
createReceiver() {
const receiver = new VoiceReceiver(this);
this.receivers.push(receiver);
return receiver;
}
play() {} // eslint-disable-line no-empty-function
}
PlayInterface.applyToClass(VoiceConnection);
module.exports = VoiceConnection;

View File

@@ -0,0 +1,46 @@
'use strict';
const StreamDispatcher = require('./StreamDispatcher');
/**
* The class that sends voice packet data to the voice connection.
* @implements {VolumeInterface}
* @extends {StreamDispatcher}
*/
class BroadcastDispatcher extends StreamDispatcher {
constructor(player, options, streams) {
super(player, options, streams);
this.broadcast = player.broadcast;
}
_write(chunk, enc, done) {
if (!this.startTime) this.startTime = Date.now();
for (const dispatcher of this.broadcast.subscribers) {
dispatcher._write(chunk, enc);
}
this._step(done);
}
_destroy(err, cb) {
if (this.player.dispatcher === this) this.player.dispatcher = null;
const { streams } = this;
if (streams.opus) streams.opus.unpipe(this);
if (streams.ffmpeg) streams.ffmpeg.destroy();
super._destroy(err, cb);
}
/**
* Set the bitrate of the current Opus encoder if using a compatible Opus stream.
* @param {number} value New bitrate, in kbps
* If set to 'auto', 48kbps will be used
* @returns {boolean} true if the bitrate has been successfully changed.
*/
setBitrate(value) {
if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false;
const bitrate = value === 'auto' ? 48 : value;
this.streams.opus.setBitrate(bitrate * 1000);
return true;
}
}
module.exports = BroadcastDispatcher;

View File

@@ -1,11 +1,21 @@
const VolumeInterface = require('../util/VolumeInterface');
const VoiceBroadcast = require('../VoiceBroadcast');
const Constants = require('../../../util/Constants');
'use strict';
const { Writable } = require('stream');
const secretbox = require('../util/Secretbox');
const Silence = require('../util/Silence');
const VolumeInterface = require('../util/VolumeInterface');
const FRAME_LENGTH = 20;
const CHANNELS = 2;
const TIMESTAMP_INC = (48000 / 100) * CHANNELS;
const MAX_NONCE_SIZE = 2 ** 32 - 1;
const nonce = Buffer.alloc(24);
nonce.fill(0);
/**
* @external WritableStream
* @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable}
*/
/**
* The class that sends voice packet data to the voice connection.
@@ -13,148 +23,253 @@ nonce.fill(0);
* // Obtained using:
* voiceChannel.join().then(connection => {
* // You can play a file or a stream here:
* const dispatcher = connection.playFile('./file.mp3');
* const dispatcher = connection.play('/home/hydrabolt/audio.mp3');
* });
* ```
* @implements {VolumeInterface}
* @extends {WritableStream}
*/
class StreamDispatcher extends VolumeInterface {
constructor(player, stream, streamOptions) {
class StreamDispatcher extends Writable {
constructor(player, { seek = 0, volume = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, streams) {
const streamOptions = { seek, volume, fec, plp, bitrate, highWaterMark };
super(streamOptions);
/**
* The Audio Player that controls this dispatcher
* @type {AudioPlayer}
*/
this.player = player;
/**
* The stream that the dispatcher plays
* @type {ReadableStream|VoiceBroadcast}
*/
this.stream = stream;
if (!(this.stream instanceof VoiceBroadcast)) this.startStreaming();
this.streamOptions = streamOptions;
this.streams = streams;
this.streams.silence = new Silence();
const data = this.streamingData;
data.length = 20;
data.missed = 0;
this._nonce = 0;
this._nonceBuffer = Buffer.alloc(24);
/**
* Whether playing is paused
* @type {boolean}
* The time that the stream was paused at (null if not paused)
* @type {?number}
*/
this.paused = false;
this.pausedSince = null;
this._writeCallback = null;
/**
* Whether this dispatcher has been destroyed
* @type {boolean}
* The broadcast controlling this dispatcher, if any
* @type {?VoiceBroadcast}
*/
this.destroyed = false;
this.broadcast = this.streams.broadcast;
this._opus = streamOptions.opus;
this._pausedTime = 0;
this._silentPausedTime = 0;
this.count = 0;
this.on('finish', () => {
this._cleanup();
this._setSpeaking(0);
});
this.setVolume(volume);
this.setBitrate(bitrate);
if (typeof fec !== 'undefined') this.setFEC(fec);
if (typeof plp !== 'undefined') this.setPLP(plp);
const streamError = (type, err) => {
/**
* Emitted when the dispatcher encounters an error.
* @event StreamDispatcher#error
*/
if (type && err) {
err.message = `${type} stream: ${err.message}`;
this.emit(this.player.dispatcher === this ? 'error' : 'debug', err);
}
this.destroy();
};
this.on('error', () => streamError());
if (this.streams.input) this.streams.input.on('error', err => streamError('input', err));
if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', err => streamError('ffmpeg', err));
if (this.streams.opus) this.streams.opus.on('error', err => streamError('opus', err));
if (this.streams.volume) this.streams.volume.on('error', err => streamError('volume', err));
}
/**
* How many passes the dispatcher should take when sending packets to reduce packet loss. Values over 5
* aren't recommended, as it means you are using 5x more bandwidth. You _can_ edit this at runtime
* @type {number}
* @readonly
*/
get passes() {
return this.streamOptions.passes || 1;
}
set passes(n) {
this.streamOptions.passes = n;
}
get streamingData() {
get _sdata() {
return this.player.streamingData;
}
/**
* How long the stream dispatcher has been "speaking" for
* @type {number}
* @readonly
*/
get time() {
return this.streamingData.count * (this.streamingData.length || 0);
_write(chunk, enc, done) {
if (!this.startTime) {
/**
* Emitted once the stream has started to play.
* @event StreamDispatcher#start
*/
this.emit('start');
this.startTime = Date.now();
}
this._playChunk(chunk);
this._step(done);
}
_destroy(err, cb) {
this._cleanup();
super._destroy(err, cb);
}
_cleanup() {
if (this.player.dispatcher === this) this.player.dispatcher = null;
const { streams } = this;
if (streams.broadcast) streams.broadcast.delete(this);
if (streams.opus) streams.opus.destroy();
if (streams.ffmpeg) streams.ffmpeg.destroy();
}
/**
* The total time, taking into account pauses and skips, that the dispatcher has been streaming for
* Pauses playback
* @param {boolean} [silence=false] Whether to play silence while paused to prevent audio glitches
*/
pause(silence = false) {
if (this.paused) return;
if (this.streams.opus) this.streams.opus.unpipe(this);
if (silence) {
this.streams.silence.pipe(this);
this._silence = true;
} else {
this._setSpeaking(0);
}
this.pausedSince = Date.now();
}
/**
* Whether or not playback is paused
* @type {boolean}
* @readonly
*/
get paused() {
return Boolean(this.pausedSince);
}
/**
* Total time that this dispatcher has been paused in milliseconds
* @type {number}
* @readonly
*/
get pausedTime() {
return this._silentPausedTime + this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0);
}
/**
* Resumes playback
*/
resume() {
if (!this.pausedSince) return;
this.streams.silence.unpipe(this);
if (this.streams.opus) this.streams.opus.pipe(this);
if (this._silence) {
this._silentPausedTime += Date.now() - this.pausedSince;
this._silence = false;
} else {
this._pausedTime += Date.now() - this.pausedSince;
}
this.pausedSince = null;
if (typeof this._writeCallback === 'function') this._writeCallback();
}
/**
* The time (in milliseconds) that the dispatcher has actually been playing audio for
* @type {number}
* @readonly
*/
get streamTime() {
return this.count * FRAME_LENGTH;
}
/**
* The time (in milliseconds) that the dispatcher has been playing audio for, taking into account skips and pauses
* @type {number}
* @readonly
*/
get totalStreamTime() {
return this.time + this.streamingData.pausedTime;
return Date.now() - this.startTime;
}
/**
* Stops sending voice packets to the voice connection (stream may still progress however).
*/
pause() { this.setPaused(true); }
/**
* Resumes sending voice packets to the voice connection (may be further on in the stream than when paused).
*/
resume() { this.setPaused(false); }
/**
* Stops the current stream permanently and emits an `end` event.
* @param {string} [reason='user'] An optional reason for stopping the dispatcher
*/
end(reason = 'user') {
this.destroy('end', reason);
}
setSpeaking(value) {
if (this.speaking === value) return;
if (this.player.voiceConnection.status !== Constants.VoiceStatus.CONNECTED) return;
this.speaking = value;
/**
* Emitted when the dispatcher starts/stops speaking.
* @event StreamDispatcher#speaking
* @param {boolean} value Whether or not the dispatcher is speaking
*/
this.emit('speaking', value);
}
/**
* Set the bitrate of the current Opus encoder.
* @param {number} bitrate New bitrate, in kbps
* Set the bitrate of the current Opus encoder if using a compatible Opus stream.
* @param {number} value New bitrate, in kbps
* If set to 'auto', the voice channel's bitrate will be used
* @returns {boolean} true if the bitrate has been successfully changed.
*/
setBitrate(bitrate) {
this.player.setBitrate(bitrate);
setBitrate(value) {
if (!value || !this.bitrateEditable) return false;
const bitrate = value === 'auto' ? this.player.voiceConnection.channel.bitrate : value;
this.streams.opus.setBitrate(bitrate * 1000);
return true;
}
sendBuffer(buffer, sequence, timestamp, opusPacket) {
opusPacket = opusPacket || this.player.opusEncoder.encode(buffer);
const packet = this.createPacket(sequence, timestamp, opusPacket);
this.sendPacket(packet);
/**
* Sets the expected packet loss percentage if using a compatible Opus stream.
* @param {number} value between 0 and 1
* @returns {boolean} Returns true if it was successfully set.
*/
setPLP(value) {
if (!this.bitrateEditable) return false;
this.streams.opus.setPLP(value);
return true;
}
sendPacket(packet) {
let repeats = this.passes;
/**
* Emitted whenever the dispatcher has debug information.
* @event StreamDispatcher#debug
* @param {string} info The debug info
*/
this.setSpeaking(true);
while (repeats-- && this.player.voiceConnection.sockets.udp) {
this.player.voiceConnection.sockets.udp.send(packet)
.catch(e => {
this.setSpeaking(false);
this.emit('debug', `Failed to send a packet ${e}`);
});
/**
* Enables or disables forward error correction if using a compatible Opus stream.
* @param {boolean} enabled true to enable
* @returns {boolean} Returns true if it was successfully set.
*/
setFEC(enabled) {
if (!this.bitrateEditable) return false;
this.streams.opus.setFEC(enabled);
return true;
}
_step(done) {
this._writeCallback = () => {
this._writeCallback = null;
done();
};
if (!this.streams.broadcast) {
const next = FRAME_LENGTH + this.count * FRAME_LENGTH - (Date.now() - this.startTime - this._pausedTime);
setTimeout(() => {
if ((!this.pausedSince || this._silence) && this._writeCallback) this._writeCallback();
}, next);
}
this._sdata.sequence++;
this._sdata.timestamp += TIMESTAMP_INC;
if (this._sdata.sequence >= 2 ** 16) this._sdata.sequence = 0;
if (this._sdata.timestamp >= 2 ** 32) this._sdata.timestamp = 0;
this.count++;
}
_final(callback) {
this._writeCallback = null;
callback();
}
_playChunk(chunk) {
if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secret_key) return;
this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk));
}
_encrypt(buffer) {
const { secret_key, mode } = this.player.voiceConnection.authentication;
if (mode === 'xsalsa20_poly1305_lite') {
this._nonce++;
if (this._nonce > MAX_NONCE_SIZE) this._nonce = 0;
this._nonceBuffer.writeUInt32BE(this._nonce, 0);
return [secretbox.methods.close(buffer, this._nonceBuffer, secret_key), this._nonceBuffer.slice(0, 4)];
} else if (mode === 'xsalsa20_poly1305_suffix') {
const random = secretbox.methods.random(24);
return [secretbox.methods.close(buffer, random, secret_key), random];
} else {
return [secretbox.methods.close(buffer, nonce, secret_key)];
}
}
createPacket(sequence, timestamp, buffer) {
const packetBuffer = Buffer.alloc(buffer.length + 28);
packetBuffer.fill(0);
_createPacket(sequence, timestamp, buffer) {
const packetBuffer = Buffer.alloc(12);
packetBuffer[0] = 0x80;
packetBuffer[1] = 0x78;
@@ -163,169 +278,77 @@ class StreamDispatcher extends VolumeInterface {
packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4);
packetBuffer.copy(nonce, 0, 0, 12);
buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key);
for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
return packetBuffer;
return Buffer.concat([packetBuffer, ...this._encrypt(buffer)]);
}
processPacket(packet) {
try {
if (this.destroyed || !this.player.voiceConnection.authentication.secretKey) {
this.setSpeaking(false);
return;
}
const data = this.streamingData;
if (this.paused) {
this.setSpeaking(false);
data.pausedTime = data.length * 10;
return;
}
if (!packet) {
data.missed++;
data.pausedTime += data.length * 10;
return;
}
this.started();
this.missed = 0;
this.stepStreamingData();
this.sendBuffer(null, data.sequence, data.timestamp, packet);
} catch (e) {
this.destroy('error', e);
}
}
process() {
try {
if (this.destroyed) {
this.setSpeaking(false);
return;
}
const data = this.streamingData;
if (data.missed >= 5) {
this.destroy('end', 'Stream is not generating quickly enough.');
return;
}
if (this.paused) {
this.setSpeaking(false);
// Old code?
// data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
data.pausedTime += data.length * 10;
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
return;
}
this.started();
const buffer = this.readStreamBuffer();
if (!buffer) {
data.missed++;
data.pausedTime += data.length * 10;
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
return;
}
data.missed = 0;
this.stepStreamingData();
if (this._opus) {
this.sendBuffer(null, data.sequence, data.timestamp, buffer);
} else {
this.sendBuffer(buffer, data.sequence, data.timestamp);
}
const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now());
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), nextTime);
} catch (e) {
this.destroy('error', e);
}
}
readStreamBuffer() {
const data = this.streamingData;
const bufferLength = (this._opus ? 80 : 1920) * data.channels;
let buffer = this.stream.read(bufferLength);
if (this._opus) return buffer;
if (!buffer) return null;
if (buffer.length !== bufferLength) {
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer);
buffer = newBuffer;
}
buffer = this.applyVolume(buffer);
return buffer;
}
started() {
const data = this.streamingData;
if (!data.startTime) {
/**
* Emitted once the dispatcher starts streaming.
* @event StreamDispatcher#start
*/
this.emit('start');
data.startTime = Date.now();
}
}
stepStreamingData() {
const data = this.streamingData;
data.count++;
data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0;
data.timestamp = (data.timestamp + 960) < 4294967295 ? data.timestamp + 960 : 0;
}
destroy(type, reason) {
if (this.destroyed) return;
this.destroyed = true;
this.setSpeaking(false);
this.emit(type, reason);
_sendPacket(packet) {
/**
* Emitted once the dispatcher ends.
* @param {string} [reason] The reason the dispatcher ended
* @event StreamDispatcher#end
* Emitted whenever the dispatcher has debug information.
* @event StreamDispatcher#debug
* @param {string} info The debug info
*/
if (type !== 'end') this.emit('end', `destroyed due to ${type} - ${reason}`);
}
startStreaming() {
if (!this.stream) {
/**
* Emitted if the dispatcher encounters an error.
* @event StreamDispatcher#error
* @param {string} error The error message
*/
this.emit('error', 'No stream');
this._setSpeaking(1);
if (!this.player.voiceConnection.sockets.udp) {
this.emit('debug', 'Failed to send a packet - no UDP socket');
return;
}
this.stream.on('end', err => this.destroy('end', err || 'stream'));
this.stream.on('error', err => this.destroy('error', err));
const data = this.streamingData;
data.length = 20;
data.missed = 0;
this.stream.once('readable', () => {
data.startTime = null;
data.count = 0;
this.process();
this.player.voiceConnection.sockets.udp.send(packet).catch(e => {
this._setSpeaking(0);
this.emit('debug', `Failed to send a packet - ${e}`);
});
}
setPaused(paused) { this.setSpeaking(!(this.paused = paused)); }
_setSpeaking(value) {
if (typeof this.player.voiceConnection !== 'undefined') {
this.player.voiceConnection.setSpeaking(value);
}
/**
* Emitted when the dispatcher starts/stops speaking.
* @event StreamDispatcher#speaking
* @param {boolean} value Whether or not the dispatcher is speaking
*/
this.emit('speaking', value);
}
get volumeEditable() {
return Boolean(this.streams.volume);
}
/**
* Whether or not the Opus bitrate of this stream is editable
* @type {boolean}
* @readonly
*/
get bitrateEditable() {
return this.streams.opus && this.streams.opus.setBitrate;
}
// Volume
get volume() {
return this.streams.volume ? this.streams.volume.volume : 1;
}
setVolume(value) {
if (!this.streams.volume) return false;
/**
* Emitted when the volume of this dispatcher changes.
* @event StreamDispatcher#volumeChange
* @param {number} oldVolume The old volume of this dispatcher
* @param {number} newVolume The new volume of this dispatcher
*/
this.emit('volumeChange', this.volume, value);
this.streams.volume.setVolume(value);
return true;
}
// Volume stubs for docs
/* eslint-disable no-empty-function*/
get volumeDecibels() {}
get volumeLogarithmic() {}
setVolumeDecibels() {}
setVolumeLogarithmic() {}
}
VolumeInterface.applyToClass(StreamDispatcher);
module.exports = StreamDispatcher;

View File

@@ -1,6 +1,9 @@
'use strict';
const udp = require('dgram');
const Constants = require('../../util/Constants');
const EventEmitter = require('events').EventEmitter;
const EventEmitter = require('events');
const { Error } = require('../../../errors');
const { VoiceOPCodes } = require('../../../util/Constants');
/**
* Represents a UDP client for a Voice Connection.
@@ -45,6 +48,7 @@ class VoiceConnectionUDPClient extends EventEmitter {
}
shutdown() {
this.emit('debug', `[UDP] shutdown requested`);
if (this.socket) {
this.socket.removeAllListeners('message');
try {
@@ -71,21 +75,38 @@ class VoiceConnectionUDPClient extends EventEmitter {
*/
send(packet) {
return new Promise((resolve, reject) => {
if (!this.socket) throw new Error('Tried to send a UDP packet, but there is no socket available.');
if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.');
if (!this.socket) throw new Error('UDP_SEND_FAIL');
if (!this.discordAddress || !this.discordPort) throw new Error('UDP_ADDRESS_MALFORMED');
this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => {
if (error) reject(error); else resolve(packet);
if (error) {
this.emit('debug', `[UDP] >> ERROR: ${error}`);
reject(error);
} else {
resolve(packet);
}
});
});
}
createUDPSocket(address) {
async createUDPSocket(address) {
this.discordAddress = address;
const socket = this.socket = udp.createSocket('udp4');
const socket = (this.socket = udp.createSocket('udp4'));
socket.on('error', e => {
this.emit('debug', `[UDP] Error: ${e}`);
this.emit('error', e);
});
socket.on('close', () => {
this.emit('debug', '[UDP] socket closed');
});
this.emit('debug', `[UDP] created socket`);
socket.once('message', message => {
this.emit('debug', `[UDP] message: [${[...message]}] (${message})`);
// Stop if the sockets have been deleted because the connection has been closed already
if (!this.voiceConnection.sockets.ws) return;
const packet = parseLocalPacket(message);
if (packet.error) {
this.emit('debug', `[UDP] ERROR: ${packet.error}`);
this.emit('error', packet.error);
return;
}
@@ -94,21 +115,27 @@ class VoiceConnectionUDPClient extends EventEmitter {
this.localPort = packet.port;
this.voiceConnection.sockets.ws.sendPacket({
op: Constants.VoiceOPCodes.SELECT_PROTOCOL,
op: VoiceOPCodes.SELECT_PROTOCOL,
d: {
protocol: 'udp',
data: {
address: packet.address,
port: packet.port,
mode: 'xsalsa20_poly1305',
mode: this.voiceConnection.authentication.mode,
},
},
});
this.emit('debug', `[UDP] << ${JSON.stringify(packet)}`);
socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer));
});
const blankMessage = Buffer.alloc(70);
blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4);
this.send(blankMessage);
this.emit('debug', `Sending IP discovery packet: [${[...blankMessage]}]`);
await this.send(blankMessage);
this.emit('debug', `Successfully sent IP discovery packet`);
}
}

View File

@@ -1,13 +1,9 @@
const Constants = require('../../util/Constants');
const SecretKey = require('./util/SecretKey');
const EventEmitter = require('events').EventEmitter;
'use strict';
let WebSocket;
try {
WebSocket = require('@discordjs/uws');
} catch (err) {
WebSocket = require('ws');
}
const EventEmitter = require('events');
const WebSocket = require('../../../WebSocket');
const { Error } = require('../../../errors');
const { OPCodes, VoiceOPCodes } = require('../../../util/Constants');
/**
* Represents a Voice Connection's WebSocket.
@@ -15,20 +11,13 @@ try {
* @private
*/
class VoiceWebSocket extends EventEmitter {
constructor(voiceConnection) {
constructor(connection) {
super();
/**
* The client of this voice WebSocket
* @type {Client}
*/
this.client = voiceConnection.voiceManager.client;
/**
* The Voice Connection that this WebSocket serves
* @type {VoiceConnection}
*/
this.voiceConnection = voiceConnection;
this.connection = connection;
/**
* How many connection attempts have been made
@@ -36,12 +25,21 @@ class VoiceWebSocket extends EventEmitter {
*/
this.attempts = 0;
this.connect();
this.dead = false;
this.voiceConnection.on('closing', this.shutdown.bind(this));
this.connection.on('closing', this.shutdown.bind(this));
}
/**
* The client of this voice WebSocket
* @type {Client}
* @readonly
*/
get client() {
return this.connection.client;
}
shutdown() {
this.emit('debug', `[WS] shutdown requested`);
this.dead = true;
this.reset();
}
@@ -50,6 +48,7 @@ class VoiceWebSocket extends EventEmitter {
* Resets the current WebSocket.
*/
reset() {
this.emit('debug', `[WS] reset requested`);
if (this.ws) {
if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close();
this.ws = null;
@@ -61,10 +60,11 @@ class VoiceWebSocket extends EventEmitter {
* Starts connecting to the Voice WebSocket Server.
*/
connect() {
this.emit('debug', `[WS] connect requested`);
if (this.dead) return;
if (this.ws) this.reset();
if (this.attempts >= 5) {
this.emit('debug', new Error(`Too many connection attempts (${this.attempts}).`));
this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
return;
}
@@ -74,7 +74,8 @@ class VoiceWebSocket extends EventEmitter {
* The actual WebSocket used to connect to the Voice WebSocket Server.
* @type {WebSocket}
*/
this.ws = new WebSocket(`wss://${this.voiceConnection.authentication.endpoint}`);
this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 4 });
this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`);
this.ws.onopen = this.onOpen.bind(this);
this.ws.onmessage = this.onMessage.bind(this);
this.ws.onclose = this.onClose.bind(this);
@@ -87,12 +88,12 @@ class VoiceWebSocket extends EventEmitter {
* @returns {Promise<string>}
*/
send(data) {
this.emit('debug', `[WS] >> ${data}`);
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error(`Voice websocket not open to send ${data}.`);
}
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data);
this.ws.send(data, null, error => {
if (error) reject(error); else resolve(data);
if (error) reject(error);
else resolve(data);
});
});
}
@@ -115,16 +116,17 @@ class VoiceWebSocket extends EventEmitter {
* Called whenever the WebSocket opens.
*/
onOpen() {
this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`);
this.sendPacket({
op: Constants.OPCodes.DISPATCH,
op: OPCodes.DISPATCH,
d: {
server_id: this.voiceConnection.channel.guild.id,
server_id: this.connection.channel.guild.id,
user_id: this.client.user.id,
token: this.voiceConnection.authentication.token,
session_id: this.voiceConnection.authentication.sessionID,
token: this.connection.authentication.token,
session_id: this.connection.authentication.sessionID,
},
}).catch(() => {
this.emit('error', new Error('Tried to send join packet, but the WebSocket is not open.'));
this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
});
}
@@ -135,7 +137,7 @@ class VoiceWebSocket extends EventEmitter {
*/
onMessage(event) {
try {
return this.onPacket(JSON.parse(event.data));
return this.onPacket(WebSocket.unpack(event.data, 'json'));
} catch (error) {
return this.onError(error);
}
@@ -145,6 +147,7 @@ class VoiceWebSocket extends EventEmitter {
* Called whenever the connection to the WebSocket server is lost.
*/
onClose() {
this.emit('debug', `[WS] closed`);
if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000);
}
@@ -153,6 +156,7 @@ class VoiceWebSocket extends EventEmitter {
* @param {Error} error The error that occurred
*/
onError(error) {
this.emit('debug', `[WS] Error: ${error}`);
this.emit('error', error);
}
@@ -161,9 +165,12 @@ class VoiceWebSocket extends EventEmitter {
* @param {Object} packet The received packet
*/
onPacket(packet) {
this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
switch (packet.op) {
case Constants.VoiceOPCodes.READY:
case VoiceOPCodes.HELLO:
this.setHeartbeat(packet.d.heartbeat_interval);
break;
case VoiceOPCodes.READY:
/**
* Emitted once the voice WebSocket receives the ready packet.
* @param {Object} packet The received packet
@@ -171,16 +178,27 @@ class VoiceWebSocket extends EventEmitter {
*/
this.emit('ready', packet.d);
break;
case Constants.VoiceOPCodes.SESSION_DESCRIPTION:
/* eslint-disable no-case-declarations */
case VoiceOPCodes.SESSION_DESCRIPTION:
packet.d.secret_key = new Uint8Array(packet.d.secret_key);
/**
* Emitted once the Voice Websocket receives a description of this voice session.
* @param {string} encryptionMode The type of encryption being used
* @param {SecretKey} secretKey The secret key used for encryption
* @param {Object} packet The received packet
* @event VoiceWebSocket#sessionDescription
*/
this.emit('sessionDescription', packet.d.mode, new SecretKey(packet.d.secret_key));
this.emit('sessionDescription', packet.d);
break;
case Constants.VoiceOPCodes.SPEAKING:
case VoiceOPCodes.CLIENT_CONNECT:
this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id);
break;
case VoiceOPCodes.CLIENT_DISCONNECT:
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);
if (streamInfo) {
this.connection.receiver.packets.streams.delete(packet.d.user_id);
streamInfo.stream.push(null);
}
break;
case VoiceOPCodes.SPEAKING:
/**
* Emitted whenever a speaking packet is received.
* @param {Object} data
@@ -205,17 +223,17 @@ class VoiceWebSocket extends EventEmitter {
*/
setHeartbeat(interval) {
if (!interval || isNaN(interval)) {
this.onError(new Error('Tried to set voice heartbeat but no valid interval was specified.'));
this.onError(new Error('VOICE_INVALID_HEARTBEAT'));
return;
}
if (this.heartbeatInterval) {
/**
* Emitted whenver the voice WebSocket encounters a non-fatal error.
* Emitted whenever the voice WebSocket encounters a non-fatal error.
* @param {string} warn The warning
* @event VoiceWebSocket#warn
*/
this.emit('warn', 'A voice heartbeat interval is being overwritten');
clearInterval(this.heartbeatInterval);
this.client.clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval);
}
@@ -228,7 +246,7 @@ class VoiceWebSocket extends EventEmitter {
this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
return;
}
clearInterval(this.heartbeatInterval);
this.client.clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
@@ -236,7 +254,7 @@ class VoiceWebSocket extends EventEmitter {
* Sends a heartbeat packet.
*/
sendHeartbeat() {
this.sendPacket({ op: Constants.VoiceOPCodes.HEARTBEAT, d: null }).catch(() => {
this.sendPacket({ op: VoiceOPCodes.HEARTBEAT, d: Math.floor(Math.random() * 10e10) }).catch(() => {
this.emit('warn', 'Tried to send heartbeat, but connection is not open');
this.clearHeartbeat();
});

View File

@@ -1,60 +0,0 @@
/**
* The base opus encoding engine.
* @private
*/
class BaseOpus {
/**
* @param {Object} [options] The options to apply to the Opus engine
* @param {number} [options.bitrate=48] The desired bitrate (kbps)
* @param {boolean} [options.fec=false] Whether to enable forward error correction
* @param {number} [options.plp=0] The expected packet loss percentage
*/
constructor({ bitrate = 48, fec = false, plp = 0 } = {}) {
this.ctl = {
BITRATE: 4002,
FEC: 4012,
PLP: 4014,
};
this.samplingRate = 48000;
this.channels = 2;
/**
* The desired bitrate (kbps)
* @type {number}
*/
this.bitrate = bitrate;
/**
* Miscellaneous Opus options
* @type {Object}
*/
this.options = { fec, plp };
}
init() {
try {
this.setBitrate(this.bitrate);
// Set FEC (forward error correction)
if (this.options.fec) this.setFEC(this.options.fec);
// Set PLP (expected packet loss percentage)
if (this.options.plp) this.setPLP(this.options.plp);
} catch (err) {
// Opus engine likely has no support for libopus CTL
}
}
encode(buffer) {
return buffer;
}
decode(buffer) {
return buffer;
}
destroy() {} // eslint-disable-line no-empty-function
}
module.exports = BaseOpus;

View File

@@ -1,34 +0,0 @@
const OpusEngine = require('./BaseOpusEngine');
class DiscordJsOpusEngine extends OpusEngine {
constructor(player) {
super(player);
const opus = require('@discordjs/opus');
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
super.init();
}
setBitrate(bitrate) {
this.encoder.setBitrate(Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) {
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
}
setPLP(percent) {
this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100)));
}
encode(buffer) {
super.encode(buffer);
return this.encoder.encode(buffer, 1920);
}
decode(buffer) {
super.decode(buffer);
return this.encoder.decode(buffer, 1920);
}
}
module.exports = DiscordJsOpusEngine;

View File

@@ -1,34 +0,0 @@
const OpusEngine = require('./BaseOpusEngine');
class NodeOpusEngine extends OpusEngine {
constructor(player) {
super(player);
const opus = require('node-opus');
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
super.init();
}
setBitrate(bitrate) {
this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) {
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
}
setPLP(percent) {
this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100)));
}
encode(buffer) {
super.encode(buffer);
return this.encoder.encode(buffer, 1920);
}
decode(buffer) {
super.decode(buffer);
return this.encoder.decode(buffer, 1920);
}
}
module.exports = NodeOpusEngine;

Some files were not shown because too many files have changed in this diff Show More