mirror of
https://github.com/danbulant/discord.js
synced 2026-06-22 00:02:13 +00:00
Merge branch 'master' into reply-prefix
This commit is contained in:
commit
63124df89f
263 changed files with 11771 additions and 7358 deletions
|
|
@ -1,47 +1,65 @@
|
|||
{
|
||||
"extends": "eslint:recommended",
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"plugins": ["import"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
"ecmaVersion": 2019
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"overrides": [
|
||||
{ "files": ["*.browser.js"], "env": { "browser": 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"],
|
||||
|
|
@ -99,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",
|
||||
|
|
@ -111,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",
|
||||
|
|
@ -121,18 +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", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}
|
||||
],
|
||||
"space-in-parens": "error",
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": "error",
|
||||
|
|
|
|||
3
.github/CONTRIBUTING.md
vendored
3
.github/CONTRIBUTING.md
vendored
|
|
@ -7,11 +7,12 @@ 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
|
||||
2. Run `npm install`
|
||||
3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript`
|
||||
3. If you're working on voice, also run `npm install @discordjs/opus` or `npm install opusscript`
|
||||
4. Code your heart out!
|
||||
5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
|
||||
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare)
|
||||
|
|
|
|||
11
.github/FUNDING.yml
vendored
Normal file
11
.github/FUNDING.yml
vendored
Normal 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
|
||||
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,9 +1,11 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
|
@ -13,26 +15,25 @@ 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 either of these applies to you, please check the respective checkbox: [ ] becomes [x].
|
||||
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 found this issue while running code on a __user account__
|
||||
|
||||
- [ ] I have also tested the issue on latest master, commit hash:
|
||||
|
|
|
|||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,9 +1,11 @@
|
|||
---
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
name: Question / General support request
|
||||
about: Ask for help in Discord instead - https://discord.gg/bRCvFy9
|
||||
|
||||
---
|
||||
|
||||
Seriously, we only use this issue tracker for bugs in the library itself and feature requests for it.
|
||||
We don't typically answer questions or help with support issues here.
|
||||
Instead, please ask in one of the 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.
|
||||
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,11 +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
7
.github/SUPPORT.md
vendored
Normal 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
47
.github/workflows/deploy.yml
vendored
Normal 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
58
.github/workflows/test-cron.yml
vendored
Normal 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
56
.github/workflows/test.yml
vendored
Normal 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,12 +1,8 @@
|
|||
{
|
||||
"ecmaVersion": 7,
|
||||
"libs": [],
|
||||
"loadEagerly": [
|
||||
"./src/*.js"
|
||||
],
|
||||
"dontLoad": [
|
||||
"node_modules/**"
|
||||
],
|
||||
"loadEagerly": ["./src/*.js"],
|
||||
"dontLoad": ["node_modules/**"],
|
||||
"plugins": {
|
||||
"es_modules": {},
|
||||
"node": {},
|
||||
|
|
|
|||
19
.travis.yml
19
.travis.yml
|
|
@ -1,19 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 8
|
||||
- 9
|
||||
install: npm install
|
||||
script: bash ./travis/test.sh
|
||||
jobs:
|
||||
include:
|
||||
- stage: deploy
|
||||
node_js: 9
|
||||
script: bash ./travis/deploy.sh
|
||||
env:
|
||||
- ENCRYPTION_LABEL="af862fa96d3e"
|
||||
- COMMIT_AUTHOR_EMAIL="amishshah.2k@gmail.com"
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
dist: trusty
|
||||
sudo: false
|
||||
2
LICENSE
2
LICENSE
|
|
@ -175,7 +175,7 @@
|
|||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 - 2018 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.
|
||||
|
|
|
|||
70
README.md
70
README.md
|
|
@ -8,7 +8,7 @@
|
|||
<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>
|
||||
|
|
@ -17,9 +17,22 @@
|
|||
</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
|
||||
|
|
@ -27,28 +40,32 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
|
|||
- 100% coverage of the Discord API
|
||||
|
||||
## Installation
|
||||
**Node.js 8.0.0 or newer is required.**
|
||||
|
||||
**Node.js 11.0.0 or newer is required.**
|
||||
Ignore any warnings about unmet peer dependencies, as they're all optional.
|
||||
|
||||
Without voice support: `npm install discord.js`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
|
||||
Without voice support: `npm install discordjs/discord.js`
|
||||
With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discordjs/discord.js @discordjs/opus`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript`
|
||||
|
||||
### Audio engines
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
Using opusscript is only recommended for development environments where node-opus is tough to get working.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
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
|
||||
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`)
|
||||
|
||||
- [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`)
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`)
|
||||
- [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();
|
||||
|
|
@ -67,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).
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ client.on('ready', () => {
|
|||
});
|
||||
|
||||
client.on('message', message => {
|
||||
// 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);
|
||||
}
|
||||
// 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
|
||||
|
|
@ -59,13 +59,13 @@ client.on('ready', () => {
|
|||
});
|
||||
|
||||
client.on('message', message => {
|
||||
// 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);
|
||||
}
|
||||
// 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
|
||||
|
|
@ -96,13 +96,13 @@ client.on('ready', () => {
|
|||
});
|
||||
|
||||
client.on('message', message => {
|
||||
// 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);
|
||||
}
|
||||
// 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
|
||||
|
|
@ -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 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);
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* An example of how you can send embeds
|
||||
*/
|
||||
|
|
@ -26,7 +28,7 @@ client.on('message', message => {
|
|||
// 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
|
||||
|
|
|
|||
|
|
@ -19,7 +19,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('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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -112,26 +115,29 @@ client.on('message', message => {
|
|||
* Read more about what ban options there are over at
|
||||
* 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!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 8.0.0 or newer.
|
||||
|
||||
Update to Node.js 11.0.0 or newer.
|
||||
|
||||
## How do I get voice working?
|
||||
|
||||
- Install FFMPEG.
|
||||
- Install either the `node-opus` package or the `opusscript` package.
|
||||
node-opus is greatly preferred, due to it having significantly better performance.
|
||||
- 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 node-opus?
|
||||
- **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats!
|
||||
## 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 node-opus` in your bot's directory should successfully build it. Woo!
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -1,72 +1,87 @@
|
|||
# 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
|
||||
|
|
@ -81,6 +96,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
|
||||
|
|
@ -90,41 +106,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`
|
||||
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
|
||||
|
|
@ -137,18 +158,21 @@ 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.
|
||||
For example, the following code:
|
||||
|
||||
|
|
@ -159,7 +183,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`);
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -18,14 +18,16 @@
|
|||
</div>
|
||||
|
||||
# Welcome!
|
||||
|
||||
Welcome to the discord.js v12 documentation.
|
||||
|
||||
v12 is still very much a work-in-progress, as we're aiming to make it the best it can possibly be before releasing.
|
||||
Only use it if you are fond of living life on the bleeding edge.
|
||||
|
||||
## 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
|
||||
|
|
@ -33,28 +35,32 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
|
|||
- 100% coverage of the Discord API
|
||||
|
||||
## Installation
|
||||
**Node.js 8.0.0 or newer is required.**
|
||||
|
||||
**Node.js 11.0.0 or newer is required.**
|
||||
Ignore any warnings about unmet peer dependencies, as they're all optional.
|
||||
|
||||
Without voice support: `npm install discord.js`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
|
||||
Without voice support: `npm install discordjs/discord.js`
|
||||
With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discordjs/discord.js @discordjs/opus`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript`
|
||||
|
||||
### Audio engines
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
Using opusscript is only recommended for development environments where node-opus is tough to get working.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
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
|
||||
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`)
|
||||
|
||||
- [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`)
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`)
|
||||
- [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 +79,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).
|
||||
|
|
|
|||
|
|
@ -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
65
docs/topics/partials.md
Normal 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.
|
||||
|
|
@ -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 node-opus` (better performance)
|
||||
* `npm install opusscript`
|
||||
* a good network connection
|
||||
|
||||
The preferred opus engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
Using opusscript is only recommended for development environments where node-opus is tough to get working.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
- 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.
|
||||
|
||||
|
|
@ -41,10 +44,12 @@ client.on('message', async 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.
|
||||
|
||||
### Introduction to playing on voice connections
|
||||
|
||||
The most basic example of playing audio over a connection would be playing a local file:
|
||||
|
||||
```js
|
||||
|
|
@ -71,12 +76,9 @@ We can also pass in options when we first play the stream:
|
|||
```js
|
||||
const dispatcher = connection.play('/home/discord/audio.mp3', {
|
||||
volume: 0.5,
|
||||
passes: 3
|
||||
});
|
||||
```
|
||||
|
||||
These are just a subset of the options available (consult documentation for a full list). Most users may be interested in the `passes` option, however. As audio is sent over UDP, there is a chance packets may not arrive. Increasing the number of passes, e.g. to `3` gives you a better chance that your packets reach your recipients, at the cost of triple the bandwidth. We recommend not going over 5 passes.
|
||||
|
||||
### What can I play?
|
||||
|
||||
Discord.js allows you to play a lot of things:
|
||||
|
|
@ -84,9 +86,7 @@ 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' }));
|
||||
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');
|
||||
|
|
@ -99,11 +99,11 @@ New to v12 is the ability to play OggOpus and WebmOpus streams with much better
|
|||
|
||||
```js
|
||||
connection.play(fs.createReadStream('./media.webm'), {
|
||||
type: 'webm/opus'
|
||||
type: 'webm/opus',
|
||||
});
|
||||
|
||||
connection.play(fs.createReadStream('./media.ogg'), {
|
||||
type: 'ogg/opus'
|
||||
type: 'ogg/opus',
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ Make sure to consult the documentation for a full list of what you can play - th
|
|||
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
|
||||
const broadcast = client.createVoiceBroadcast();
|
||||
const broadcast = client.voice.createBroadcast();
|
||||
|
||||
broadcast.on('subscribe', dispatcher => {
|
||||
console.log('New broadcast subscriber!');
|
||||
|
|
@ -122,7 +122,7 @@ broadcast.on('subscribe', dispatcher => {
|
|||
|
||||
broadcast.on('unsubscribe', dispatcher => {
|
||||
console.log('Channel unsubscribed from broadcast :(');
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
`broadcast` is an instance of `VoiceBroadcast`, which has the same `play` method you are used to with regular VoiceConnections:
|
||||
|
|
@ -136,4 +136,5 @@ 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™
|
||||
|
|
|
|||
|
|
@ -1,26 +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.
|
||||
|
||||
## 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>
|
||||
```
|
||||
|
|
@ -29,6 +35,7 @@ Rather than importing discord.js with `require('discord.js')`, the entire `Disco
|
|||
The usage of the API isn't any different from using it in Node.js.
|
||||
|
||||
#### Example
|
||||
|
||||
```html
|
||||
<script type="text/javascript" src="discord.11.1.0.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"plugins": ["node_modules/jsdoc-strip-async-await"]
|
||||
"plugins": ["node_modules/jsdoc-strip-async-await"]
|
||||
}
|
||||
|
|
|
|||
124
package.json
124
package.json
|
|
@ -8,9 +8,10 @@
|
|||
"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 *.js",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"lint": "eslint src",
|
||||
"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"
|
||||
},
|
||||
|
|
@ -35,61 +36,130 @@
|
|||
"runkitExampleFilename": "./docs/examples/ping.js",
|
||||
"unpkg": "./webpack/discord.min.js",
|
||||
"dependencies": {
|
||||
"form-data": "^2.3.2",
|
||||
"node-fetch": "^2.1.2",
|
||||
"pako": "^1.0.0",
|
||||
"prism-media": "amishshah/prism-media",
|
||||
"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",
|
||||
"sodium": "^2.0.3",
|
||||
"zlib-sync": "^0.1.4"
|
||||
"libsodium-wrappers": "^0.7.6",
|
||||
"sodium": "^3.0.2",
|
||||
"utf-8-validate": "^5.0.2",
|
||||
"zlib-sync": "^0.1.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"erlpack": {
|
||||
"optional": true
|
||||
},
|
||||
"libsodium-wrappers": {
|
||||
"optional": true
|
||||
},
|
||||
"sodium": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
},
|
||||
"zlib-sync": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^10.7.1",
|
||||
"@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",
|
||||
"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",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-config-typings": "^0.3.1",
|
||||
"typescript": "^3.0.1",
|
||||
"webpack": "^4.17.0",
|
||||
"webpack-cli": "^3.1.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": ">=8.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,
|
||||
"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/ClientVoiceManager.js": false,
|
||||
"src/client/voice/VoiceBroadcast.js": false,
|
||||
"src/client/voice/VoiceConnection.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/dispatcher/StreamDispatcher.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/VolumeInterface.js": false,
|
||||
"src/client/voice/VoiceBroadcast.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"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,47 @@
|
|||
'use strict';
|
||||
|
||||
const { browser } = require('./util/Constants');
|
||||
const querystring = require('querystring');
|
||||
|
||||
let erlpack;
|
||||
|
||||
try {
|
||||
var erlpack = require('erlpack');
|
||||
erlpack = require('erlpack');
|
||||
if (!erlpack.pack) erlpack = null;
|
||||
} catch (err) {} // eslint-disable-line no-empty
|
||||
} 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 {
|
||||
try {
|
||||
exports.WebSocket = require('@discordjs/uws');
|
||||
} catch (err) {
|
||||
exports.WebSocket = require('ws');
|
||||
}
|
||||
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 => {
|
||||
if (!erlpack || data[0] === '{') return JSON.parse(data);
|
||||
if (!(data instanceof Buffer)) data = Buffer.from(new Uint8Array(data));
|
||||
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;
|
||||
if (q) query = Object.assign(querystring.parse(q), query);
|
||||
const ws = new exports.WebSocket(`${g}?${querystring.stringify(query)}`, ...args);
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
require('setimmediate');
|
||||
const EventEmitter = require('events');
|
||||
const RESTManager = require('../rest/RESTManager');
|
||||
const Util = require('../util/Util');
|
||||
const { DefaultOptions } = require('../util/Constants');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* The base class for all clients.
|
||||
|
|
@ -25,6 +28,13 @@ class BaseClient extends EventEmitter {
|
|||
*/
|
||||
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}
|
||||
|
|
@ -53,10 +63,12 @@ class BaseClient extends EventEmitter {
|
|||
* Destroys all assets used by the base client.
|
||||
*/
|
||||
destroy() {
|
||||
for (const t of this._timeouts) clearTimeout(t);
|
||||
for (const i of this._intervals) clearInterval(i);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -106,6 +118,27 @@ class BaseClient extends EventEmitter {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
const BaseClient = require('./BaseClient');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const ClientManager = require('./ClientManager');
|
||||
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 { 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 ClientApplication = require('../structures/ClientApplication');
|
||||
const Invite = require('../structures/Invite');
|
||||
const VoiceRegion = require('../structures/VoiceRegion');
|
||||
const Webhook = require('../structures/Webhook');
|
||||
const Invite = require('../structures/Invite');
|
||||
const ClientApplication = require('../structures/ClientApplication');
|
||||
const ShardClientUtil = require('../sharding/ShardClientUtil');
|
||||
const VoiceBroadcast = require('./voice/VoiceBroadcast');
|
||||
const UserStore = require('../stores/UserStore');
|
||||
const ChannelStore = require('../stores/ChannelStore');
|
||||
const GuildStore = require('../stores/GuildStore');
|
||||
const GuildEmojiStore = require('../stores/GuildEmojiStore');
|
||||
const { Events, browser } = require('../util/Constants');
|
||||
const Collection = require('../util/Collection');
|
||||
const { Events, browser, DefaultOptions } = require('../util/Constants');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const Structures = require('../util/Structures');
|
||||
const { Error, TypeError, RangeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* The main hub for interacting with the Discord API, and the starting point for any bot.
|
||||
|
|
@ -31,27 +31,50 @@ class Client extends BaseClient {
|
|||
constructor(options = {}) {
|
||||
super(Object.assign({ _tokenType: 'Bot' }, options));
|
||||
|
||||
// Obtain shard details from environment
|
||||
if (!browser && !this.options.shardId && 'SHARD_ID' in process.env) {
|
||||
this.options.shardId = Number(process.env.SHARD_ID);
|
||||
// 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 (!browser && !this.options.shardCount && 'SHARD_COUNT' in process.env) {
|
||||
this.options.shardCount = Number(process.env.SHARD_COUNT);
|
||||
|
||||
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)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
this._validateOptions();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
|
@ -65,7 +88,6 @@ class Client extends BaseClient {
|
|||
/**
|
||||
* The voice manager of the client (`null` in browsers)
|
||||
* @type {?ClientVoiceManager}
|
||||
* @private
|
||||
*/
|
||||
this.voice = !browser ? new ClientVoiceManager(this) : null;
|
||||
|
||||
|
|
@ -73,28 +95,32 @@ class Client extends BaseClient {
|
|||
* Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
|
||||
* @type {?ShardClientUtil}
|
||||
*/
|
||||
this.shard = !browser && process.env.SHARDING_MANAGER ? 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 {UserStore<Snowflake, User>}
|
||||
* @type {UserManager}
|
||||
*/
|
||||
this.users = new UserStore(this);
|
||||
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 {GuildStore<Snowflake, Guild>}
|
||||
* @type {GuildManager}
|
||||
*/
|
||||
this.guilds = new GuildStore(this);
|
||||
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 the bot
|
||||
* is a member of, and all DM channels
|
||||
* @type {ChannelStore<Snowflake, Channel>}
|
||||
* 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 ChannelStore(this);
|
||||
this.channels = new ChannelManager(this);
|
||||
|
||||
const ClientPresence = Structures.get('ClientPresence');
|
||||
/**
|
||||
|
|
@ -129,79 +155,20 @@ class Client extends BaseClient {
|
|||
*/
|
||||
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 = [];
|
||||
|
||||
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}
|
||||
* @readonly
|
||||
* @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 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (browser) return new Collection();
|
||||
return this.voice.connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* All custom emojis that the client has access to, mapped by their IDs
|
||||
* @type {GuildEmojiStore<Snowflake, GuildEmoji>}
|
||||
* @type {GuildEmojiManager}
|
||||
* @readonly
|
||||
*/
|
||||
get emojis() {
|
||||
const emojis = new GuildEmojiStore({ client: this });
|
||||
for (const guild of this.guilds.values()) {
|
||||
if (guild.available) 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;
|
||||
}
|
||||
|
|
@ -216,13 +183,12 @@ class Client extends BaseClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a voice broadcast.
|
||||
* @returns {VoiceBroadcast}
|
||||
* How long it has been since the client last entered the `READY` state in milliseconds
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
createVoiceBroadcast() {
|
||||
const broadcast = new VoiceBroadcast(this);
|
||||
this.broadcasts.push(broadcast);
|
||||
return broadcast;
|
||||
get uptime() {
|
||||
return this.readyAt ? Date.now() - this.readyAt : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -232,15 +198,30 @@ class Client extends BaseClient {
|
|||
* @example
|
||||
* client.login('my token');
|
||||
*/
|
||||
login(token = this.token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
|
||||
token = token.replace(/^Bot\s*/i, '');
|
||||
this.manager.connectToWebSocket(token, resolve, reject);
|
||||
}).catch(e => {
|
||||
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();
|
||||
return Promise.reject(e);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,7 +230,8 @@ class Client extends BaseClient {
|
|||
*/
|
||||
destroy() {
|
||||
super.destroy();
|
||||
return this.manager.destroy();
|
||||
this.ws.destroy();
|
||||
this.token = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -263,7 +245,9 @@ class Client extends BaseClient {
|
|||
*/
|
||||
fetchInvite(invite) {
|
||||
const code = DataResolver.resolveInviteCode(invite);
|
||||
return this.api.invites(code).get({ query: { with_counts: true } })
|
||||
return this.api
|
||||
.invites(code)
|
||||
.get({ query: { with_counts: true } })
|
||||
.then(data => new Invite(this, data));
|
||||
}
|
||||
|
||||
|
|
@ -278,12 +262,15 @@ class Client extends BaseClient {
|
|||
* .catch(console.error);
|
||||
*/
|
||||
fetchWebhook(id, token) {
|
||||
return this.api.webhooks(id, token).get().then(data => new Webhook(this, data));
|
||||
return this.api
|
||||
.webhooks(id, token)
|
||||
.get()
|
||||
.then(data => new Webhook(this, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the available voice regions from Discord.
|
||||
* @returns {Collection<string, VoiceRegion>}
|
||||
* @returns {Promise<Collection<string, VoiceRegion>>}
|
||||
* @example
|
||||
* client.fetchVoiceRegions()
|
||||
* .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`))
|
||||
|
|
@ -311,10 +298,10 @@ class Client extends BaseClient {
|
|||
*/
|
||||
sweepMessages(lifetime = this.options.messageCacheLifetime) {
|
||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'Lifetime', 'a number');
|
||||
throw new TypeError('INVALID_TYPE', 'lifetime', 'number');
|
||||
}
|
||||
if (lifetime <= 0) {
|
||||
this.emit(Events.DEBUG, 'Didn\'t sweep messages - lifetime is unlimited');
|
||||
this.emit(Events.DEBUG, "Didn't sweep messages - lifetime is unlimited");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -323,17 +310,19 @@ class Client extends BaseClient {
|
|||
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(Events.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;
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +331,9 @@ class Client extends BaseClient {
|
|||
* @returns {Promise<ClientApplication>}
|
||||
*/
|
||||
fetchApplication() {
|
||||
return this.api.oauth2.applications('@me').get()
|
||||
return this.api.oauth2
|
||||
.applications('@me')
|
||||
.get()
|
||||
.then(app => new ClientApplication(this, app));
|
||||
}
|
||||
|
||||
|
|
@ -355,33 +346,24 @@ class Client extends BaseClient {
|
|||
* .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}`;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return super.toJSON({
|
||||
readyAt: false,
|
||||
broadcasts: false,
|
||||
pings: false,
|
||||
presences: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
|
||||
* with the client as `this`.
|
||||
|
|
@ -398,18 +380,14 @@ class Client extends BaseClient {
|
|||
* @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('CLIENT_INVALID_OPTION', 'shardCount', 'a number');
|
||||
_validateOptions(options = this.options) {
|
||||
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 (typeof options.shardId !== 'number' || isNaN(options.shardId)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'shardId', 'a number');
|
||||
}
|
||||
if (options.shardCount < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 0');
|
||||
if (options.shardId < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'at least 0');
|
||||
if (options.shardId !== 0 && options.shardId >= options.shardCount) {
|
||||
throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', '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('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number');
|
||||
}
|
||||
|
|
@ -422,19 +400,22 @@ class Client extends BaseClient {
|
|||
if (typeof options.fetchAllMembers !== 'boolean') {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean');
|
||||
}
|
||||
if (typeof options.disableEveryone !== 'boolean') {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', '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('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 (typeof options.internalSharding !== 'boolean') {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'internalSharding', 'a boolean');
|
||||
}
|
||||
if (!(options.disabledEvents instanceof Array)) {
|
||||
if (!Array.isArray(options.disabledEvents)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array');
|
||||
}
|
||||
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
const { Events, Status } = require('../util/Constants');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @readonly
|
||||
* @type {number}
|
||||
*/
|
||||
get status() {
|
||||
return this.connection ? this.connection.status : 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(Events.DEBUG, `Authenticated using token ${token}`);
|
||||
this.client.token = token;
|
||||
const timeout = this.client.setTimeout(() => reject(new Error('WS_CONNECTION_TIMEOUT')), 1000 * 300);
|
||||
this.client.api.gateway.get().then(async res => {
|
||||
if (this.client.options.presence != null) { // eslint-disable-line eqeqeq
|
||||
const presence = await this.client.presence._parse(this.client.options.presence);
|
||||
this.client.options.ws.presence = presence;
|
||||
this.client.presence.patch(presence);
|
||||
}
|
||||
const gateway = `${res.url}/`;
|
||||
this.client.emit(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('TOKEN_INVALID'));
|
||||
if (event.code === 4010) reject(new Error('SHARDING_INVALID'));
|
||||
if (event.code === 4011) reject(new Error('SHARDING_REQUIRED'));
|
||||
});
|
||||
this.client.once(Events.READY, () => {
|
||||
resolve(token);
|
||||
this.client.clearTimeout(timeout);
|
||||
});
|
||||
}, reject);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.client.ws.destroy();
|
||||
if (this.client.user) this.client.token = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientManager;
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
const Webhook = require('../structures/Webhook');
|
||||
'use strict';
|
||||
|
||||
const BaseClient = require('./BaseClient');
|
||||
const Webhook = require('../structures/Webhook');
|
||||
|
||||
/**
|
||||
* The webhook client.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
class ActionsManager {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
|
@ -9,23 +11,30 @@ class ActionsManager {
|
|||
this.register(require('./MessageReactionAdd'));
|
||||
this.register(require('./MessageReactionRemove'));
|
||||
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('./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('./PresenceUpdate'));
|
||||
this.register(require('./UserUpdate'));
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ChannelCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const existing = client.channels.has(data.id);
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const DMChannel = require('../../structures/DMChannel');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ChannelDeleteAction extends Action {
|
||||
|
|
@ -9,11 +12,21 @@ class ChannelDeleteAction 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) {
|
||||
client.channels.remove(channel.id);
|
||||
channel.deleted = true;
|
||||
if (channel.messages && !(channel instanceof DMChannel)) {
|
||||
for (const message of channel.messages.cache.values()) {
|
||||
message.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);
|
||||
}
|
||||
|
||||
|
|
@ -21,10 +34,4 @@ class ChannelDeleteAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a channel is deleted.
|
||||
* @event Client#channelDelete
|
||||
* @param {GroupDMChannel|GuildChannel} channel The channel that was deleted
|
||||
*/
|
||||
|
||||
module.exports = ChannelDeleteAction;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const Channel = require('../../structures/Channel');
|
||||
const { ChannelTypes } = require('../../util/Constants');
|
||||
|
||||
class ChannelUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const channel = client.channels.get(data.id);
|
||||
let channel = client.channels.cache.get(data.id);
|
||||
if (channel) {
|
||||
const old = channel._update(data);
|
||||
|
||||
if (ChannelTypes[channel.type.toUpperCase()] !== data.type) {
|
||||
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.cache.set(channel.id, channel);
|
||||
}
|
||||
|
||||
return {
|
||||
old,
|
||||
updated: channel,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildBanRemove 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
'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);
|
||||
const channel = guild.channels.cache.get(partialChannel.id);
|
||||
if (channel) channel.rawPosition = partialChannel.position;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
|
|
@ -10,15 +12,21 @@ 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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
|
@ -28,13 +36,20 @@ class GuildDeleteAction extends Action {
|
|||
};
|
||||
}
|
||||
|
||||
for (const channel of guild.channels.values()) this.client.channels.remove(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.remove(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 {
|
||||
|
|
@ -49,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;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildEmojiCreateAction extends Action {
|
||||
handle(guild, createdEmoji) {
|
||||
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 {GuildEmoji} emoji The emoji that was created
|
||||
*/
|
||||
|
||||
module.exports = GuildEmojiCreateAction;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildEmojiDeleteAction extends Action {
|
||||
handle(emoji) {
|
||||
emoji.guild.emojis.remove(emoji.id);
|
||||
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 emoji is deleted in a guild.
|
||||
* @event Client#emojiDelete
|
||||
* @param {GuildEmoji} emoji The emoji that was deleted
|
||||
*/
|
||||
|
||||
module.exports = GuildEmojiDeleteAction;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildEmojiUpdateAction extends Action {
|
||||
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 emoji is updated in a guild.
|
||||
* @event Client#emojiUpdate
|
||||
* @param {GuildEmoji} oldEmoji The old emoji
|
||||
* @param {GuildEmoji} newEmoji The new emoji
|
||||
*/
|
||||
|
||||
module.exports = GuildEmojiUpdateAction;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
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 = new Map(guild.emojis);
|
||||
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)) {
|
||||
|
|
|
|||
19
src/client/actions/GuildIntegrationsUpdate.js
Normal file
19
src/client/actions/GuildIntegrationsUpdate.js
Normal 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;
|
||||
|
|
@ -1,29 +1,30 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events, Status } = require('../../util/Constants');
|
||||
|
||||
class GuildMemberRemoveAction extends Action {
|
||||
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.voiceStates.delete(member.id);
|
||||
member.deleted = true;
|
||||
guild.members.remove(member.id);
|
||||
if (client.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member);
|
||||
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);
|
||||
}
|
||||
guild.voiceStates.cache.delete(data.user.id);
|
||||
}
|
||||
return { guild, member };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
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);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildRoleDeleteAction 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) {
|
||||
role = guild.roles.get(data.role_id);
|
||||
role = guild.roles.cache.get(data.role_id);
|
||||
if (role) {
|
||||
guild.roles.remove(data.role_id);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,10 +27,4 @@ class GuildRoleDeleteAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a guild role is deleted.
|
||||
* @event Client#roleDelete
|
||||
* @param {Role} role The role that was deleted
|
||||
*/
|
||||
|
||||
module.exports = GuildRoleDeleteAction;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
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) {
|
||||
let old = null;
|
||||
|
||||
const role = guild.roles.get(data.role.id);
|
||||
const role = guild.roles.cache.get(data.role.id);
|
||||
if (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);
|
||||
}
|
||||
|
||||
|
|
@ -28,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;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
'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);
|
||||
const role = guild.roles.cache.get(partialRole.id);
|
||||
if (role) role.rawPosition = partialRole.position;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
|
|
@ -5,9 +7,15 @@ 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 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,
|
||||
|
|
@ -22,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;
|
||||
|
|
|
|||
28
src/client/actions/InviteCreate.js
Normal file
28
src/client/actions/InviteCreate.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const Invite = require('../../structures/Invite');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class InviteCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const 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 };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InviteCreateAction;
|
||||
29
src/client/actions/InviteDelete.js
Normal file
29
src/client/actions/InviteDelete.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const Invite = require('../../structures/Invite');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class InviteDeleteAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const 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 };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InviteDeleteAction;
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class MessageCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = client.channels.get(data.channel_id);
|
||||
const channel = client.channels.cache.get(data.channel_id);
|
||||
if (channel) {
|
||||
const existing = channel.messages.get(data.id);
|
||||
const existing = channel.messages.cache.get(data.id);
|
||||
if (existing) return { message: existing };
|
||||
const message = channel.messages.add(data);
|
||||
const user = message.author;
|
||||
const member = channel.guild ? channel.guild.member(user) : null;
|
||||
let member = message.member;
|
||||
channel.lastMessageID = data.id;
|
||||
if (user) {
|
||||
user.lastMessageID = data.id;
|
||||
|
|
@ -21,6 +23,11 @@ class MessageCreateAction extends Action {
|
|||
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 };
|
||||
}
|
||||
|
|
@ -29,10 +36,4 @@ class MessageCreateAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a message is created.
|
||||
* @event Client#message
|
||||
* @param {Message} message The created message
|
||||
*/
|
||||
|
||||
module.exports = MessageCreateAction;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class MessageDeleteAction extends Action {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,10 +26,4 @@ class MessageDeleteAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a message is deleted.
|
||||
* @event Client#messageDelete
|
||||
* @param {Message} message The deleted message
|
||||
*/
|
||||
|
||||
module.exports = MessageDeleteAction;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const Collection = require('../../util/Collection');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
|
@ -5,20 +7,32 @@ const { Events } = require('../../util/Constants');
|
|||
class MessageDeleteBulkAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = client.channels.get(data.channel_id);
|
||||
const channel = client.channels.cache.get(data.channel_id);
|
||||
|
||||
if (channel) {
|
||||
const ids = data.ids;
|
||||
const messages = new Collection();
|
||||
for (const id of ids) {
|
||||
const message = channel.messages.get(id);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
|
@ -26,10 +40,4 @@ class MessageDeleteBulkAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever messages are deleted in bulk.
|
||||
* @event Client#messageDeleteBulk
|
||||
* @param {Collection<Snowflake, Message>} messages The deleted messages, mapped by their ID
|
||||
*/
|
||||
|
||||
module.exports = MessageDeleteBulkAction;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const { PartialTypes } = require('../../util/Constants');
|
||||
|
||||
/*
|
||||
{ user_id: 'id',
|
||||
|
|
@ -9,31 +13,38 @@ const Action = require('./Action');
|
|||
|
||||
class MessageReactionAdd extends Action {
|
||||
handle(data) {
|
||||
const user = data.user || this.client.users.get(data.user_id);
|
||||
if (!user) return false;
|
||||
// Verify channel
|
||||
const channel = data.channel || this.client.channels.get(data.channel_id);
|
||||
if (!channel || channel.type === 'voice') return false;
|
||||
// Verify message
|
||||
const message = data.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
|
||||
if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false;
|
||||
const reaction = message.reactions.add({
|
||||
emoji: data.emoji,
|
||||
count: 0,
|
||||
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 guild or reaction emoji
|
||||
*/
|
||||
|
||||
module.exports = MessageReactionAdd;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
|
|
@ -10,31 +12,33 @@ const { Events } = 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 emojiID = data.emoji.id || decodeURIComponent(data.emoji.name);
|
||||
const reaction = message.reactions.get(emojiID);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
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.reactions.clear();
|
||||
message.reactions.cache.clear();
|
||||
this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message);
|
||||
|
||||
return { message };
|
||||
|
|
|
|||
28
src/client/actions/MessageReactionRemoveEmoji.js
Normal file
28
src/client/actions/MessageReactionRemoveEmoji.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class MessageReactionRemoveEmoji extends Action {
|
||||
handle(data) {
|
||||
const channel = this.getChannel(data);
|
||||
if (!channel || channel.type === 'voice') return false;
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageReactionRemoveEmoji;
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
|
||||
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);
|
||||
return {
|
||||
|
|
|
|||
44
src/client/actions/PresenceUpdate.js
Normal file
44
src/client/actions/PresenceUpdate.js
Normal 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;
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
|
|
@ -5,19 +7,20 @@ 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 = client.user._update(data);
|
||||
client.emit(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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
44
src/client/actions/VoiceStateUpdate.js
Normal file
44
src/client/actions/VoiceStateUpdate.js
Normal 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;
|
||||
19
src/client/actions/WebhooksUpdate.js
Normal file
19
src/client/actions/WebhooksUpdate.js
Normal 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;
|
||||
|
|
@ -1,19 +1,22 @@
|
|||
const Collection = require('../../util/Collection');
|
||||
const { VoiceStatus } = require('../../util/Constants');
|
||||
'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
|
||||
|
|
@ -21,23 +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.status !== VoiceStatus.DISCONNECTED) {
|
||||
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,6 +64,7 @@ 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) => {
|
||||
|
|
@ -62,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);
|
||||
}
|
||||
|
||||
|
|
@ -70,12 +94,13 @@ class ClientVoiceManager {
|
|||
reject(reason);
|
||||
});
|
||||
|
||||
connection.on('error', reject);
|
||||
|
||||
connection.once('authenticated', () => {
|
||||
connection.once('ready', () => {
|
||||
resolve(connection);
|
||||
connection.removeListener('error', reject);
|
||||
});
|
||||
connection.on('error', reject);
|
||||
connection.once('disconnect', () => this.connections.delete(channel.guild.id));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer');
|
||||
const DispatcherSet = require('./util/DispatcherSet');
|
||||
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();
|
||||
* 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()) {
|
||||
* for (const connection of client.voice.connections.values()) {
|
||||
* connection.play(broadcast);
|
||||
* }
|
||||
* ```
|
||||
|
|
@ -25,13 +27,18 @@ class VoiceBroadcast extends EventEmitter {
|
|||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this.dispatchers = new DispatcherSet(this);
|
||||
/**
|
||||
* The subscribed StreamDispatchers of this broadcast
|
||||
* @type {StreamDispatcher[]}
|
||||
*/
|
||||
this.subscribers = [];
|
||||
this.player = new BroadcastAudioPlayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current master dispatcher, if any. This dispatcher controls all that is played by subscribed dispatchers.
|
||||
* @type {?BroadcastDispatcher}
|
||||
* @readonly
|
||||
*/
|
||||
get dispatcher() {
|
||||
return this.player.dispatcher;
|
||||
|
|
@ -52,7 +59,50 @@ class VoiceBroadcast extends EventEmitter {
|
|||
* broadcast.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
|
||||
* @returns {BroadcastDispatcher}
|
||||
*/
|
||||
play() { return null; }
|
||||
play() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
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} subscriber The subscribed dispatcher
|
||||
*/
|
||||
this.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
delete(dispatcher) {
|
||||
const index = this.subscribers.indexOf(dispatcher);
|
||||
if (index !== -1) {
|
||||
this.subscribers.splice(index, 1);
|
||||
dispatcher.destroy();
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher unsubscribes to the broadcast.
|
||||
* @event VoiceBroadcast#unsubscribe
|
||||
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
|
||||
*/
|
||||
this.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PlayInterface.applyToClass(VoiceBroadcast);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,26 @@
|
|||
const VoiceWebSocket = require('./networking/VoiceWebSocket');
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const VoiceUDP = require('./networking/VoiceUDPClient');
|
||||
const Util = require('../../util/Util');
|
||||
const { OPCodes, VoiceOPCodes, VoiceStatus, Events } = require('../../util/Constants');
|
||||
const VoiceWebSocket = require('./networking/VoiceWebSocket');
|
||||
const AudioPlayer = require('./player/AudioPlayer');
|
||||
const VoiceReceiver = require('./receiver/Receiver');
|
||||
const EventEmitter = require('events');
|
||||
const { Error } = require('../../errors');
|
||||
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');
|
||||
|
||||
const SUPPORTED_MODES = [
|
||||
'xsalsa20_poly1305_lite',
|
||||
'xsalsa20_poly1305_suffix',
|
||||
'xsalsa20_poly1305',
|
||||
];
|
||||
// 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.
|
||||
|
|
@ -113,14 +120,13 @@ class VoiceConnection extends EventEmitter {
|
|||
* The voice receiver of this connection
|
||||
* @type {VoiceReceiver}
|
||||
*/
|
||||
this.receiver = null;
|
||||
|
||||
this.authenticate();
|
||||
this.receiver = new VoiceReceiver(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* The client that instantiated this connection
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
*/
|
||||
get client() {
|
||||
return this.voiceManager.client;
|
||||
|
|
@ -144,35 +150,54 @@ class VoiceConnection extends EventEmitter {
|
|||
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);
|
||||
});
|
||||
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: 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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,6 +208,7 @@ class VoiceConnection extends EventEmitter {
|
|||
* @returns {void}
|
||||
*/
|
||||
setTokenAndEndpoint(token, endpoint) {
|
||||
this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`);
|
||||
if (!endpoint) {
|
||||
// Signifies awaiting endpoint stage
|
||||
return;
|
||||
|
|
@ -194,6 +220,7 @@ class VoiceConnection extends EventEmitter {
|
|||
}
|
||||
|
||||
endpoint = endpoint.match(/([^:]*)/)[0];
|
||||
this.emit('debug', `Endpoint resolved as ${endpoint}`);
|
||||
|
||||
if (!endpoint) {
|
||||
this.authenticateFailed('VOICE_INVALID_ENDPOINT');
|
||||
|
|
@ -215,6 +242,7 @@ class VoiceConnection extends EventEmitter {
|
|||
* @private
|
||||
*/
|
||||
setSessionID(sessionID) {
|
||||
this.emit('debug', `Setting sessionID ${sessionID} (stored as "${this.authentication.sessionID}")`);
|
||||
if (!sessionID) {
|
||||
this.authenticateFailed('VOICE_SESSION_ABSENT');
|
||||
return;
|
||||
|
|
@ -240,7 +268,7 @@ class VoiceConnection extends EventEmitter {
|
|||
*/
|
||||
checkAuthenticated() {
|
||||
const { token, endpoint, sessionID } = this.authentication;
|
||||
|
||||
this.emit('debug', `Authenticated with sessionID ${sessionID}`);
|
||||
if (token && endpoint && sessionID) {
|
||||
this.status = VoiceStatus.CONNECTING;
|
||||
/**
|
||||
|
|
@ -258,7 +286,8 @@ class VoiceConnection extends EventEmitter {
|
|||
* @private
|
||||
*/
|
||||
authenticateFailed(reason) {
|
||||
clearTimeout(this.connectTimeout);
|
||||
this.client.clearTimeout(this.connectTimeout);
|
||||
this.emit('debug', `Authenticate failed - ${reason}`);
|
||||
if (this.status === VoiceStatus.AUTHENTICATING) {
|
||||
/**
|
||||
* Emitted when we fail to initiate a voice connection.
|
||||
|
|
@ -293,8 +322,7 @@ class VoiceConnection extends EventEmitter {
|
|||
*/
|
||||
authenticate() {
|
||||
this.sendVoiceStateUpdate();
|
||||
this.connectTimeout = this.client.setTimeout(
|
||||
() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15000);
|
||||
this.connectTimeout = this.client.setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -308,6 +336,7 @@ class VoiceConnection extends EventEmitter {
|
|||
this.authentication.endpoint = endpoint;
|
||||
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
|
||||
|
|
@ -321,7 +350,8 @@ class VoiceConnection extends EventEmitter {
|
|||
*/
|
||||
disconnect() {
|
||||
this.emit('closing');
|
||||
clearTimeout(this.connectTimeout);
|
||||
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({
|
||||
|
|
@ -344,7 +374,6 @@ class VoiceConnection extends EventEmitter {
|
|||
this.emit('disconnect');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cleans up after disconnect.
|
||||
* @private
|
||||
|
|
@ -354,11 +383,14 @@ class VoiceConnection extends EventEmitter {
|
|||
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('speaking');
|
||||
ws.shutdown();
|
||||
}
|
||||
|
||||
if (udp) udp.removeAllListeners('error');
|
||||
|
|
@ -372,6 +404,7 @@ class VoiceConnection extends EventEmitter {
|
|||
* @private
|
||||
*/
|
||||
connect() {
|
||||
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');
|
||||
|
|
@ -385,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('speaking', this.onSpeaking.bind(this));
|
||||
ws.on('startSpeaking', this.onStartSpeaking.bind(this));
|
||||
|
||||
this.sockets.ws.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -398,7 +435,7 @@ class VoiceConnection extends EventEmitter {
|
|||
* @private
|
||||
*/
|
||||
onReady(data) {
|
||||
this.authentication = data;
|
||||
Object.assign(this.authentication, data);
|
||||
for (let mode of data.modes) {
|
||||
if (SUPPORTED_MODES.includes(mode)) {
|
||||
this.authentication.mode = mode;
|
||||
|
|
@ -417,14 +454,27 @@ class VoiceConnection extends EventEmitter {
|
|||
onSessionDescription(data) {
|
||||
Object.assign(this.authentication, data);
|
||||
this.status = VoiceStatus.CONNECTED;
|
||||
clearTimeout(this.connectTimeout);
|
||||
this.receiver = new VoiceReceiver(this);
|
||||
/**
|
||||
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
|
||||
* the connection will already be ready.
|
||||
* @event VoiceConnection#ready
|
||||
*/
|
||||
this.emit('ready');
|
||||
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.
|
||||
* @event VoiceConnection#ready
|
||||
*/
|
||||
this.emit('ready');
|
||||
};
|
||||
if (this.dispatcher) {
|
||||
ready();
|
||||
} else {
|
||||
// This serves to provide support for voice receive, sending audio is required to receive it.
|
||||
const dispatcher = this.play(new SingleSilence(), { type: 'opus' });
|
||||
dispatcher.once('finish', ready);
|
||||
}
|
||||
}
|
||||
|
||||
onStartSpeaking({ user_id, ssrc, speaking }) {
|
||||
this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -432,11 +482,10 @@ class VoiceConnection extends EventEmitter {
|
|||
* @param {Object} data The received data
|
||||
* @private
|
||||
*/
|
||||
onSpeaking({ user_id, ssrc, speaking }) {
|
||||
onSpeaking({ user_id, speaking }) {
|
||||
speaking = new Speaking(speaking).freeze();
|
||||
const guild = this.channel.guild;
|
||||
const user = this.client.users.get(user_id);
|
||||
this.ssrcMap.set(+ssrc, user_id);
|
||||
const user = this.client.users.cache.get(user_id);
|
||||
const old = this._speaking.get(user_id);
|
||||
this._speaking.set(user_id, speaking);
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const StreamDispatcher = require('./StreamDispatcher');
|
||||
|
||||
/**
|
||||
|
|
@ -13,7 +15,7 @@ class BroadcastDispatcher extends StreamDispatcher {
|
|||
|
||||
_write(chunk, enc, done) {
|
||||
if (!this.startTime) this.startTime = Date.now();
|
||||
for (const dispatcher of this.broadcast.dispatchers) {
|
||||
for (const dispatcher of this.broadcast.subscribers) {
|
||||
dispatcher._write(chunk, enc);
|
||||
}
|
||||
this._step(done);
|
||||
|
|
@ -27,6 +29,12 @@ class BroadcastDispatcher extends StreamDispatcher {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
const VolumeInterface = require('../util/VolumeInterface');
|
||||
const { Writable } = require('stream');
|
||||
'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 MAX_NONCE_SIZE = 2 ** 32 - 1;
|
||||
const nonce = Buffer.alloc(24);
|
||||
|
||||
/**
|
||||
|
|
@ -29,11 +30,8 @@ const nonce = Buffer.alloc(24);
|
|||
* @extends {WritableStream}
|
||||
*/
|
||||
class StreamDispatcher extends Writable {
|
||||
constructor(
|
||||
player,
|
||||
{ seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {},
|
||||
streams) {
|
||||
const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark };
|
||||
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
|
||||
|
|
@ -65,15 +63,14 @@ class StreamDispatcher extends Writable {
|
|||
this.count = 0;
|
||||
|
||||
this.on('finish', () => {
|
||||
// Still emitting end for backwards compatibility, probably remove it in the future!
|
||||
this.emit('end');
|
||||
this._cleanup();
|
||||
this._setSpeaking(0);
|
||||
});
|
||||
|
||||
if (typeof volume !== 'undefined') this.setVolume(volume);
|
||||
this.setVolume(volume);
|
||||
this.setBitrate(bitrate);
|
||||
if (typeof fec !== 'undefined') this.setFEC(fec);
|
||||
if (typeof plp !== 'undefined') this.setPLP(plp);
|
||||
if (typeof bitrate !== 'undefined') this.setBitrate(bitrate);
|
||||
|
||||
const streamError = (type, err) => {
|
||||
/**
|
||||
|
|
@ -112,12 +109,16 @@ class StreamDispatcher extends Writable {
|
|||
}
|
||||
|
||||
_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.dispatchers.delete(this);
|
||||
if (streams.opus) streams.opus.unpipe(this);
|
||||
if (streams.broadcast) streams.broadcast.delete(this);
|
||||
if (streams.opus) streams.opus.destroy();
|
||||
if (streams.ffmpeg) streams.ffmpeg.destroy();
|
||||
super._destroy(err, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -139,12 +140,16 @@ class StreamDispatcher extends Writable {
|
|||
/**
|
||||
* Whether or not playback is paused
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get paused() { return Boolean(this.pausedSince); }
|
||||
get paused() {
|
||||
return Boolean(this.pausedSince);
|
||||
}
|
||||
|
||||
/**
|
||||
* Total time that this dispatcher has been paused
|
||||
* 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);
|
||||
|
|
@ -170,6 +175,7 @@ class StreamDispatcher extends Writable {
|
|||
/**
|
||||
* The time (in milliseconds) that the dispatcher has actually been playing audio for
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get streamTime() {
|
||||
return this.count * FRAME_LENGTH;
|
||||
|
|
@ -178,6 +184,7 @@ class StreamDispatcher extends Writable {
|
|||
/**
|
||||
* The time (in milliseconds) that the dispatcher has been playing audio for, taking into account skips and pauses
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get totalStreamTime() {
|
||||
return Date.now() - this.startTime;
|
||||
|
|
@ -224,7 +231,7 @@ class StreamDispatcher extends Writable {
|
|||
done();
|
||||
};
|
||||
if (!this.streams.broadcast) {
|
||||
const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this._pausedTime);
|
||||
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);
|
||||
|
|
@ -252,10 +259,7 @@ class StreamDispatcher extends Writable {
|
|||
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),
|
||||
];
|
||||
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];
|
||||
|
|
@ -278,24 +282,20 @@ class StreamDispatcher extends Writable {
|
|||
}
|
||||
|
||||
_sendPacket(packet) {
|
||||
let repeats = this.streamOptions.passes;
|
||||
/**
|
||||
* Emitted whenever the dispatcher has debug information.
|
||||
* @event StreamDispatcher#debug
|
||||
* @param {string} info The debug info
|
||||
*/
|
||||
this._setSpeaking(1);
|
||||
while (repeats--) {
|
||||
if (!this.player.voiceConnection.sockets.udp) {
|
||||
this.emit('debug', 'Failed to send a packet - no UDP socket');
|
||||
return;
|
||||
}
|
||||
this.player.voiceConnection.sockets.udp.send(packet)
|
||||
.catch(e => {
|
||||
this._setSpeaking(0);
|
||||
this.emit('debug', `Failed to send a packet - ${e}`);
|
||||
});
|
||||
if (!this.player.voiceConnection.sockets.udp) {
|
||||
this.emit('debug', 'Failed to send a packet - no UDP socket');
|
||||
return;
|
||||
}
|
||||
this.player.voiceConnection.sockets.udp.send(packet).catch(e => {
|
||||
this._setSpeaking(0);
|
||||
this.emit('debug', `Failed to send a packet - ${e}`);
|
||||
});
|
||||
}
|
||||
|
||||
_setSpeaking(value) {
|
||||
|
|
@ -310,13 +310,18 @@ class StreamDispatcher extends Writable {
|
|||
this.emit('speaking', value);
|
||||
}
|
||||
|
||||
get volumeEditable() { return Boolean(this.streams.volume); }
|
||||
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; }
|
||||
get bitrateEditable() {
|
||||
return this.streams.opus && this.streams.opus.setBitrate;
|
||||
}
|
||||
|
||||
// Volume
|
||||
get volume() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const udp = require('dgram');
|
||||
const { VoiceOPCodes } = require('../../../util/Constants');
|
||||
const EventEmitter = require('events');
|
||||
const { Error } = require('../../../errors');
|
||||
const { VoiceOPCodes } = require('../../../util/Constants');
|
||||
|
||||
/**
|
||||
* Represents a UDP client for a Voice Connection.
|
||||
|
|
@ -46,6 +48,7 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
|||
}
|
||||
|
||||
shutdown() {
|
||||
this.emit('debug', `[UDP] shutdown requested`);
|
||||
if (this.socket) {
|
||||
this.socket.removeAllListeners('message');
|
||||
try {
|
||||
|
|
@ -75,21 +78,35 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
|||
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;
|
||||
}
|
||||
|
|
@ -108,11 +125,17 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
const { OPCodes, VoiceOPCodes } = require('../../../util/Constants');
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { Error } = require('../../../errors');
|
||||
const WebSocket = require('../../../WebSocket');
|
||||
const { Error } = require('../../../errors');
|
||||
const { OPCodes, VoiceOPCodes } = require('../../../util/Constants');
|
||||
|
||||
/**
|
||||
* Represents a Voice Connection's WebSocket.
|
||||
|
|
@ -23,7 +25,6 @@ class VoiceWebSocket extends EventEmitter {
|
|||
*/
|
||||
this.attempts = 0;
|
||||
|
||||
this.connect();
|
||||
this.dead = false;
|
||||
this.connection.on('closing', this.shutdown.bind(this));
|
||||
}
|
||||
|
|
@ -31,12 +32,14 @@ class VoiceWebSocket extends EventEmitter {
|
|||
/**
|
||||
* The client of this voice WebSocket
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
*/
|
||||
get client() {
|
||||
return this.connection.voiceManager.client;
|
||||
return this.connection.client;
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
this.emit('debug', `[WS] shutdown requested`);
|
||||
this.dead = true;
|
||||
this.reset();
|
||||
}
|
||||
|
|
@ -45,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;
|
||||
|
|
@ -56,6 +60,7 @@ 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) {
|
||||
|
|
@ -70,6 +75,7 @@ class VoiceWebSocket extends EventEmitter {
|
|||
* @type {WebSocket}
|
||||
*/
|
||||
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);
|
||||
|
|
@ -82,10 +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('WS_NOT_OPEN', data);
|
||||
this.ws.send(data, null, error => {
|
||||
if (error) reject(error); else resolve(data);
|
||||
if (error) reject(error);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -108,6 +116,7 @@ class VoiceWebSocket extends EventEmitter {
|
|||
* Called whenever the WebSocket opens.
|
||||
*/
|
||||
onOpen() {
|
||||
this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`);
|
||||
this.sendPacket({
|
||||
op: OPCodes.DISPATCH,
|
||||
d: {
|
||||
|
|
@ -128,7 +137,7 @@ class VoiceWebSocket extends EventEmitter {
|
|||
*/
|
||||
onMessage(event) {
|
||||
try {
|
||||
return this.onPacket(WebSocket.unpack(event.data));
|
||||
return this.onPacket(WebSocket.unpack(event.data, 'json'));
|
||||
} catch (error) {
|
||||
return this.onError(error);
|
||||
}
|
||||
|
|
@ -138,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);
|
||||
}
|
||||
|
||||
|
|
@ -146,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);
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +165,7 @@ class VoiceWebSocket extends EventEmitter {
|
|||
* @param {Object} packet The received packet
|
||||
*/
|
||||
onPacket(packet) {
|
||||
this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
|
||||
switch (packet.op) {
|
||||
case VoiceOPCodes.HELLO:
|
||||
this.setHeartbeat(packet.d.heartbeat_interval);
|
||||
|
|
@ -190,9 +202,9 @@ class VoiceWebSocket extends EventEmitter {
|
|||
/**
|
||||
* Emitted whenever a speaking packet is received.
|
||||
* @param {Object} data
|
||||
* @event VoiceWebSocket#speaking
|
||||
* @event VoiceWebSocket#startSpeaking
|
||||
*/
|
||||
this.emit('speaking', packet.d);
|
||||
this.emit('startSpeaking', packet.d);
|
||||
break;
|
||||
default:
|
||||
/**
|
||||
|
|
@ -221,7 +233,7 @@ class VoiceWebSocket extends EventEmitter {
|
|||
* @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);
|
||||
}
|
||||
|
|
@ -234,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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const BasePlayer = require('./BasePlayer');
|
||||
|
||||
/**
|
||||
|
|
@ -17,7 +19,7 @@ class AudioPlayer extends BasePlayer {
|
|||
|
||||
playBroadcast(broadcast, options) {
|
||||
const dispatcher = this.createDispatcher(options, { broadcast });
|
||||
broadcast.dispatchers.add(dispatcher);
|
||||
broadcast.add(dispatcher);
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
const EventEmitter = require('events').EventEmitter;
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { Readable: ReadableStream } = require('stream');
|
||||
const prism = require('prism-media');
|
||||
const StreamDispatcher = require('../dispatcher/StreamDispatcher');
|
||||
|
||||
const FFMPEG_ARGUMENTS = [
|
||||
'-analyzeduration', '0',
|
||||
'-loglevel', '0',
|
||||
'-f', 's16le',
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
];
|
||||
const FFMPEG_ARGUMENTS = ['-analyzeduration', '0', '-loglevel', '0', '-f', 's16le', '-ar', '48000', '-ac', '2'];
|
||||
|
||||
/**
|
||||
* An Audio Player for a Voice Connection.
|
||||
|
|
@ -46,7 +42,7 @@ class BasePlayer extends EventEmitter {
|
|||
const isStream = input instanceof ReadableStream;
|
||||
|
||||
const args = isStream ? FFMPEG_ARGUMENTS.slice() : ['-i', input, ...FFMPEG_ARGUMENTS];
|
||||
if (options.seek) args.push('-ss', String(options.seek));
|
||||
if (options.seek) args.unshift('-ss', String(options.seek));
|
||||
|
||||
const ffmpeg = new prism.FFmpeg({ args });
|
||||
const streams = { ffmpeg };
|
||||
|
|
@ -59,13 +55,13 @@ class BasePlayer extends EventEmitter {
|
|||
|
||||
playPCMStream(stream, options, streams = {}) {
|
||||
this.destroyDispatcher();
|
||||
const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 });
|
||||
const opus = (streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }));
|
||||
if (options && options.volume === false) {
|
||||
stream.pipe(opus);
|
||||
return this.playOpusStream(opus, options, streams);
|
||||
}
|
||||
const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 });
|
||||
stream.pipe(volume).pipe(opus);
|
||||
streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 });
|
||||
stream.pipe(streams.volume).pipe(opus);
|
||||
return this.playOpusStream(opus, options, streams);
|
||||
}
|
||||
|
||||
|
|
@ -75,10 +71,10 @@ class BasePlayer extends EventEmitter {
|
|||
if (options.volume !== false && !streams.input) {
|
||||
streams.input = stream;
|
||||
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 });
|
||||
const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 });
|
||||
streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 });
|
||||
streams.opus = stream
|
||||
.pipe(decoder)
|
||||
.pipe(volume)
|
||||
.pipe(streams.volume)
|
||||
.pipe(new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }));
|
||||
}
|
||||
const dispatcher = this.createDispatcher(options, streams);
|
||||
|
|
@ -88,7 +84,7 @@ class BasePlayer extends EventEmitter {
|
|||
|
||||
createDispatcher(options, streams, broadcast) {
|
||||
this.destroyDispatcher();
|
||||
const dispatcher = this.dispatcher = new StreamDispatcher(this, options, streams, broadcast);
|
||||
const dispatcher = (this.dispatcher = new StreamDispatcher(this, options, streams, broadcast));
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher');
|
||||
'use strict';
|
||||
|
||||
const BasePlayer = require('./BasePlayer');
|
||||
const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher');
|
||||
|
||||
/**
|
||||
* An Audio Player for a Voice Connection.
|
||||
|
|
@ -18,7 +20,7 @@ class AudioPlayer extends BasePlayer {
|
|||
|
||||
createDispatcher(options, streams) {
|
||||
this.destroyDispatcher();
|
||||
const dispatcher = this.dispatcher = new BroadcastDispatcher(this, options, streams);
|
||||
const dispatcher = (this.dispatcher = new BroadcastDispatcher(this, options, streams));
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
const secretbox = require('../util/Secretbox');
|
||||
const EventEmitter = require('events');
|
||||
'use strict';
|
||||
|
||||
class Readable extends require('stream').Readable { _read() {} } // eslint-disable-line no-empty-function
|
||||
const EventEmitter = require('events');
|
||||
const secretbox = require('../util/Secretbox');
|
||||
|
||||
// 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;
|
||||
|
||||
class Readable extends require('stream').Readable {
|
||||
_read() {} // eslint-disable-line no-empty-function
|
||||
}
|
||||
|
||||
class PacketHandler extends EventEmitter {
|
||||
constructor(receiver) {
|
||||
|
|
@ -9,6 +17,7 @@ class PacketHandler extends EventEmitter {
|
|||
this.nonce = Buffer.alloc(24);
|
||||
this.receiver = receiver;
|
||||
this.streams = new Map();
|
||||
this.speakingTimeouts = new Map();
|
||||
}
|
||||
|
||||
get connection() {
|
||||
|
|
@ -52,7 +61,7 @@ class PacketHandler extends EventEmitter {
|
|||
packet = Buffer.from(packet);
|
||||
|
||||
// Strip RTP Header Extensions (one-byte only)
|
||||
if (packet[0] === 0xBE && packet[1] === 0xDE && packet.length > 4) {
|
||||
if (packet[0] === 0xbe && packet[1] === 0xde && packet.length > 4) {
|
||||
const headerExtensionLength = packet.readUInt16BE(2);
|
||||
let offset = 4;
|
||||
for (let i = 0; i < headerExtensionLength; i++) {
|
||||
|
|
@ -61,20 +70,38 @@ class PacketHandler extends EventEmitter {
|
|||
if (byte === 0) continue;
|
||||
offset += 1 + (0b1111 & (byte >> 4));
|
||||
}
|
||||
while (packet[offset] === 0) offset++;
|
||||
// Skip over undocumented Discord byte
|
||||
offset++;
|
||||
|
||||
packet = packet.slice(offset);
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
userFromSSRC(ssrc) { return this.connection.client.users.get(this.connection.ssrcMap.get(ssrc)); }
|
||||
|
||||
push(buffer) {
|
||||
const ssrc = buffer.readUInt32BE(8);
|
||||
const user = this.userFromSSRC(ssrc);
|
||||
if (!user) return;
|
||||
let stream = this.streams.get(user.id);
|
||||
const userStat = this.connection.ssrcMap.get(ssrc);
|
||||
if (!userStat) return;
|
||||
|
||||
let speakingTimeout = this.speakingTimeouts.get(ssrc);
|
||||
if (typeof speakingTimeout === 'undefined') {
|
||||
this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking });
|
||||
speakingTimeout = this.receiver.connection.client.setTimeout(() => {
|
||||
try {
|
||||
this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: 0 });
|
||||
this.receiver.connection.client.clearTimeout(speakingTimeout);
|
||||
this.speakingTimeouts.delete(ssrc);
|
||||
} catch {
|
||||
// Connection already closed, ignore
|
||||
}
|
||||
}, DISCORD_SPEAKING_DELAY);
|
||||
this.speakingTimeouts.set(ssrc, speakingTimeout);
|
||||
} else {
|
||||
speakingTimeout.refresh();
|
||||
}
|
||||
|
||||
let stream = this.streams.get(userStat.userID);
|
||||
if (!stream) return;
|
||||
stream = stream.stream;
|
||||
const opusPacket = this.parseBuffer(buffer);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const prism = require('prism-media');
|
||||
const PacketHandler = require('./PacketHandler');
|
||||
|
|
@ -21,7 +23,6 @@ class VoiceReceiver extends EventEmitter {
|
|||
* @param {Error|string} error The error or message to debug
|
||||
*/
|
||||
this.packets.on('error', err => this.emit('debug', err));
|
||||
this.connection.sockets.udp.socket.on('message', buffer => this.packets.push(buffer));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
/**
|
||||
* A "store" for handling broadcast dispatcher (un)subscription
|
||||
* @private
|
||||
*/
|
||||
class DispatcherSet extends Set {
|
||||
constructor(broadcast) {
|
||||
super();
|
||||
/**
|
||||
* The broadcast that this set belongs to
|
||||
* @type {VoiceBroadcast}
|
||||
*/
|
||||
this.broadcast = broadcast;
|
||||
}
|
||||
|
||||
add(dispatcher) {
|
||||
super.add(dispatcher);
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher subscribes to the broadcast.
|
||||
* @event VoiceBroadcast#subscribe
|
||||
* @param {StreamDispatcher} dispatcher The subscribed dispatcher
|
||||
*/
|
||||
this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(dispatcher) {
|
||||
const ret = super.delete(dispatcher);
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher unsubscribes to the broadcast.
|
||||
* @event VoiceBroadcast#unsubscribe
|
||||
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
|
||||
*/
|
||||
if (ret) this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DispatcherSet;
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const { Readable } = require('stream');
|
||||
const prism = require('prism-media');
|
||||
const { Error } = require('../../../errors');
|
||||
|
|
@ -9,7 +11,6 @@ const { Error } = require('../../../errors');
|
|||
* @property {number} [seek=0] The time to seek to, will be ignored when playing `ogg/opus` or `webm/opus` streams
|
||||
* @property {number|boolean} [volume=1] The volume to play at. Set this to false to disable volume transforms for
|
||||
* this stream to improve performance.
|
||||
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
|
||||
* @property {number} [plp] Expected packet loss percentage
|
||||
* @property {boolean} [fec] Enabled forward error correction
|
||||
* @property {number|string} [bitrate=96] The bitrate (quality) of the audio in kbps.
|
||||
|
|
@ -73,10 +74,10 @@ class PlayInterface {
|
|||
return this.player.playOpusStream(resource, options);
|
||||
} else if (type === 'ogg/opus') {
|
||||
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
|
||||
return this.player.playOpusStream(resource.pipe(new prism.OggOpusDemuxer()), options);
|
||||
return this.player.playOpusStream(resource.pipe(new prism.opus.OggDemuxer()), options);
|
||||
} else if (type === 'webm/opus') {
|
||||
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
|
||||
return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer()), options);
|
||||
return this.player.playOpusStream(resource.pipe(new prism.opus.WebmDemuxer()), options);
|
||||
}
|
||||
}
|
||||
throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE');
|
||||
|
|
@ -84,12 +85,12 @@ class PlayInterface {
|
|||
|
||||
static applyToClass(structure) {
|
||||
for (const prop of ['play']) {
|
||||
Object.defineProperty(structure.prototype, prop,
|
||||
Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop));
|
||||
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlayInterface;
|
||||
|
||||
// eslint-disable-next-line import/order
|
||||
const Broadcast = require('../VoiceBroadcast');
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const libs = {
|
||||
sodium: sodium => ({
|
||||
open: sodium.api.crypto_secretbox_open_easy,
|
||||
|
|
@ -25,6 +27,6 @@ exports.methods = {};
|
|||
if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; // eslint-disable-line no-await-in-loop
|
||||
exports.methods = libs[libName](lib);
|
||||
break;
|
||||
} catch (err) {} // eslint-disable-line no-empty
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const { Readable } = require('stream');
|
||||
|
||||
const SILENCE_FRAME = Buffer.from([0xF8, 0xFF, 0xFE]);
|
||||
const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
|
||||
|
||||
class Silence extends Readable {
|
||||
_read() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
/**
|
||||
|
|
@ -13,6 +15,7 @@ class VolumeInterface extends EventEmitter {
|
|||
/**
|
||||
* Whether or not the volume of this stream is editable
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get volumeEditable() {
|
||||
return true;
|
||||
|
|
@ -20,8 +23,8 @@ class VolumeInterface extends EventEmitter {
|
|||
|
||||
/**
|
||||
* The current volume of the stream
|
||||
* @readonly
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get volume() {
|
||||
return this._volume;
|
||||
|
|
@ -29,8 +32,8 @@ class VolumeInterface extends EventEmitter {
|
|||
|
||||
/**
|
||||
* The current volume of the stream in decibels
|
||||
* @readonly
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get volumeDecibels() {
|
||||
return Math.log10(this.volume) * 20;
|
||||
|
|
@ -38,8 +41,8 @@ class VolumeInterface extends EventEmitter {
|
|||
|
||||
/**
|
||||
* The current volume of the stream from a logarithmic scale
|
||||
* @readonly
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get volumeLogarithmic() {
|
||||
return Math.pow(this.volume, 1 / 1.660964);
|
||||
|
|
@ -91,19 +94,10 @@ class VolumeInterface extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
const props = [
|
||||
'volumeDecibels',
|
||||
'volumeLogarithmic',
|
||||
'setVolumeDecibels',
|
||||
'setVolumeLogarithmic',
|
||||
];
|
||||
const props = ['volumeDecibels', 'volumeLogarithmic', 'setVolumeDecibels', 'setVolumeLogarithmic'];
|
||||
|
||||
exports.applyToClass = function applyToClass(structure) {
|
||||
for (const prop of props) {
|
||||
Object.defineProperty(
|
||||
structure.prototype,
|
||||
prop,
|
||||
Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop)
|
||||
);
|
||||
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,488 +0,0 @@
|
|||
const EventEmitter = require('events');
|
||||
const { Events, OPCodes, Status, WSCodes } = require('../../util/Constants');
|
||||
const PacketManager = require('./packets/WebSocketPacketManager');
|
||||
const WebSocket = require('../../WebSocket');
|
||||
try {
|
||||
var zlib = require('zlib-sync');
|
||||
if (!zlib.Inflate) zlib = require('pako');
|
||||
} catch (err) {
|
||||
zlib = require('pako');
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts a WebSocket connection with decoding/encoding for the Discord gateway.
|
||||
* @private
|
||||
*/
|
||||
class WebSocketConnection extends EventEmitter {
|
||||
/**
|
||||
* @param {WebSocketManager} manager The WebSocket manager
|
||||
* @param {string} gateway The WebSocket gateway to connect to
|
||||
*/
|
||||
constructor(manager, gateway) {
|
||||
super();
|
||||
/**
|
||||
* The WebSocket Manager of this connection
|
||||
* @type {WebSocketManager}
|
||||
*/
|
||||
this.manager = manager;
|
||||
|
||||
/**
|
||||
* The client this belongs to
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = manager.client;
|
||||
|
||||
/**
|
||||
* The WebSocket connection itself
|
||||
* @type {WebSocket}
|
||||
*/
|
||||
this.ws = null;
|
||||
|
||||
/**
|
||||
* The current sequence of the WebSocket
|
||||
* @type {number}
|
||||
*/
|
||||
this.sequence = -1;
|
||||
|
||||
/**
|
||||
* The current sessionID of the WebSocket
|
||||
* @type {string}
|
||||
*/
|
||||
this.sessionID = null;
|
||||
|
||||
/**
|
||||
* The current status of the client
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = Status.IDLE;
|
||||
|
||||
/**
|
||||
* The Packet Manager of the connection
|
||||
* @type {WebSocketPacketManager}
|
||||
*/
|
||||
this.packetManager = new PacketManager(this);
|
||||
|
||||
/**
|
||||
* The last time a ping was sent (a timestamp)
|
||||
* @type {number}
|
||||
*/
|
||||
this.lastPingTimestamp = 0;
|
||||
|
||||
/**
|
||||
* Contains the rate limit queue and metadata
|
||||
* @type {Object}
|
||||
*/
|
||||
this.ratelimit = {
|
||||
queue: [],
|
||||
remaining: 120,
|
||||
total: 120,
|
||||
time: 60e3,
|
||||
resetTimer: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Events that are disabled (will not be processed)
|
||||
* @type {Object}
|
||||
*/
|
||||
this.disabledEvents = {};
|
||||
for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true;
|
||||
|
||||
/**
|
||||
* The sequence on WebSocket close
|
||||
* @type {number}
|
||||
*/
|
||||
this.closeSequence = 0;
|
||||
|
||||
/**
|
||||
* Whether or not the WebSocket is expecting to be closed
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.expectingClose = false;
|
||||
|
||||
this.inflate = null;
|
||||
this.connect(gateway);
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the client to be marked as ready and emits the ready event.
|
||||
* @returns {void}
|
||||
*/
|
||||
triggerReady() {
|
||||
if (this.status === Status.READY) {
|
||||
this.debug('Tried to mark self as ready, but already ready');
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Emitted when the client becomes ready to start working.
|
||||
* @event Client#ready
|
||||
*/
|
||||
this.status = Status.READY;
|
||||
this.client.emit(Events.READY);
|
||||
this.packetManager.handleQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the client is ready to be marked as ready.
|
||||
* @returns {void}
|
||||
*/
|
||||
checkIfReady() {
|
||||
if (this.status === Status.READY || this.status === Status.NEARLY) return false;
|
||||
let unavailableGuilds = 0;
|
||||
for (const guild of this.client.guilds.values()) {
|
||||
if (!guild.available) unavailableGuilds++;
|
||||
}
|
||||
if (unavailableGuilds === 0) {
|
||||
this.status = Status.NEARLY;
|
||||
if (!this.client.options.fetchAllMembers) return this.triggerReady();
|
||||
// Fetch all members before marking self as ready
|
||||
const promises = this.client.guilds.map(g => g.members.fetch());
|
||||
Promise.all(promises)
|
||||
.then(() => this.triggerReady())
|
||||
.catch(e => {
|
||||
this.debug(`Failed to fetch all members before ready! ${e}`);
|
||||
this.triggerReady();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Util
|
||||
/**
|
||||
* Emits a debug message.
|
||||
* @param {string} message Debug message
|
||||
* @returns {void}
|
||||
*/
|
||||
debug(message) {
|
||||
if (message instanceof Error) message = message.stack;
|
||||
return this.manager.debug(`[connection] ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the current WebSocket queue.
|
||||
*/
|
||||
processQueue() {
|
||||
if (this.ratelimit.remaining === 0) return;
|
||||
if (this.ratelimit.queue.length === 0) return;
|
||||
if (this.ratelimit.remaining === this.ratelimit.total) {
|
||||
this.ratelimit.resetTimer = this.client.setTimeout(() => {
|
||||
this.ratelimit.remaining = this.ratelimit.total;
|
||||
this.processQueue();
|
||||
}, this.ratelimit.time);
|
||||
}
|
||||
while (this.ratelimit.remaining > 0) {
|
||||
const item = this.ratelimit.queue.shift();
|
||||
if (!item) return;
|
||||
this._send(item);
|
||||
this.ratelimit.remaining--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data, bypassing the queue.
|
||||
* @param {Object} data Packet to send
|
||||
* @returns {void}
|
||||
*/
|
||||
_send(data) {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
this.debug(`Tried to send packet ${data} but no WebSocket is available!`);
|
||||
return;
|
||||
}
|
||||
this.ws.send(WebSocket.pack(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds data to the queue to be sent.
|
||||
* @param {Object} data Packet to send
|
||||
* @returns {void}
|
||||
*/
|
||||
send(data) {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
this.debug(`Tried to send packet ${data} but no WebSocket is available!`);
|
||||
return;
|
||||
}
|
||||
this.ratelimit.queue.push(data);
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection to a gateway.
|
||||
* @param {string} gateway The gateway to connect to
|
||||
* @param {number} [after=0] How long to wait before connecting
|
||||
* @param {boolean} [force=false] Whether or not to force a new connection even if one already exists
|
||||
* @returns {boolean}
|
||||
*/
|
||||
connect(gateway = this.gateway, after = 0, force = false) {
|
||||
if (after) return this.client.setTimeout(() => this.connect(gateway, 0, force), after); // eslint-disable-line
|
||||
if (this.ws && !force) {
|
||||
this.debug('WebSocket connection already exists');
|
||||
return false;
|
||||
} else if (typeof gateway !== 'string') {
|
||||
this.debug(`Tried to connect to an invalid gateway: ${gateway}`);
|
||||
return false;
|
||||
}
|
||||
this.inflate = new zlib.Inflate({
|
||||
chunkSize: 65535,
|
||||
flush: zlib.Z_SYNC_FLUSH,
|
||||
to: WebSocket.encoding === 'json' ? 'string' : '',
|
||||
});
|
||||
this.expectingClose = false;
|
||||
this.gateway = gateway;
|
||||
this.debug(`Connecting to ${gateway}`);
|
||||
const ws = this.ws = WebSocket.create(gateway, {
|
||||
v: this.client.options.ws.version,
|
||||
compress: 'zlib-stream',
|
||||
});
|
||||
ws.onmessage = this.onMessage.bind(this);
|
||||
ws.onopen = this.onOpen.bind(this);
|
||||
ws.onerror = this.onError.bind(this);
|
||||
ws.onclose = this.onClose.bind(this);
|
||||
this.status = Status.CONNECTING;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the connection.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
destroy() {
|
||||
const ws = this.ws;
|
||||
if (!ws) {
|
||||
this.debug('Attempted to destroy WebSocket but no connection exists!');
|
||||
return false;
|
||||
}
|
||||
this.heartbeat(-1);
|
||||
this.expectingClose = true;
|
||||
ws.close(1000);
|
||||
this.packetManager.handleQueue();
|
||||
this.ws = null;
|
||||
this.status = Status.DISCONNECTED;
|
||||
this.ratelimit.remaining = this.ratelimit.total;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a message is received.
|
||||
* @param {Event} event Event received
|
||||
*/
|
||||
onMessage({ data }) {
|
||||
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
|
||||
const l = data.length;
|
||||
const flush = l >= 4 &&
|
||||
data[l - 4] === 0x00 &&
|
||||
data[l - 3] === 0x00 &&
|
||||
data[l - 2] === 0xFF &&
|
||||
data[l - 1] === 0xFF;
|
||||
|
||||
this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
|
||||
if (!flush) return;
|
||||
let packet;
|
||||
try {
|
||||
packet = WebSocket.unpack(this.inflate.result);
|
||||
} catch (err) {
|
||||
this.client.emit('debug', err);
|
||||
return;
|
||||
}
|
||||
this.onPacket(packet);
|
||||
if (this.client.listenerCount('raw')) this.client.emit('raw', packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current sequence of the connection.
|
||||
* @param {number} s New sequence
|
||||
*/
|
||||
setSequence(s) {
|
||||
this.sequence = s > this.sequence ? s : this.sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a packet is received.
|
||||
* @param {Object} packet Received packet
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onPacket(packet) {
|
||||
if (!packet) {
|
||||
this.debug('Received null packet');
|
||||
return false;
|
||||
}
|
||||
switch (packet.op) {
|
||||
case OPCodes.HELLO:
|
||||
return this.heartbeat(packet.d.heartbeat_interval);
|
||||
case OPCodes.RECONNECT:
|
||||
return this.reconnect();
|
||||
case OPCodes.INVALID_SESSION:
|
||||
if (!packet.d) this.sessionID = null;
|
||||
this.sequence = -1;
|
||||
this.debug('Session invalidated -- will identify with a new session');
|
||||
return this.identify(packet.d ? 2500 : 0);
|
||||
case OPCodes.HEARTBEAT_ACK:
|
||||
return this.ackHeartbeat();
|
||||
case OPCodes.HEARTBEAT:
|
||||
return this.heartbeat();
|
||||
default:
|
||||
return this.packetManager.handle(packet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a connection is opened to the gateway.
|
||||
* @param {Event} event Received open event
|
||||
*/
|
||||
onOpen(event) {
|
||||
if (event && event.target && event.target.url) this.gateway = event.target.url;
|
||||
this.debug(`Connected to gateway ${this.gateway}`);
|
||||
this.identify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes a reconnection to the gateway.
|
||||
*/
|
||||
reconnect() {
|
||||
this.debug('Attempting to reconnect in 5500ms...');
|
||||
/**
|
||||
* Emitted whenever the client tries to reconnect to the WebSocket.
|
||||
* @event Client#reconnecting
|
||||
*/
|
||||
this.client.emit(Events.RECONNECTING);
|
||||
this.connect(this.gateway, 5500, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever an error occurs with the WebSocket.
|
||||
* @param {Error} error The error that occurred
|
||||
*/
|
||||
onError(error) {
|
||||
if (error && error.message === 'uWs client connection error') {
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Emitted whenever the client's WebSocket encounters a connection error.
|
||||
* @event Client#error
|
||||
* @param {Error} error The encountered error
|
||||
*/
|
||||
this.client.emit(Events.ERROR, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @external CloseEvent
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called whenever a connection to the gateway is closed.
|
||||
* @param {CloseEvent} event Close event that was received
|
||||
*/
|
||||
onClose(event) {
|
||||
this.debug(`${this.expectingClose ? 'Client' : 'Server'} closed the WebSocket connection: ${event.code}`);
|
||||
this.closeSequence = this.sequence;
|
||||
// Reset the state before trying to fix anything
|
||||
this.emit('close', event);
|
||||
this.heartbeat(-1);
|
||||
// Should we reconnect?
|
||||
if (event.code === 1000 ? this.expectingClose : WSCodes[event.code]) {
|
||||
this.expectingClose = false;
|
||||
/**
|
||||
* Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect.
|
||||
* @event Client#disconnect
|
||||
* @param {CloseEvent} event The WebSocket close event
|
||||
*/
|
||||
this.client.emit(Events.DISCONNECT, event);
|
||||
this.debug(WSCodes[event.code]);
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
this.expectingClose = false;
|
||||
this.reconnect();
|
||||
}
|
||||
|
||||
// Heartbeat
|
||||
/**
|
||||
* Acknowledges a heartbeat.
|
||||
*/
|
||||
ackHeartbeat() {
|
||||
this.debug(`Heartbeat acknowledged, latency of ${Date.now() - this.lastPingTimestamp}ms`);
|
||||
this.client._pong(this.lastPingTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a heartbeat or sets an interval for sending heartbeats.
|
||||
* @param {number} [time] If -1, clears the interval, any other number sets an interval
|
||||
* If no value is given, a heartbeat will be sent instantly
|
||||
*/
|
||||
heartbeat(time) {
|
||||
if (!isNaN(time)) {
|
||||
if (time === -1) {
|
||||
this.debug('Clearing heartbeat interval');
|
||||
this.client.clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
} else {
|
||||
this.debug(`Setting a heartbeat interval for ${time}ms`);
|
||||
this.heartbeatInterval = this.client.setInterval(() => this.heartbeat(), time);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.debug('Sending a heartbeat');
|
||||
this.lastPingTimestamp = Date.now();
|
||||
this.send({
|
||||
op: OPCodes.HEARTBEAT,
|
||||
d: this.sequence,
|
||||
});
|
||||
}
|
||||
|
||||
// Identification
|
||||
/**
|
||||
* Identifies the client on a connection.
|
||||
* @param {number} [after] How long to wait before identifying
|
||||
* @returns {void}
|
||||
*/
|
||||
identify(after) {
|
||||
if (after) return this.client.setTimeout(this.identify.bind(this), after);
|
||||
return this.sessionID ? this.identifyResume() : this.identifyNew();
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies as a new connection on the gateway.
|
||||
* @returns {void}
|
||||
*/
|
||||
identifyNew() {
|
||||
if (!this.client.token) {
|
||||
this.debug('No token available to identify a new session with');
|
||||
return;
|
||||
}
|
||||
// Clone the generic payload and assign the token
|
||||
const d = Object.assign({ token: this.client.token }, this.client.options.ws);
|
||||
|
||||
// Sharding stuff
|
||||
const { shardId, shardCount } = this.client.options;
|
||||
if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)];
|
||||
|
||||
// Send the payload
|
||||
this.debug('Identifying as a new session');
|
||||
this.send({ op: OPCodes.IDENTIFY, d });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes a session on the gateway.
|
||||
* @returns {void}
|
||||
*/
|
||||
identifyResume() {
|
||||
if (!this.sessionID) {
|
||||
this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead');
|
||||
return this.identifyNew();
|
||||
}
|
||||
this.debug(`Attempting to resume session ${this.sessionID}`);
|
||||
|
||||
const d = {
|
||||
token: this.client.token,
|
||||
session_id: this.sessionID,
|
||||
seq: this.sequence,
|
||||
};
|
||||
|
||||
return this.send({
|
||||
op: OPCodes.RESUME,
|
||||
d,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketConnection;
|
||||
|
|
@ -1,89 +1,438 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { Events, Status } = require('../../util/Constants');
|
||||
const WebSocketConnection = require('./WebSocketConnection');
|
||||
const WebSocketShard = require('./WebSocketShard');
|
||||
const PacketHandlers = require('./handlers');
|
||||
const { Error: DJSError } = require('../../errors');
|
||||
const Collection = require('../../util/Collection');
|
||||
const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants');
|
||||
const Util = require('../../util/Util');
|
||||
|
||||
const BeforeReadyWhitelist = [
|
||||
WSEvents.READY,
|
||||
WSEvents.RESUMED,
|
||||
WSEvents.GUILD_CREATE,
|
||||
WSEvents.GUILD_DELETE,
|
||||
WSEvents.GUILD_MEMBERS_CHUNK,
|
||||
WSEvents.GUILD_MEMBER_ADD,
|
||||
WSEvents.GUILD_MEMBER_REMOVE,
|
||||
];
|
||||
|
||||
const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes)
|
||||
.slice(1)
|
||||
.map(Number);
|
||||
const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007];
|
||||
|
||||
/**
|
||||
* WebSocket Manager of the client.
|
||||
* @private
|
||||
* The WebSocket manager for this client.
|
||||
* <info>This class forwards raw dispatch events,
|
||||
* read more about it here {@link https://discordapp.com/developers/docs/topics/gateway}</info>
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
class WebSocketManager extends EventEmitter {
|
||||
constructor(client) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The client that instantiated this WebSocketManager
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
* @name WebSocketManager#client
|
||||
*/
|
||||
this.client = client;
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
|
||||
/**
|
||||
* The WebSocket connection of this manager
|
||||
* @type {?WebSocketConnection}
|
||||
* The gateway this manager uses
|
||||
* @type {?string}
|
||||
*/
|
||||
this.connection = null;
|
||||
this.gateway = undefined;
|
||||
|
||||
/**
|
||||
* The amount of shards this manager handles
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
this.totalShards = this.client.options.shards.length;
|
||||
|
||||
/**
|
||||
* A collection of all shards this manager handles
|
||||
* @type {Collection<number, WebSocketShard>}
|
||||
*/
|
||||
this.shards = new Collection();
|
||||
|
||||
/**
|
||||
* An array of shards to be connected or that need to reconnect
|
||||
* @type {Set<WebSocketShard>}
|
||||
* @private
|
||||
* @name WebSocketManager#shardQueue
|
||||
*/
|
||||
Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true });
|
||||
|
||||
/**
|
||||
* An array of queued events before this WebSocketManager became ready
|
||||
* @type {object[]}
|
||||
* @private
|
||||
* @name WebSocketManager#packetQueue
|
||||
*/
|
||||
Object.defineProperty(this, 'packetQueue', { value: [] });
|
||||
|
||||
/**
|
||||
* The current status of this WebSocketManager
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = Status.IDLE;
|
||||
|
||||
/**
|
||||
* If this manager was destroyed. It will prevent shards from reconnecting
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.destroyed = false;
|
||||
|
||||
/**
|
||||
* If this manager is currently reconnecting one or multiple shards
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.reconnecting = false;
|
||||
|
||||
/**
|
||||
* The current session limit of the client
|
||||
* @private
|
||||
* @type {?Object}
|
||||
* @prop {number} total Total number of identifies available
|
||||
* @prop {number} remaining Number of identifies remaining
|
||||
* @prop {number} reset_after Number of milliseconds after which the limit resets
|
||||
*/
|
||||
this.sessionStartLimit = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a heartbeat on the available connection.
|
||||
* @returns {void}
|
||||
* The average ping of all WebSocketShards
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
heartbeat() {
|
||||
if (!this.connection) return this.debug('No connection to heartbeat');
|
||||
return this.connection.heartbeat();
|
||||
get ping() {
|
||||
const sum = this.shards.reduce((a, b) => a + b.ping, 0);
|
||||
return sum / this.shards.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a debug event.
|
||||
* @param {string} message Debug message
|
||||
* @returns {void}
|
||||
* Emits a debug message.
|
||||
* @param {string} message The debug message
|
||||
* @param {?WebSocketShard} [shard] The shard that emitted this message, if any
|
||||
* @private
|
||||
*/
|
||||
debug(message) {
|
||||
return this.client.emit(Events.DEBUG, `[ws] ${message}`);
|
||||
debug(message, shard) {
|
||||
this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the client.
|
||||
* @returns {void} Whether or not destruction was successful
|
||||
* Connects this manager to the gateway.
|
||||
* @private
|
||||
*/
|
||||
async connect() {
|
||||
const invalidToken = new DJSError(WSCodes[4004]);
|
||||
const {
|
||||
url: gatewayURL,
|
||||
shards: recommendedShards,
|
||||
session_start_limit: sessionStartLimit,
|
||||
} = await this.client.api.gateway.bot.get().catch(error => {
|
||||
throw error.httpStatus === 401 ? invalidToken : error;
|
||||
});
|
||||
|
||||
this.sessionStartLimit = sessionStartLimit;
|
||||
|
||||
const { total, remaining, reset_after } = sessionStartLimit;
|
||||
|
||||
this.debug(`Fetched Gateway Information
|
||||
URL: ${gatewayURL}
|
||||
Recommended Shards: ${recommendedShards}`);
|
||||
|
||||
this.debug(`Session Limit Information
|
||||
Total: ${total}
|
||||
Remaining: ${remaining}`);
|
||||
|
||||
this.gateway = `${gatewayURL}/`;
|
||||
|
||||
let { shards } = this.client.options;
|
||||
|
||||
if (shards === 'auto') {
|
||||
this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`);
|
||||
this.totalShards = this.client.options.shardCount = recommendedShards;
|
||||
shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i);
|
||||
}
|
||||
|
||||
this.totalShards = shards.length;
|
||||
this.debug(`Spawning shards: ${shards.join(', ')}`);
|
||||
this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id)));
|
||||
|
||||
await this._handleSessionLimit(remaining, reset_after);
|
||||
|
||||
return this.createShards();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation of a shard.
|
||||
* @returns {Promise<boolean>}
|
||||
* @private
|
||||
*/
|
||||
async createShards() {
|
||||
// If we don't have any shards to handle, return
|
||||
if (!this.shardQueue.size) return false;
|
||||
|
||||
const [shard] = this.shardQueue;
|
||||
|
||||
this.shardQueue.delete(shard);
|
||||
|
||||
if (!shard.eventsAttached) {
|
||||
shard.on(ShardEvents.ALL_READY, unavailableGuilds => {
|
||||
/**
|
||||
* Emitted when a shard turns ready.
|
||||
* @event Client#shardReady
|
||||
* @param {number} id The shard ID that turned ready
|
||||
* @param {?Set<string>} unavailableGuilds Set of unavailable guild IDs, if any
|
||||
*/
|
||||
this.client.emit(Events.SHARD_READY, shard.id, unavailableGuilds);
|
||||
|
||||
if (!this.shardQueue.size) this.reconnecting = false;
|
||||
this.checkShardsReady();
|
||||
});
|
||||
|
||||
shard.on(ShardEvents.CLOSE, event => {
|
||||
if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) {
|
||||
/**
|
||||
* Emitted when a shard's WebSocket disconnects and will no longer reconnect.
|
||||
* @event Client#shardDisconnect
|
||||
* @param {CloseEvent} event The WebSocket close event
|
||||
* @param {number} id The shard ID that disconnected
|
||||
*/
|
||||
this.client.emit(Events.SHARD_DISCONNECT, event, shard.id);
|
||||
this.debug(WSCodes[event.code], shard);
|
||||
return;
|
||||
}
|
||||
|
||||
if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
|
||||
// These event codes cannot be resumed
|
||||
shard.sessionID = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when a shard is attempting to reconnect or re-identify.
|
||||
* @event Client#shardReconnecting
|
||||
* @param {number} id The shard ID that is attempting to reconnect
|
||||
*/
|
||||
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||
|
||||
this.shardQueue.add(shard);
|
||||
|
||||
if (shard.sessionID) {
|
||||
this.debug(`Session ID is present, attempting an immediate reconnect...`, shard);
|
||||
this.reconnect(true);
|
||||
} else {
|
||||
shard.destroy({ reset: true, emit: false, log: false });
|
||||
this.reconnect();
|
||||
}
|
||||
});
|
||||
|
||||
shard.on(ShardEvents.INVALID_SESSION, () => {
|
||||
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||
});
|
||||
|
||||
shard.on(ShardEvents.DESTROYED, () => {
|
||||
this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard);
|
||||
|
||||
this.client.emit(Events.SHARD_RECONNECTING, shard.id);
|
||||
|
||||
this.shardQueue.add(shard);
|
||||
this.reconnect();
|
||||
});
|
||||
|
||||
shard.eventsAttached = true;
|
||||
}
|
||||
|
||||
this.shards.set(shard.id, shard);
|
||||
|
||||
try {
|
||||
await shard.connect();
|
||||
} catch (error) {
|
||||
if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) {
|
||||
throw new DJSError(WSCodes[error.code]);
|
||||
// Undefined if session is invalid, error event for regular closes
|
||||
} else if (!error || error.code) {
|
||||
this.debug('Failed to connect to the gateway, requeueing...', shard);
|
||||
this.shardQueue.add(shard);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// If we have more shards, add a 5s delay
|
||||
if (this.shardQueue.size) {
|
||||
this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`);
|
||||
await Util.delayFor(5000);
|
||||
await this._handleSessionLimit();
|
||||
return this.createShards();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles reconnects for this manager.
|
||||
* @param {boolean} [skipLimit=false] IF this reconnect should skip checking the session limit
|
||||
* @private
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async reconnect(skipLimit = false) {
|
||||
if (this.reconnecting || this.status !== Status.READY) return false;
|
||||
this.reconnecting = true;
|
||||
try {
|
||||
if (!skipLimit) await this._handleSessionLimit();
|
||||
await this.createShards();
|
||||
} catch (error) {
|
||||
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
|
||||
if (error.httpStatus !== 401) {
|
||||
this.debug(`Possible network error occurred. Retrying in 5s...`);
|
||||
await Util.delayFor(5000);
|
||||
this.reconnecting = false;
|
||||
return this.reconnect();
|
||||
}
|
||||
// If we get an error at this point, it means we cannot reconnect anymore
|
||||
if (this.client.listenerCount(Events.INVALIDATED)) {
|
||||
/**
|
||||
* Emitted when the client's session becomes invalidated.
|
||||
* You are expected to handle closing the process gracefully and preventing a boot loop
|
||||
* if you are listening to this event.
|
||||
* @event Client#invalidated
|
||||
*/
|
||||
this.client.emit(Events.INVALIDATED);
|
||||
// Destroy just the shards. This means you have to handle the cleanup yourself
|
||||
this.destroy();
|
||||
} else {
|
||||
this.client.destroy();
|
||||
}
|
||||
} finally {
|
||||
this.reconnecting = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a packet to every shard this manager handles.
|
||||
* @param {Object} packet The packet to send
|
||||
* @private
|
||||
*/
|
||||
broadcast(packet) {
|
||||
for (const shard of this.shards.values()) shard.send(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this manager and all its shards.
|
||||
* @private
|
||||
*/
|
||||
destroy() {
|
||||
if (!this.connection) {
|
||||
this.debug('Attempted to destroy WebSocket but no connection exists!');
|
||||
return false;
|
||||
}
|
||||
return this.connection.destroy();
|
||||
if (this.destroyed) return;
|
||||
this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`);
|
||||
this.destroyed = true;
|
||||
this.shardQueue.clear();
|
||||
for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false, log: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet on the available WebSocket.
|
||||
* @param {Object} packet Packet to send
|
||||
* @returns {void}
|
||||
* Handles the timeout required if we cannot identify anymore.
|
||||
* @param {number} [remaining] The amount of remaining identify sessions that can be done today
|
||||
* @param {number} [resetAfter] The amount of time in which the identify counter resets
|
||||
* @private
|
||||
*/
|
||||
send(packet) {
|
||||
if (!this.connection) {
|
||||
this.debug('No connection to websocket');
|
||||
async _handleSessionLimit(remaining, resetAfter) {
|
||||
if (typeof remaining === 'undefined' && typeof resetAfter === 'undefined') {
|
||||
const { session_start_limit } = await this.client.api.gateway.bot.get();
|
||||
this.sessionStartLimit = session_start_limit;
|
||||
remaining = session_start_limit.remaining;
|
||||
resetAfter = session_start_limit.reset_after;
|
||||
this.debug(`Session Limit Information
|
||||
Total: ${session_start_limit.total}
|
||||
Remaining: ${remaining}`);
|
||||
}
|
||||
if (!remaining) {
|
||||
this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`);
|
||||
await Util.delayFor(resetAfter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a packet and queues it if this WebSocketManager is not ready.
|
||||
* @param {Object} [packet] The packet to be handled
|
||||
* @param {WebSocketShard} [shard] The shard that will handle this packet
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
handlePacket(packet, shard) {
|
||||
if (packet && this.status !== Status.READY) {
|
||||
if (!BeforeReadyWhitelist.includes(packet.t)) {
|
||||
this.packetQueue.push({ packet, shard });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.packetQueue.length) {
|
||||
const item = this.packetQueue.shift();
|
||||
this.client.setImmediate(() => {
|
||||
this.handlePacket(item.packet, item.shard);
|
||||
});
|
||||
}
|
||||
|
||||
if (packet && !this.client.options.disabledEvents.includes(packet.t) && PacketHandlers[packet.t]) {
|
||||
PacketHandlers[packet.t](this.client, packet, shard);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the client is ready to be marked as ready.
|
||||
* @private
|
||||
*/
|
||||
async checkShardsReady() {
|
||||
if (this.status === Status.READY) return;
|
||||
if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) {
|
||||
return;
|
||||
}
|
||||
this.connection.send(packet);
|
||||
|
||||
this.status = Status.NEARLY;
|
||||
|
||||
if (this.client.options.fetchAllMembers) {
|
||||
try {
|
||||
const promises = this.client.guilds.cache.map(guild => {
|
||||
if (guild.available) return guild.members.fetch();
|
||||
// Return empty promise if guild is unavailable
|
||||
return Promise.resolve();
|
||||
});
|
||||
await Promise.all(promises);
|
||||
} catch (err) {
|
||||
this.debug(`Failed to fetch all members before ready! ${err}\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.triggerClientReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the client to a gateway.
|
||||
* @param {string} gateway The gateway to connect to
|
||||
* @returns {boolean}
|
||||
* Causes the client to be marked as ready and emits the ready event.
|
||||
* @private
|
||||
*/
|
||||
connect(gateway) {
|
||||
if (!this.connection) {
|
||||
this.connection = new WebSocketConnection(this, gateway);
|
||||
return true;
|
||||
}
|
||||
switch (this.connection.status) {
|
||||
case Status.IDLE:
|
||||
case Status.DISCONNECTED:
|
||||
this.connection.connect(gateway, 5500);
|
||||
return true;
|
||||
default:
|
||||
this.debug(`Couldn't connect to ${gateway} as the websocket is at state ${this.connection.status}`);
|
||||
return false;
|
||||
}
|
||||
triggerClientReady() {
|
||||
this.status = Status.READY;
|
||||
|
||||
this.client.readyAt = new Date();
|
||||
|
||||
/**
|
||||
* Emitted when the client becomes ready to start working.
|
||||
* @event Client#ready
|
||||
*/
|
||||
this.client.emit(Events.CLIENT_READY);
|
||||
|
||||
this.handlePacket();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
762
src/client/websocket/WebSocketShard.js
Normal file
762
src/client/websocket/WebSocketShard.js
Normal file
|
|
@ -0,0 +1,762 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const WebSocket = require('../../WebSocket');
|
||||
const { browser, Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants');
|
||||
|
||||
const STATUS_KEYS = Object.keys(Status);
|
||||
const CONNECTION_STATE = Object.keys(WebSocket.WebSocket);
|
||||
|
||||
let zlib;
|
||||
|
||||
if (!browser) {
|
||||
try {
|
||||
zlib = require('zlib-sync');
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Shard's WebSocket connection
|
||||
*/
|
||||
class WebSocketShard extends EventEmitter {
|
||||
constructor(manager, id) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The WebSocketManager of the shard
|
||||
* @type {WebSocketManager}
|
||||
*/
|
||||
this.manager = manager;
|
||||
|
||||
/**
|
||||
* The ID of the shard
|
||||
* @type {number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* The current status of the shard
|
||||
* @type {Status}
|
||||
*/
|
||||
this.status = Status.IDLE;
|
||||
|
||||
/**
|
||||
* The current sequence of the shard
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.sequence = -1;
|
||||
|
||||
/**
|
||||
* The sequence of the shard after close
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.closeSequence = 0;
|
||||
|
||||
/**
|
||||
* The current session ID of the shard
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.sessionID = undefined;
|
||||
|
||||
/**
|
||||
* The previous heartbeat ping of the shard
|
||||
* @type {number}
|
||||
*/
|
||||
this.ping = -1;
|
||||
|
||||
/**
|
||||
* The last time a ping was sent (a timestamp)
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.lastPingTimestamp = -1;
|
||||
|
||||
/**
|
||||
* If we received a heartbeat ack back. Used to identify zombie connections
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.lastHeartbeatAcked = true;
|
||||
|
||||
/**
|
||||
* Contains the rate limit queue and metadata
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'ratelimit', {
|
||||
value: {
|
||||
queue: [],
|
||||
total: 120,
|
||||
remaining: 120,
|
||||
time: 60e3,
|
||||
timer: null,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The WebSocket connection for the current shard
|
||||
* @type {?WebSocket}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'connection', { value: null, writable: true });
|
||||
|
||||
/**
|
||||
* @external Inflate
|
||||
* @see {@link https://www.npmjs.com/package/zlib-sync}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The compression to use
|
||||
* @type {?Inflate}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'inflate', { value: null, writable: true });
|
||||
|
||||
/**
|
||||
* The HELLO timeout
|
||||
* @type {?NodeJS.Timer}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'helloTimeout', { value: undefined, writable: true });
|
||||
|
||||
/**
|
||||
* If the manager attached its event handlers on the shard
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'eventsAttached', { value: false, writable: true });
|
||||
|
||||
/**
|
||||
* A set of guild IDs this shard expects to receive
|
||||
* @type {?Set<string>}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'expectedGuilds', { value: undefined, writable: true });
|
||||
|
||||
/**
|
||||
* The ready timeout
|
||||
* @type {?NodeJS.Timer}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'readyTimeout', { value: undefined, writable: true });
|
||||
|
||||
/**
|
||||
* Time when the WebSocket connection was opened
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, 'connectedAt', { value: 0, writable: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a debug event.
|
||||
* @param {string} message The debug message
|
||||
* @private
|
||||
*/
|
||||
debug(message) {
|
||||
this.manager.debug(message, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the shard to the gateway.
|
||||
* @private
|
||||
* @returns {Promise<void>} A promise that will resolve if the shard turns ready successfully,
|
||||
* or reject if we couldn't connect
|
||||
*/
|
||||
connect() {
|
||||
const { gateway, client } = this.manager;
|
||||
|
||||
if (this.connection && this.connection.readyState === WebSocket.OPEN && this.status === Status.READY) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
this.removeListener(ShardEvents.CLOSE, onClose);
|
||||
this.removeListener(ShardEvents.READY, onReady);
|
||||
this.removeListener(ShardEvents.RESUMED, onResumed);
|
||||
this.removeListener(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed);
|
||||
this.removeListener(ShardEvents.DESTROYED, onInvalidOrDestroyed);
|
||||
};
|
||||
|
||||
const onReady = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onResumed = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onClose = event => {
|
||||
cleanup();
|
||||
reject(event);
|
||||
};
|
||||
|
||||
const onInvalidOrDestroyed = () => {
|
||||
cleanup();
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject();
|
||||
};
|
||||
|
||||
this.once(ShardEvents.READY, onReady);
|
||||
this.once(ShardEvents.RESUMED, onResumed);
|
||||
this.once(ShardEvents.CLOSE, onClose);
|
||||
this.once(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed);
|
||||
this.once(ShardEvents.DESTROYED, onInvalidOrDestroyed);
|
||||
|
||||
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
|
||||
this.debug('An open connection was found, attempting an immediate identify.');
|
||||
this.identify();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.connection) {
|
||||
this.debug(`A connection object was found. Cleaning up before continuing.
|
||||
State: ${CONNECTION_STATE[this.connection.readyState]}`);
|
||||
this.destroy({ emit: false });
|
||||
}
|
||||
|
||||
const wsQuery = { v: client.options.ws.version };
|
||||
|
||||
if (zlib) {
|
||||
this.inflate = new zlib.Inflate({
|
||||
chunkSize: 65535,
|
||||
flush: zlib.Z_SYNC_FLUSH,
|
||||
to: WebSocket.encoding === 'json' ? 'string' : '',
|
||||
});
|
||||
wsQuery.compress = 'zlib-stream';
|
||||
}
|
||||
|
||||
this.debug(
|
||||
`[CONNECT]
|
||||
Gateway : ${gateway}
|
||||
Version : ${client.options.ws.version}
|
||||
Encoding : ${WebSocket.encoding}
|
||||
Compression: ${zlib ? 'zlib-stream' : 'none'}`,
|
||||
);
|
||||
|
||||
this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING;
|
||||
this.setHelloTimeout();
|
||||
|
||||
this.connectedAt = Date.now();
|
||||
|
||||
const ws = (this.connection = WebSocket.create(gateway, wsQuery));
|
||||
ws.onopen = this.onOpen.bind(this);
|
||||
ws.onmessage = this.onMessage.bind(this);
|
||||
ws.onerror = this.onError.bind(this);
|
||||
ws.onclose = this.onClose.bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a connection is opened to the gateway.
|
||||
* @private
|
||||
*/
|
||||
onOpen() {
|
||||
this.debug(`[CONNECTED] ${this.connection.url} in ${Date.now() - this.connectedAt}ms`);
|
||||
this.status = Status.NEARLY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a message is received.
|
||||
* @param {MessageEvent} event Event received
|
||||
* @private
|
||||
*/
|
||||
onMessage({ data }) {
|
||||
let raw;
|
||||
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
|
||||
if (zlib) {
|
||||
const l = data.length;
|
||||
const flush =
|
||||
l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff;
|
||||
|
||||
this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
|
||||
if (!flush) return;
|
||||
raw = this.inflate.result;
|
||||
} else {
|
||||
raw = data;
|
||||
}
|
||||
let packet;
|
||||
try {
|
||||
packet = WebSocket.unpack(raw);
|
||||
this.manager.client.emit(Events.RAW, packet, this.id);
|
||||
if (packet.op === OPCodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id);
|
||||
} catch (err) {
|
||||
this.manager.client.emit(Events.SHARD_ERROR, err, this.id);
|
||||
return;
|
||||
}
|
||||
this.onPacket(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever an error occurs with the WebSocket.
|
||||
* @param {ErrorEvent} event The error that occurred
|
||||
* @private
|
||||
*/
|
||||
onError(event) {
|
||||
const error = event && event.error ? event.error : event;
|
||||
if (!error) return;
|
||||
|
||||
/**
|
||||
* Emitted whenever a shard's WebSocket encounters a connection error.
|
||||
* @event Client#shardError
|
||||
* @param {Error} error The encountered error
|
||||
* @param {number} shardID The shard that encountered this error
|
||||
*/
|
||||
this.manager.client.emit(Events.SHARD_ERROR, error, this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @external CloseEvent
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external ErrorEvent
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external MessageEvent
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called whenever a connection to the gateway is closed.
|
||||
* @param {CloseEvent} event Close event that was received
|
||||
* @private
|
||||
*/
|
||||
onClose(event) {
|
||||
if (this.sequence !== -1) this.closeSequence = this.sequence;
|
||||
this.sequence = -1;
|
||||
|
||||
this.debug(`[CLOSE]
|
||||
Event Code: ${event.code}
|
||||
Clean : ${event.wasClean}
|
||||
Reason : ${event.reason || 'No reason received'}`);
|
||||
|
||||
this.setHeartbeatTimer(-1);
|
||||
this.setHelloTimeout(-1);
|
||||
// If we still have a connection object, clean up its listeners
|
||||
if (this.connection) this._cleanupConnection();
|
||||
|
||||
this.status = Status.DISCONNECTED;
|
||||
|
||||
/**
|
||||
* Emitted when a shard's WebSocket closes.
|
||||
* @private
|
||||
* @event WebSocketShard#close
|
||||
* @param {CloseEvent} event The received event
|
||||
*/
|
||||
this.emit(ShardEvents.CLOSE, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a packet is received.
|
||||
* @param {Object} packet The received packet
|
||||
* @private
|
||||
*/
|
||||
onPacket(packet) {
|
||||
if (!packet) {
|
||||
this.debug(`Received broken packet: '${packet}'.`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (packet.t) {
|
||||
case WSEvents.READY:
|
||||
/**
|
||||
* Emitted when the shard receives the READY payload and is now waiting for guilds
|
||||
* @event WebSocketShard#ready
|
||||
*/
|
||||
this.emit(ShardEvents.READY);
|
||||
|
||||
this.sessionID = packet.d.session_id;
|
||||
this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id));
|
||||
this.status = Status.WAITING_FOR_GUILDS;
|
||||
this.debug(`[READY] Session ${this.sessionID}.`);
|
||||
this.lastHeartbeatAcked = true;
|
||||
this.sendHeartbeat('ReadyHeartbeat');
|
||||
break;
|
||||
case WSEvents.RESUMED: {
|
||||
/**
|
||||
* Emitted when the shard resumes successfully
|
||||
* @event WebSocketShard#resumed
|
||||
*/
|
||||
this.emit(ShardEvents.RESUMED);
|
||||
|
||||
this.status = Status.READY;
|
||||
const replayed = packet.s - this.closeSequence;
|
||||
this.debug(`[RESUMED] Session ${this.sessionID} | Replayed ${replayed} events.`);
|
||||
this.lastHeartbeatAcked = true;
|
||||
this.sendHeartbeat('ResumeHeartbeat');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet.s > this.sequence) this.sequence = packet.s;
|
||||
|
||||
switch (packet.op) {
|
||||
case OPCodes.HELLO:
|
||||
this.setHelloTimeout(-1);
|
||||
this.setHeartbeatTimer(packet.d.heartbeat_interval);
|
||||
this.identify();
|
||||
break;
|
||||
case OPCodes.RECONNECT:
|
||||
this.destroy({ closeCode: 4000 });
|
||||
break;
|
||||
case OPCodes.INVALID_SESSION:
|
||||
this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`);
|
||||
// If we can resume the session, do so immediately
|
||||
if (packet.d) {
|
||||
this.identifyResume();
|
||||
return;
|
||||
}
|
||||
// Reset the sequence
|
||||
this.sequence = -1;
|
||||
// Reset the session ID as it's invalid
|
||||
this.sessionID = undefined;
|
||||
// Set the status to reconnecting
|
||||
this.status = Status.RECONNECTING;
|
||||
// Finally, emit the INVALID_SESSION event
|
||||
this.emit(ShardEvents.INVALID_SESSION);
|
||||
break;
|
||||
case OPCodes.HEARTBEAT_ACK:
|
||||
this.ackHeartbeat();
|
||||
break;
|
||||
case OPCodes.HEARTBEAT:
|
||||
this.sendHeartbeat('HeartbeatRequest');
|
||||
break;
|
||||
default:
|
||||
this.manager.handlePacket(packet, this);
|
||||
if (this.status === Status.WAITING_FOR_GUILDS && packet.t === WSEvents.GUILD_CREATE) {
|
||||
this.expectedGuilds.delete(packet.d.id);
|
||||
this.checkReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the shard can be marked as ready
|
||||
* @private
|
||||
*/
|
||||
checkReady() {
|
||||
// Step 0. Clear the ready timeout, if it exists
|
||||
if (this.readyTimeout) {
|
||||
this.manager.client.clearTimeout(this.readyTimeout);
|
||||
this.readyTimeout = undefined;
|
||||
}
|
||||
// Step 1. If we don't have any other guilds pending, we are ready
|
||||
if (!this.expectedGuilds.size) {
|
||||
this.debug('Shard received all its guilds. Marking as fully ready.');
|
||||
this.status = Status.READY;
|
||||
|
||||
/**
|
||||
* Emitted when the shard is fully ready.
|
||||
* This event is emitted if:
|
||||
* * all guilds were received by this shard
|
||||
* * the ready timeout expired, and some guilds are unavailable
|
||||
* @event WebSocketShard#allReady
|
||||
* @param {?Set<string>} unavailableGuilds Set of unavailable guilds, if any
|
||||
*/
|
||||
this.emit(ShardEvents.ALL_READY);
|
||||
return;
|
||||
}
|
||||
// Step 2. Create a 15s timeout that will mark the shard as ready if there are still unavailable guilds
|
||||
this.readyTimeout = this.manager.client.setTimeout(() => {
|
||||
this.debug(`Shard did not receive any more guild packets in 15 seconds.
|
||||
Unavailable guild count: ${this.expectedGuilds.size}`);
|
||||
|
||||
this.readyTimeout = undefined;
|
||||
|
||||
this.status = Status.READY;
|
||||
|
||||
this.emit(ShardEvents.ALL_READY, this.expectedGuilds);
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HELLO packet timeout.
|
||||
* @param {number} [time] If set to -1, it will clear the hello timeout timeout
|
||||
* @private
|
||||
*/
|
||||
setHelloTimeout(time) {
|
||||
if (time === -1) {
|
||||
if (this.helloTimeout) {
|
||||
this.debug('Clearing the HELLO timeout.');
|
||||
this.manager.client.clearTimeout(this.helloTimeout);
|
||||
this.helloTimeout = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.debug('Setting a HELLO timeout for 20s.');
|
||||
this.helloTimeout = this.manager.client.setTimeout(() => {
|
||||
this.debug('Did not receive HELLO in time. Destroying and connecting again.');
|
||||
this.destroy({ reset: true, closeCode: 4009 });
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the heartbeat timer for this shard.
|
||||
* @param {number} time If -1, clears the interval, any other number sets an interval
|
||||
* @private
|
||||
*/
|
||||
setHeartbeatTimer(time) {
|
||||
if (time === -1) {
|
||||
if (this.heartbeatInterval) {
|
||||
this.debug('Clearing the heartbeat interval.');
|
||||
this.manager.client.clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.debug(`Setting a heartbeat interval for ${time}ms.`);
|
||||
// Sanity checks
|
||||
if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a heartbeat to the WebSocket.
|
||||
* If this shard didn't receive a heartbeat last time, it will destroy it and reconnect
|
||||
* @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent
|
||||
* @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully.
|
||||
* @private
|
||||
*/
|
||||
sendHeartbeat(
|
||||
tag = 'HeartbeatTimer',
|
||||
ignoreHeartbeatAck = [Status.WAITING_FOR_GUILDS, Status.IDENTIFYING, Status.RESUMING].includes(this.status),
|
||||
) {
|
||||
if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) {
|
||||
this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`);
|
||||
} else if (!this.lastHeartbeatAcked) {
|
||||
this.debug(
|
||||
`[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting.
|
||||
Status : ${STATUS_KEYS[this.status]}
|
||||
Sequence : ${this.sequence}
|
||||
Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`,
|
||||
);
|
||||
|
||||
this.destroy({ closeCode: 4009, reset: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug(`[${tag}] Sending a heartbeat.`);
|
||||
this.lastHeartbeatAcked = false;
|
||||
this.lastPingTimestamp = Date.now();
|
||||
this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges a heartbeat.
|
||||
* @private
|
||||
*/
|
||||
ackHeartbeat() {
|
||||
this.lastHeartbeatAcked = true;
|
||||
const latency = Date.now() - this.lastPingTimestamp;
|
||||
this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`);
|
||||
this.ping = latency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the client on the connection.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
identify() {
|
||||
return this.sessionID ? this.identifyResume() : this.identifyNew();
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies as a new connection on the gateway.
|
||||
* @private
|
||||
*/
|
||||
identifyNew() {
|
||||
const { client } = this.manager;
|
||||
if (!client.token) {
|
||||
this.debug('[IDENTIFY] No token available to identify a new session.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = Status.IDENTIFYING;
|
||||
|
||||
// Clone the identify payload and assign the token and shard info
|
||||
const d = {
|
||||
...client.options.ws,
|
||||
token: client.token,
|
||||
shard: [this.id, Number(client.options.shardCount)],
|
||||
};
|
||||
|
||||
this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount}`);
|
||||
this.send({ op: OPCodes.IDENTIFY, d }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes a session on the gateway.
|
||||
* @private
|
||||
*/
|
||||
identifyResume() {
|
||||
if (!this.sessionID) {
|
||||
this.debug('[RESUME] No session ID was present; identifying as a new session.');
|
||||
this.identifyNew();
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = Status.RESUMING;
|
||||
|
||||
this.debug(`[RESUME] Session ${this.sessionID}, sequence ${this.closeSequence}`);
|
||||
|
||||
const d = {
|
||||
token: this.manager.client.token,
|
||||
session_id: this.sessionID,
|
||||
seq: this.closeSequence,
|
||||
};
|
||||
|
||||
this.send({ op: OPCodes.RESUME, d }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a packet to the queue to be sent to the gateway.
|
||||
* <warn>If you use this method, make sure you understand that you need to provide
|
||||
* a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
|
||||
* Do not use this method if you don't know what you're doing.</warn>
|
||||
* @param {Object} data The full packet to send
|
||||
* @param {boolean} [important=false] If this packet should be added first in queue
|
||||
*/
|
||||
send(data, important = false) {
|
||||
this.ratelimit.queue[important ? 'unshift' : 'push'](data);
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data, bypassing the queue.
|
||||
* @param {Object} data Packet to send
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
_send(data) {
|
||||
if (!this.connection || this.connection.readyState !== WebSocket.OPEN) {
|
||||
this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`);
|
||||
this.destroy({ close: 4000 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.connection.send(WebSocket.pack(data), err => {
|
||||
if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the current WebSocket queue.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
processQueue() {
|
||||
if (this.ratelimit.remaining === 0) return;
|
||||
if (this.ratelimit.queue.length === 0) return;
|
||||
if (this.ratelimit.remaining === this.ratelimit.total) {
|
||||
this.ratelimit.timer = this.manager.client.setTimeout(() => {
|
||||
this.ratelimit.remaining = this.ratelimit.total;
|
||||
this.processQueue();
|
||||
}, this.ratelimit.time);
|
||||
}
|
||||
while (this.ratelimit.remaining > 0) {
|
||||
const item = this.ratelimit.queue.shift();
|
||||
if (!item) return;
|
||||
this._send(item);
|
||||
this.ratelimit.remaining--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this shard and closes its WebSocket connection.
|
||||
* @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard
|
||||
* @private
|
||||
*/
|
||||
destroy({ closeCode = 1000, reset = false, emit = true, log = true } = {}) {
|
||||
if (log) {
|
||||
this.debug(`[DESTROY]
|
||||
Close Code : ${closeCode}
|
||||
Reset : ${reset}
|
||||
Emit DESTROYED: ${emit}`);
|
||||
}
|
||||
|
||||
// Step 0: Remove all timers
|
||||
this.setHeartbeatTimer(-1);
|
||||
this.setHelloTimeout(-1);
|
||||
|
||||
// Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED
|
||||
if (this.connection) {
|
||||
// If the connection is currently opened, we will (hopefully) receive close
|
||||
if (this.connection.readyState === WebSocket.OPEN) {
|
||||
this.connection.close(closeCode);
|
||||
} else {
|
||||
// Connection is not OPEN
|
||||
this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
|
||||
// Remove listeners from the connection
|
||||
this._cleanupConnection();
|
||||
// Attempt to close the connection just in case
|
||||
try {
|
||||
this.connection.close(closeCode);
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
// Emit the destroyed event if needed
|
||||
if (emit) this._emitDestroyed();
|
||||
}
|
||||
} else if (emit) {
|
||||
// We requested a destroy, but we had no connection. Emit destroyed
|
||||
this._emitDestroyed();
|
||||
}
|
||||
|
||||
// Step 2: Null the connection object
|
||||
this.connection = null;
|
||||
|
||||
// Step 3: Set the shard status to DISCONNECTED
|
||||
this.status = Status.DISCONNECTED;
|
||||
|
||||
// Step 4: Cache the old sequence (use to attempt a resume)
|
||||
if (this.sequence !== -1) this.closeSequence = this.sequence;
|
||||
|
||||
// Step 5: Reset the sequence and session ID if requested
|
||||
if (reset) {
|
||||
this.sequence = -1;
|
||||
this.sessionID = undefined;
|
||||
}
|
||||
|
||||
// Step 6: reset the ratelimit data
|
||||
this.ratelimit.remaining = this.ratelimit.total;
|
||||
this.ratelimit.queue.length = 0;
|
||||
if (this.ratelimit.timer) {
|
||||
this.manager.client.clearTimeout(this.ratelimit.timer);
|
||||
this.ratelimit.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the WebSocket connection listeners.
|
||||
* @private
|
||||
*/
|
||||
_cleanupConnection() {
|
||||
this.connection.onopen = this.connection.onclose = this.connection.onerror = this.connection.onmessage = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the DESTROYED event on the shard
|
||||
* @private
|
||||
*/
|
||||
_emitDestroyed() {
|
||||
/**
|
||||
* Emitted when a shard is destroyed, but no WebSocket connection was present.
|
||||
* @private
|
||||
* @event WebSocketShard#destroyed
|
||||
*/
|
||||
this.emit(ShardEvents.DESTROYED);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketShard;
|
||||
5
src/client/websocket/handlers/CHANNEL_CREATE.js
Normal file
5
src/client/websocket/handlers/CHANNEL_CREATE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.ChannelCreate.handle(packet.d);
|
||||
};
|
||||
5
src/client/websocket/handlers/CHANNEL_DELETE.js
Normal file
5
src/client/websocket/handlers/CHANNEL_DELETE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.ChannelDelete.handle(packet.d);
|
||||
};
|
||||
22
src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js
Normal file
22
src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
const channel = client.channels.cache.get(data.channel_id);
|
||||
const time = new Date(data.last_pin_timestamp);
|
||||
|
||||
if (channel && !Number.isNaN(time.getTime())) {
|
||||
// Discord sends null for last_pin_timestamp if the last pinned message was removed
|
||||
channel.lastPinTimestamp = time.getTime() || null;
|
||||
|
||||
/**
|
||||
* Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event,
|
||||
* not much information can be provided easily here - you need to manually check the pins yourself.
|
||||
* @event Client#channelPinsUpdate
|
||||
* @param {DMChannel|TextChannel} channel The channel that the pins update occurred in
|
||||
* @param {Date} time The time of the pins update
|
||||
*/
|
||||
client.emit(Events.CHANNEL_PINS_UPDATE, channel, time);
|
||||
}
|
||||
};
|
||||
16
src/client/websocket/handlers/CHANNEL_UPDATE.js
Normal file
16
src/client/websocket/handlers/CHANNEL_UPDATE.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
const { old, updated } = client.actions.ChannelUpdate.handle(packet.d);
|
||||
if (old && updated) {
|
||||
/**
|
||||
* Emitted whenever a channel is updated - e.g. name change, topic change, channel type change.
|
||||
* @event Client#channelUpdate
|
||||
* @param {DMChannel|GuildChannel} oldChannel The channel before the update
|
||||
* @param {DMChannel|GuildChannel} newChannel The channel after the update
|
||||
*/
|
||||
client.emit(Events.CHANNEL_UPDATE, old, updated);
|
||||
}
|
||||
};
|
||||
16
src/client/websocket/handlers/GUILD_BAN_ADD.js
Normal file
16
src/client/websocket/handlers/GUILD_BAN_ADD.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
const user = client.users.add(data.user);
|
||||
|
||||
/**
|
||||
* Emitted whenever a member is banned from a guild.
|
||||
* @event Client#guildBanAdd
|
||||
* @param {Guild} guild The guild that the ban occurred in
|
||||
* @param {User} user The user that was banned
|
||||
*/
|
||||
if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user);
|
||||
};
|
||||
5
src/client/websocket/handlers/GUILD_BAN_REMOVE.js
Normal file
5
src/client/websocket/handlers/GUILD_BAN_REMOVE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildBanRemove.handle(packet.d);
|
||||
};
|
||||
36
src/client/websocket/handlers/GUILD_CREATE.js
Normal file
36
src/client/websocket/handlers/GUILD_CREATE.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
'use strict';
|
||||
|
||||
const { Events, Status } = require('../../../util/Constants');
|
||||
|
||||
module.exports = async (client, { d: data }, shard) => {
|
||||
let guild = client.guilds.cache.get(data.id);
|
||||
if (guild) {
|
||||
if (!guild.available && !data.unavailable) {
|
||||
// A newly available guild
|
||||
guild._patch(data);
|
||||
// If the client was ready before and we had unavailable guilds, fetch them
|
||||
if (client.ws.status === Status.READY && client.options.fetchAllMembers) {
|
||||
await guild.members
|
||||
.fetch()
|
||||
.catch(err => client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// A new guild
|
||||
data.shardID = shard.id;
|
||||
guild = client.guilds.add(data);
|
||||
if (client.ws.status === Status.READY) {
|
||||
/**
|
||||
* Emitted whenever the client joins a guild.
|
||||
* @event Client#guildCreate
|
||||
* @param {Guild} guild The created guild
|
||||
*/
|
||||
if (client.options.fetchAllMembers) {
|
||||
await guild.members
|
||||
.fetch()
|
||||
.catch(err => client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`));
|
||||
}
|
||||
client.emit(Events.GUILD_CREATE, guild);
|
||||
}
|
||||
}
|
||||
};
|
||||
5
src/client/websocket/handlers/GUILD_DELETE.js
Normal file
5
src/client/websocket/handlers/GUILD_DELETE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildDelete.handle(packet.d);
|
||||
};
|
||||
5
src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js
Normal file
5
src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildEmojisUpdate.handle(packet.d);
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildIntegrationsUpdate.handle(packet.d);
|
||||
};
|
||||
22
src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js
Normal file
22
src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const Collection = require('../../../util/Collection');
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
if (!guild) return;
|
||||
const members = new Collection();
|
||||
|
||||
for (const member of data.members) members.set(member.user.id, guild.members.add(member));
|
||||
if (data.presences) {
|
||||
for (const presence of data.presences) guild.presences.cache.add(Object.assign(presence, { guild }));
|
||||
}
|
||||
/**
|
||||
* Emitted whenever a chunk of guild members is received (all members come from the same guild).
|
||||
* @event Client#guildMembersChunk
|
||||
* @param {Collection<Snowflake, GuildMember>} members The members in the chunk
|
||||
* @param {Guild} guild The guild related to the member chunk
|
||||
*/
|
||||
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild);
|
||||
};
|
||||
19
src/client/websocket/handlers/GUILD_MEMBER_ADD.js
Normal file
19
src/client/websocket/handlers/GUILD_MEMBER_ADD.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const { Events, Status } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }, shard) => {
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
if (guild) {
|
||||
guild.memberCount++;
|
||||
const member = guild.members.add(data);
|
||||
if (shard.status === Status.READY) {
|
||||
/**
|
||||
* Emitted whenever a user joins a guild.
|
||||
* @event Client#guildMemberAdd
|
||||
* @param {GuildMember} member The member that has joined a guild
|
||||
*/
|
||||
client.emit(Events.GUILD_MEMBER_ADD, member);
|
||||
}
|
||||
}
|
||||
};
|
||||
5
src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js
Normal file
5
src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (client, packet, shard) => {
|
||||
client.actions.GuildMemberRemove.handle(packet.d, shard);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue