Initial commit

This commit is contained in:
2021-03-14 11:09:02 +01:00
commit 21e364ee38
1046 changed files with 126647 additions and 0 deletions

17
node_modules/discord.js/.tern-project generated vendored Normal file
View File

@ -0,0 +1,17 @@
{
"ecmaVersion": 7,
"libs": [],
"loadEagerly": ["./src/*.js"],
"dontLoad": ["node_modules/**"],
"plugins": {
"es_modules": {},
"node": {},
"doc_comment": {
"fullDocs": true,
"strong": true
},
"webpack": {
"configPath": "./webpack.config.js"
}
}
}

190
node_modules/discord.js/LICENSE generated vendored Normal file
View File

@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

111
node_modules/discord.js/README.md generated vendored Normal file
View File

@ -0,0 +1,111 @@
<div align="center">
<br />
<p>
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
</p>
<br />
<p>
<a href="https://discord.gg/bRCvFy9"><img src="https://img.shields.io/discord/222078108977594368?color=7289da&logo=discord&logoColor=white" 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://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/workflows/Testing/badge.svg" alt="Build status" /></a>
<a href="https://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
<a href="https://www.patreon.com/discordjs"><img src="https://img.shields.io/badge/donate-patreon-F96854.svg" alt="Patreon" /></a>
</p>
<p>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="npm installnfo" /></a>
</p>
</div>
## Table of contents
- [About](#about)
- [Installation](#installation)
- [Audio engines](#audio-engines)
- [Optional packages](#optional-packages)
- [Example Usage](#example-usage)
- [Links](#links)
- [Extensions](#extensions)
- [Contributing](#contributing)
- [Help](#help)
## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
[Discord API](https://discord.com/developers/docs/intro).
- Object-oriented
- Predictable abstractions
- Performant
- 100% coverage of the Discord API
## Installation
**Node.js 12.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discord.js @discordjs/opus`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
### Audio engines
The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/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`)
- [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();
client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}!`);
});
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
}
});
client.login('token');
```
## Links
- [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))
## 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).

95
node_modules/discord.js/esm/discord.mjs generated vendored Normal file
View File

@ -0,0 +1,95 @@
import Discord from '../src/index.js';
export default Discord;
export const {
BaseClient,
Client,
Shard,
ShardClientUtil,
ShardingManager,
WebhookClient,
ActivityFlags,
BitField,
Collection,
Constants,
DataResolver,
BaseManager,
DiscordAPIError,
HTTPError,
MessageFlags,
Intents,
Permissions,
Speaking,
Snowflake,
SnowflakeUtil,
Structures,
SystemChannelFlags,
UserFlags,
Util,
version,
ChannelManager,
GuildChannelManager,
GuildEmojiManager,
GuildEmojiRoleManager,
GuildMemberManager,
GuildMemberRoleManager,
GuildManager,
ReactionManager,
ReactionUserManager,
MessageManager,
PresenceManager,
RoleManager,
UserManager,
discordSort,
escapeMarkdown,
fetchRecommendedShards,
resolveColor,
resolveString,
splitMessage,
Application,
Base,
Activity,
APIMessage,
BaseGuildEmoji,
CategoryChannel,
Channel,
ClientApplication,
ClientUser,
Collector,
DMChannel,
Emoji,
Guild,
GuildAuditLogs,
GuildChannel,
GuildEmoji,
GuildMember,
GuildPreview,
GuildTemplate,
Integration,
Invite,
Message,
MessageAttachment,
MessageCollector,
MessageEmbed,
MessageMentions,
MessageReaction,
NewsChannel,
PermissionOverwrites,
Presence,
ClientPresence,
ReactionCollector,
ReactionEmoji,
RichPresenceAssets,
Role,
StoreChannel,
Team,
TeamMember,
TextChannel,
User,
VoiceChannel,
VoiceRegion,
VoiceState,
Webhook,
WebSocket
} = Discord;

3
node_modules/discord.js/jsdoc.json generated vendored Normal file
View File

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

192
node_modules/discord.js/package.json generated vendored Normal file
View File

@ -0,0 +1,192 @@
{
"_from": "discord.js@^12.5.1",
"_id": "discord.js@12.5.1",
"_inBundle": false,
"_integrity": "sha512-VwZkVaUAIOB9mKdca0I5MefPMTQJTNg0qdgi1huF3iwsFwJ0L5s/Y69AQe+iPmjuV6j9rtKoG0Ta0n9vgEIL6w==",
"_location": "/discord.js",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "discord.js@^12.5.1",
"name": "discord.js",
"escapedName": "discord.js",
"rawSpec": "^12.5.1",
"saveSpec": null,
"fetchSpec": "^12.5.1"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.1.tgz",
"_shasum": "992b45753e3815526a279914ccc281d3496f5990",
"_spec": "discord.js@^12.5.1",
"_where": "E:\\IntellijProjects\\woam-antispam-bot",
"author": {
"name": "Amish Shah",
"email": "amishshah.2k@gmail.com"
},
"browser": {
"@discordjs/opus": false,
"https": false,
"ws": 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/player/AudioPlayer.js": false,
"src/client/voice/player/BasePlayer.js": false,
"src/client/voice/player/BroadcastAudioPlayer.js": false,
"src/client/voice/receiver/PacketHandler.js": false,
"src/client/voice/receiver/Receiver.js": false,
"src/client/voice/util/PlayInterface.js": false,
"src/client/voice/util/Secretbox.js": false,
"src/client/voice/util/Silence.js": false,
"src/client/voice/util/VolumeInterface.js": false
},
"bugs": {
"url": "https://github.com/discordjs/discord.js/issues"
},
"bundleDependencies": false,
"commitlint": {
"extends": [
"@commitlint/config-angular"
],
"rules": {
"scope-case": [
2,
"always",
"pascal-case"
],
"type-enum": [
2,
"always",
[
"chore",
"build",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test"
]
]
}
},
"dependencies": {
"@discordjs/collection": "^0.1.6",
"@discordjs/form-data": "^3.0.1",
"abort-controller": "^3.0.0",
"node-fetch": "^2.6.1",
"prism-media": "^1.2.2",
"setimmediate": "^1.0.5",
"tweetnacl": "^1.0.3",
"ws": "^7.3.1"
},
"deprecated": false,
"description": "A powerful library for interacting with the Discord API",
"devDependencies": {
"@commitlint/cli": "^11.0.0",
"@commitlint/config-angular": "^11.0.0",
"@types/node": "^12.12.6",
"@types/ws": "^7.2.7",
"cross-env": "^7.0.2",
"discord.js-docgen": "git+https://github.com/discordjs/docgen.git",
"dtslint": "^4.0.4",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.3.0",
"jest": "^26.6.0",
"json-filter-loader": "^1.0.0",
"lint-staged": "^10.4.2",
"prettier": "^2.1.2",
"terser-webpack-plugin": "^4.2.3",
"tslint": "^6.1.3",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
},
"engines": {
"node": ">=12.0.0"
},
"exports": {
".": [
{
"require": "./src/index.js",
"import": "./esm/discord.mjs"
},
"./src/index.js"
],
"./esm": "./esm/discord.mjs"
},
"homepage": "https://github.com/discordjs/discord.js#readme",
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"keywords": [
"discord",
"api",
"bot",
"client",
"node",
"discordapp"
],
"license": "Apache-2.0",
"lint-staged": {
"*.js": "eslint --fix",
"*.ts": "prettier --write"
},
"main": "./src/index",
"name": "discord.js",
"prettier": {
"singleQuote": true,
"printWidth": 120,
"trailingComma": "all",
"endOfLine": "lf",
"arrowParens": "avoid"
},
"repository": {
"type": "git",
"url": "git+https://github.com/discordjs/discord.js.git"
},
"runkitExampleFilename": "./docs/examples/ping.js",
"scripts": {
"build:browser": "webpack",
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:typings": "tslint typings/index.d.ts",
"prepublishOnly": "npm run test && cross-env NODE_ENV=production npm run build:browser",
"prettier": "prettier --write src/**/*.js typings/**/*.ts",
"test": "npm run lint && npm run docs:test && npm run lint:typings",
"test:typescript": "tsc"
},
"types": "./typings/index.d.ts",
"unpkg": "./webpack/discord.min.js",
"version": "12.5.1"
}

49
node_modules/discord.js/src/WebSocket.js generated vendored Normal file
View File

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

169
node_modules/discord.js/src/client/BaseClient.js generated vendored Normal file
View File

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

504
node_modules/discord.js/src/client/Client.js generated vendored Normal file
View File

@ -0,0 +1,504 @@
'use strict';
const BaseClient = require('./BaseClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
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 GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
const Collection = require('../util/Collection');
const { Events, browser, DefaultOptions } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Intents = require('../util/Intents');
const Permissions = require('../util/Permissions');
const Structures = require('../util/Structures');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
* @extends {BaseClient}
*/
class Client extends BaseClient {
/**
* @param {ClientOptions} [options] Options for the client
*/
constructor(options = {}) {
super(Object.assign({ _tokenType: 'Bot' }, options));
// Obtain shard details from environment or if present, worker threads
let data = process.env;
try {
// Test if worker threads module is present and used
data = require('worker_threads').workerData || data;
} catch {
// Do nothing
}
if (this.options.shards === DefaultOptions.shards) {
if ('SHARDS' in data) {
this.options.shards = JSON.parse(data.SHARDS);
}
}
if (this.options.shardCount === DefaultOptions.shardCount) {
if ('SHARD_COUNT' in data) {
this.options.shardCount = Number(data.SHARD_COUNT);
} else if (Array.isArray(this.options.shards)) {
this.options.shardCount = this.options.shards.length;
}
}
const typeofShards = typeof this.options.shards;
if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') {
this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i);
}
if (typeofShards === 'number') this.options.shards = [this.options.shards];
if (Array.isArray(this.options.shards)) {
this.options.shards = [
...new Set(
this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)),
),
];
}
this._validateOptions();
/**
* The WebSocket manager of the client
* @type {WebSocketManager}
*/
this.ws = new WebSocketManager(this);
/**
* The action manager of the client
* @type {ActionsManager}
* @private
*/
this.actions = new ActionsManager(this);
/**
* The voice manager of the client (`null` in browsers)
* @type {?ClientVoiceManager}
*/
this.voice = !browser ? new ClientVoiceManager(this) : null;
/**
* 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, process.env.SHARDING_MANAGER_MODE)
: null;
/**
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {UserManager}
*/
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 {GuildManager}
*/
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. 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 ChannelManager(this);
const ClientPresence = Structures.get('ClientPresence');
/**
* The presence of the Client
* @private
* @type {ClientPresence}
*/
this.presence = new ClientPresence(this);
Object.defineProperty(this, 'token', { writable: true });
if (!browser && !this.token && 'DISCORD_TOKEN' in process.env) {
/**
* Authorization token for the logged in bot.
* If present, this defaults to `process.env.DISCORD_TOKEN` when instantiating the client
* <warn>This should be kept private at all times.</warn>
* @type {?string}
*/
this.token = process.env.DISCORD_TOKEN;
} else {
this.token = null;
}
/**
* User that the client is logged in as
* @type {?ClientUser}
*/
this.user = null;
/**
* Time at which the client was last regarded as being in the `READY` state
* (each time the client disconnects and successfully reconnects, this will be overwritten)
* @type {?Date}
*/
this.readyAt = null;
if (this.options.messageSweepInterval > 0) {
this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000);
}
}
/**
* All custom emojis that the client has access to, mapped by their IDs
* @type {GuildEmojiManager}
* @readonly
*/
get emojis() {
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;
}
/**
* Timestamp of the time the client was last `READY` at
* @type {?number}
* @readonly
*/
get readyTimestamp() {
return this.readyAt ? this.readyAt.getTime() : 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;
}
/**
* Logs the client in, establishing a websocket connection to Discord.
* @param {string} [token=this.token] Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token');
*/
async login(token = this.token) {
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
this.emit(
Events.DEBUG,
`Provided token: ${token
.split('.')
.map((val, i) => (i > 1 ? val.replace(/./g, '*') : val))
.join('.')}`,
);
if (this.options.presence) {
this.options.ws.presence = await this.presence._parse(this.options.presence);
}
this.emit(Events.DEBUG, 'Preparing to connect to the gateway...');
try {
await this.ws.connect();
return this.token;
} catch (error) {
this.destroy();
throw error;
}
}
/**
* Logs out, terminates the connection to Discord, and destroys the client.
* @returns {void}
*/
destroy() {
super.destroy();
this.ws.destroy();
this.token = null;
}
/**
* Obtains an invite from Discord.
* @param {InviteResolvable} invite Invite code or URL
* @returns {Promise<Invite>}
* @example
* client.fetchInvite('https://discord.gg/bRCvFy9')
* .then(invite => console.log(`Obtained invite with code: ${invite.code}`))
* .catch(console.error);
*/
fetchInvite(invite) {
const code = DataResolver.resolveInviteCode(invite);
return this.api
.invites(code)
.get({ query: { with_counts: true } })
.then(data => new Invite(this, data));
}
/**
* Obtains a template from Discord.
* @param {GuildTemplateResolvable} template Template code or URL
* @returns {Promise<GuildTemplate>}
* @example
* client.fetchGuildTemplate('https://discord.new/FKvmczH2HyUf')
* .then(template => console.log(`Obtained template with code: ${template.code}`))
* .catch(console.error);
*/
fetchGuildTemplate(template) {
const code = DataResolver.resolveGuildTemplateCode(template);
return this.api.guilds
.templates(code)
.get()
.then(data => new GuildTemplate(this, data));
}
/**
* Obtains a webhook from Discord.
* @param {Snowflake} id ID of the webhook
* @param {string} [token] Token for the webhook
* @returns {Promise<Webhook>}
* @example
* client.fetchWebhook('id', 'token')
* .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`))
* .catch(console.error);
*/
fetchWebhook(id, token) {
return this.api
.webhooks(id, token)
.get()
.then(data => new Webhook(this, data));
}
/**
* Obtains the available voice regions from Discord.
* @returns {Promise<Collection<string, VoiceRegion>>}
* @example
* client.fetchVoiceRegions()
* .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`))
* .catch(console.error);
*/
fetchVoiceRegions() {
return this.api.voice.regions.get().then(res => {
const regions = new Collection();
for (const region of res) regions.set(region.id, new VoiceRegion(region));
return regions;
});
}
/**
* Sweeps all text-based channels' messages and removes the ones older than the max message lifetime.
* If the message has been edited, the time of the edit is used rather than the time of the original message.
* @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds)
* will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime}
* @returns {number} Amount of messages that were removed from the caches,
* or -1 if the message cache lifetime is unlimited
* @example
* // Remove all messages older than 1800 seconds from the messages cache
* const amount = client.sweepMessages(1800);
* console.log(`Successfully removed ${amount} messages from the cache.`);
*/
sweepMessages(lifetime = this.options.messageCacheLifetime) {
if (typeof lifetime !== 'number' || isNaN(lifetime)) {
throw new TypeError('INVALID_TYPE', 'lifetime', 'number');
}
if (lifetime <= 0) {
this.emit(Events.DEBUG, "Didn't sweep messages - lifetime is unlimited");
return -1;
}
const lifetimeMs = lifetime * 1000;
const now = Date.now();
let channels = 0;
let messages = 0;
for (const channel of this.channels.cache.values()) {
if (!channel.messages) continue;
channels++;
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`,
);
return messages;
}
/**
* Obtains the OAuth Application of this bot from Discord.
* @returns {Promise<ClientApplication>}
*/
fetchApplication() {
return this.api.oauth2
.applications('@me')
.get()
.then(app => new ClientApplication(this, app));
}
/**
* Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds.
* @param {GuildResolvable} guild The guild to fetch the preview for
* @returns {Promise<GuildPreview>}
*/
fetchGuildPreview(guild) {
const id = this.guilds.resolveID(guild);
if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
return this.api
.guilds(id)
.preview.get()
.then(data => new GuildPreview(this, data));
}
/**
* Generates a link that can be used to invite the bot to a guild.
* @param {InviteGenerationOptions|PermissionResolvable} [options] Permissions to request
* @returns {Promise<string>}
* @example
* client.generateInvite({
* permissions: ['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'],
* })
* .then(link => console.log(`Generated bot invite link: ${link}`))
* .catch(console.error);
*/
async generateInvite(options = {}) {
if (Array.isArray(options) || ['string', 'number'].includes(typeof options) || options instanceof Permissions) {
process.emitWarning(
'Client#generateInvite: Generate invite with an options object instead of a PermissionResolvable',
'DeprecationWarning',
);
options = { permissions: options };
}
const application = await this.fetchApplication();
const query = new URLSearchParams({
client_id: application.id,
permissions: Permissions.resolve(options.permissions),
scope: 'bot',
});
if (typeof options.disableGuildSelect === 'boolean') {
query.set('disable_guild_select', options.disableGuildSelect.toString());
}
if (typeof options.guild !== 'undefined') {
const guildID = this.guilds.resolveID(options.guild);
if (!guildID) throw new TypeError('INVALID_TYPE', 'options.guild', 'GuildResolvable');
query.set('guild_id', guildID);
}
return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
}
toJSON() {
return super.toJSON({
readyAt: false,
});
}
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
* with the client as `this`.
* @param {string} script Script to eval
* @returns {*}
* @private
*/
_eval(script) {
return eval(script);
}
/**
* Validates the client options.
* @param {ClientOptions} [options=this.options] Options to validate
* @private
*/
_validateOptions(options = this.options) {
if (typeof options.ws.intents !== 'undefined') {
options.ws.intents = Intents.resolve(options.ws.intents);
}
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) {
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1');
}
if (options.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');
}
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number');
}
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number');
}
if (
typeof options.messageEditHistoryMaxSize !== 'number' ||
isNaN(options.messageEditHistoryMaxSize) ||
options.messageEditHistoryMaxSize < -1
) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageEditHistoryMaxSize', 'a number greater than or equal to -1');
}
if (typeof options.fetchAllMembers !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', '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.retryLimit !== 'number' || isNaN(options.retryLimit)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
}
}
}
module.exports = Client;
/**
* Options for {@link Client#generateInvite}.
* @typedef {Object} InviteGenerationOptions
* @property {PermissionResolvable} [permissions] Permissions to request
* @property {GuildResolvable} [guild] Guild to preselect
* @property {boolean} [disableGuildSelect] Whether to disable the guild selection
*/
/**
* Emitted for general warnings.
* @event Client#warn
* @param {string} info The warning
*/
/**
* Emitted for general debugging information.
* @event Client#debug
* @param {string} info The debug information
*/

31
node_modules/discord.js/src/client/WebhookClient.js generated vendored Normal file
View File

@ -0,0 +1,31 @@
'use strict';
const BaseClient = require('./BaseClient');
const Webhook = require('../structures/Webhook');
/**
* The webhook client.
* @implements {Webhook}
* @extends {BaseClient}
*/
class WebhookClient extends BaseClient {
/**
* @param {Snowflake} id ID of the webhook
* @param {string} token Token of the webhook
* @param {ClientOptions} [options] Options for the client
* @example
* // Create a new webhook and send a message
* const hook = new Discord.WebhookClient('1234', 'abcdef');
* hook.send('This will send a message').catch(console.error);
*/
constructor(id, token, options) {
super(options);
Object.defineProperty(this, 'client', { value: this });
this.id = id;
Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true });
}
}
Webhook.applyToClass(WebhookClient);
module.exports = WebhookClient;

105
node_modules/discord.js/src/client/actions/Action.js generated vendored Normal file
View File

@ -0,0 +1,105 @@
'use strict';
const { PartialTypes } = require('../../util/Constants');
/*
ABOUT ACTIONS
Actions are similar to WebSocket Packet Handlers, but since introducing
the REST API methods, in order to prevent rewriting code to handle data,
"actions" have been introduced. They're basically what Packet Handlers
used to be but they're strictly for manipulating data and making sure
that WebSocket events don't clash with REST methods.
*/
class GenericAction {
constructor(client) {
this.client = client;
}
handle(data) {
return data;
}
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) {
return this.getPayload(data, guild.members, data.user.id, PartialTypes.GUILD_MEMBER);
}
getUser(data) {
const id = data.user_id;
return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER);
}
getUserFromMember(data) {
if (data.guild_id && data.member && data.member.user) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (guild) {
return guild.members.add(data.member).user;
} else {
return this.client.users.add(data.member.user);
}
}
return this.getUser(data);
}
}
module.exports = GenericAction;

View File

@ -0,0 +1,47 @@
'use strict';
class ActionsManager {
constructor(client) {
this.client = client;
this.register(require('./MessageCreate'));
this.register(require('./MessageDelete'));
this.register(require('./MessageDeleteBulk'));
this.register(require('./MessageUpdate'));
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('./GuildMemberUpdate'));
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'));
this.register(require('./TypingStart'));
}
register(Action) {
this[Action.name.replace(/Action$/, '')] = new Action(this.client);
}
}
module.exports = ActionsManager;

View File

@ -0,0 +1,23 @@
'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.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 };
}
}
module.exports = ChannelCreateAction;

View File

@ -0,0 +1,37 @@
'use strict';
const Action = require('./Action');
const DMChannel = require('../../structures/DMChannel');
const { Events } = require('../../util/Constants');
class ChannelDeleteAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
const client = this.client;
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);
}
return { channel };
}
}
module.exports = ChannelDeleteAction;

View File

@ -0,0 +1,33 @@
'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;
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,
};
}
return {};
}
}
module.exports = ChannelUpdateAction;

View File

@ -0,0 +1,21 @@
'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.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);
}
}
module.exports = GuildBanRemove;

View File

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

View File

@ -0,0 +1,67 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildDeleteAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
const client = this.client;
let guild = client.guilds.cache.get(data.id);
if (guild) {
for (const channel of guild.channels.cache.values()) {
if (channel.type === 'text') channel.stopTyping(true);
}
if (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,
// handles emitting of event itself
return {
guild: null,
};
}
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.cache.delete(guild.id);
guild.deleted = true;
/**
* Emitted whenever a guild kicks the client or the guild is deleted/left.
* @event Client#guildDelete
* @param {Guild} guild The guild that was deleted
*/
client.emit(Events.GUILD_DELETE, guild);
this.deleted.set(guild.id, guild);
this.scheduleForDeletion(guild.id);
} else {
guild = this.deleted.get(data.id) || null;
}
return { guild };
}
scheduleForDeletion(id) {
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout);
}
}
module.exports = GuildDeleteAction;

View File

@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildEmojiCreateAction extends Action {
handle(guild, createdEmoji) {
const already = guild.emojis.cache.has(createdEmoji.id);
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
*/
if (!already) this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
return { emoji };
}
}
module.exports = GuildEmojiCreateAction;

View File

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

View File

@ -0,0 +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 };
}
}
module.exports = GuildEmojiUpdateAction;

View File

@ -0,0 +1,34 @@
'use strict';
const Action = require('./Action');
class GuildEmojisUpdateAction extends Action {
handle(data) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (!guild || !guild.emojis) return;
const deletions = new Map(guild.emojis.cache);
for (const emoji of data.emojis) {
// Determine type of emoji event
const cachedEmoji = guild.emojis.cache.get(emoji.id);
if (cachedEmoji) {
deletions.delete(emoji.id);
if (!cachedEmoji.equals(emoji)) {
// Emoji updated
this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji);
}
} else {
// Emoji added
this.client.actions.GuildEmojiCreate.handle(guild, emoji);
}
}
for (const emoji of deletions.values()) {
// Emoji deleted
this.client.actions.GuildEmojiDelete.handle(emoji);
}
}
}
module.exports = GuildEmojisUpdateAction;

View File

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

View File

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

View File

@ -0,0 +1,44 @@
'use strict';
const Action = require('./Action');
const { Status, Events } = require('../../util/Constants');
class GuildMemberUpdateAction extends Action {
handle(data, shard) {
const { client } = this;
if (data.user.username) {
const user = client.users.cache.get(data.user.id);
if (!user) {
client.users.add(data.user);
} else if (!user.equals(data.user)) {
client.actions.UserUpdate.handle(data.user);
}
}
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const member = this.getMember({ user: data.user }, guild);
if (member) {
const old = member._update(data);
/**
* Emitted whenever a guild member changes - i.e. new role, removed role, nickname.
* Also emitted when the user's details (e.g. username) change.
* @event Client#guildMemberUpdate
* @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update
*/
if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_UPDATE, old, member);
} else {
const newMember = guild.members.add(data);
/**
* Emitted whenever a member becomes available in a large guild.
* @event Client#guildMemberAvailable
* @param {GuildMember} member The member that became available
*/
this.client.emit(Events.GUILD_MEMBER_AVAILABLE, newMember);
}
}
}
}
module.exports = GuildMemberUpdateAction;

View File

@ -0,0 +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.cache.get(data.guild_id);
let role;
if (guild) {
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 };
}
}
module.exports = GuildRoleCreate;

View File

@ -0,0 +1,30 @@
'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.cache.get(data.guild_id);
let role;
if (guild) {
role = guild.roles.cache.get(data.role_id);
if (role) {
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);
}
}
return { role };
}
}
module.exports = GuildRoleDeleteAction;

View File

@ -0,0 +1,39 @@
'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.cache.get(data.guild_id);
if (guild) {
let old = null;
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);
}
return {
old,
updated: role,
};
}
return {
old: null,
updated: null,
};
}
}
module.exports = GuildRoleUpdateAction;

View File

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

View File

@ -0,0 +1,33 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class GuildUpdateAction extends Action {
handle(data) {
const client = this.client;
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,
updated: guild,
};
}
return {
old: null,
updated: null,
};
}
}
module.exports = GuildUpdateAction;

View 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) 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;

View 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;

View File

@ -0,0 +1,39 @@
'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.cache.get(data.channel_id);
if (channel) {
const existing = channel.messages.cache.get(data.id);
if (existing) return { message: existing };
const message = channel.messages.add(data);
const user = message.author;
let member = message.member;
channel.lastMessageID = data.id;
if (user) {
user.lastMessageID = data.id;
user.lastMessageChannelID = channel.id;
}
if (member) {
member.lastMessageID = data.id;
member.lastMessageChannelID = channel.id;
}
/**
* Emitted whenever a message is created.
* @event Client#message
* @param {Message} message The created message
*/
client.emit(Events.MESSAGE_CREATE, message);
return { message };
}
return {};
}
}
module.exports = MessageCreateAction;

View File

@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class MessageDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel(data);
let message;
if (channel) {
message = this.getMessage(data, channel);
if (message) {
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);
}
}
return { message };
}
}
module.exports = MessageDeleteAction;

View File

@ -0,0 +1,43 @@
'use strict';
const Action = require('./Action');
const Collection = require('../../util/Collection');
const { Events } = require('../../util/Constants');
class MessageDeleteBulkAction extends Action {
handle(data) {
const client = this.client;
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 = this.getMessage(
{
id,
guild_id: data.guild_id,
},
channel,
false,
);
if (message) {
message.deleted = true;
messages.set(message.id, message);
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 };
}
return {};
}
}
module.exports = MessageDeleteBulkAction;

View File

@ -0,0 +1,55 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
const { PartialTypes } = require('../../util/Constants');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id',
// If originating from a guild
guild_id: 'id',
member: { ..., user: { ... } } }
*/
class MessageReactionAdd extends Action {
handle(data) {
if (!data.emoji) return false;
const user = this.getUserFromMember(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 existing = message.reactions.cache.get(data.emoji.id || data.emoji.name);
if (existing && existing.users.cache.has(user.id)) return { message, reaction: existing, user };
const reaction = message.reactions.add({
emoji: data.emoji,
count: message.partial ? null : 0,
me: user.id === this.client.user.id,
});
if (!reaction) return false;
reaction._add(user);
/**
* Emitted whenever a reaction is added to a cached message.
* @event Client#messageReactionAdd
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user that applied the guild or reaction emoji
*/
this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user);
return { message, reaction, user };
}
}
module.exports = MessageReactionAdd;

View File

@ -0,0 +1,45 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id',
guild_id: 'id' }
*/
class MessageReactionRemove extends Action {
handle(data) {
if (!data.emoji) return false;
const user = this.getUser(data);
if (!user) return false;
// Verify channel
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
// Verify reaction
const reaction = 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 };
}
}
module.exports = MessageReactionRemove;

View File

@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class MessageReactionRemoveAll extends Action {
handle(data) {
// 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;
message.reactions.cache.clear();
this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message);
return { message };
}
}
/**
* Emitted whenever all reactions are removed from a cached message.
* @event Client#messageReactionRemoveAll
* @param {Message} message The message the reactions were removed from
*/
module.exports = MessageReactionRemoveAll;

View 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;

View File

@ -0,0 +1,24 @@
'use strict';
const Action = require('./Action');
class MessageUpdateAction extends Action {
handle(data) {
const channel = this.getChannel(data);
if (channel) {
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) {
const old = message.patch(data);
return {
old,
updated: message,
};
}
}
return {};
}
}
module.exports = MessageUpdateAction;

View File

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

View File

@ -0,0 +1,58 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
const textBasedChannelTypes = ['dm', 'text', 'news'];
class TypingStart extends Action {
handle(data) {
const channel = this.getChannel(data);
if (!channel) {
return;
}
if (!textBasedChannelTypes.includes(channel.type)) {
this.client.emit(Events.WARN, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`);
return;
}
const user = this.getUserFromMember(data);
const timestamp = new Date(data.timestamp * 1000);
if (channel && user) {
if (channel._typing.has(user.id)) {
const typing = channel._typing.get(user.id);
typing.lastTimestamp = timestamp;
typing.elapsedTime = Date.now() - typing.since;
this.client.clearTimeout(typing.timeout);
typing.timeout = this.tooLate(channel, user);
} else {
const since = new Date();
const lastTimestamp = new Date();
channel._typing.set(user.id, {
user,
since,
lastTimestamp,
elapsedTime: Date.now() - since,
timeout: this.tooLate(channel, user),
});
/**
* Emitted whenever a user starts typing in a channel.
* @event Client#typingStart
* @param {Channel} channel The channel the user started typing in
* @param {User} user The user that started typing
*/
this.client.emit(Events.TYPING_START, channel, user);
}
}
}
tooLate(channel, user) {
return channel.client.setTimeout(() => {
channel._typing.delete(user.id);
}, 10000);
}
}
module.exports = TypingStart;

View File

@ -0,0 +1,35 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class UserUpdateAction extends Action {
handle(data) {
const client = this.client;
const newUser = client.users.cache.get(data.id);
const oldUser = newUser._update(data);
if (!oldUser.equals(newUser)) {
/**
* Emitted whenever a user's details (e.g. username) are changed.
* Triggered by the Discord gateway events USER_UPDATE, GUILD_MEMBER_UPDATE, and PRESENCE_UPDATE.
* @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: newUser,
};
}
return {
old: null,
updated: null,
};
}
}
module.exports = UserUpdateAction;

View File

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

View File

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

View File

@ -0,0 +1,110 @@
'use strict';
const VoiceBroadcast = require('./VoiceBroadcast');
const VoiceConnection = require('./VoiceConnection');
const { Error } = require('../../errors');
const Collection = require('../../util/Collection');
/**
* Manages voice connections for the client
*/
class ClientVoiceManager {
constructor(client) {
/**
* The client that instantiated this voice manager
* @type {Client}
* @readonly
* @name ClientVoiceManager#client
*/
Object.defineProperty(this, 'client', { value: client });
/**
* A collection mapping connection IDs to the Connection objects
* @type {Collection<Snowflake, VoiceConnection>}
*/
this.connections = new Collection();
/**
* Active voice broadcasts that have been created
* @type {VoiceBroadcast[]}
*/
this.broadcasts = [];
}
/**
* Creates a voice broadcast.
* @returns {VoiceBroadcast}
*/
createBroadcast() {
const broadcast = new VoiceBroadcast(this.client);
this.broadcasts.push(broadcast);
return broadcast;
}
onVoiceServer({ guild_id, token, endpoint }) {
this.client.emit('debug', `[VOICE] voiceServer guild: ${guild_id} token: ${token} endpoint: ${endpoint}`);
const connection = this.connections.get(guild_id);
if (connection) connection.setTokenAndEndpoint(token, endpoint);
}
onVoiceStateUpdate({ guild_id, session_id, channel_id }) {
const connection = this.connections.get(guild_id);
this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`);
if (!connection) return;
if (!channel_id) {
connection._disconnect();
this.connections.delete(guild_id);
return;
}
connection.channel = this.client.channels.cache.get(channel_id);
connection.setSessionID(session_id);
}
/**
* Sets up a request to join a voice channel.
* @param {VoiceChannel} channel The voice channel to join
* @returns {Promise<VoiceConnection>}
* @private
*/
joinChannel(channel) {
return new Promise((resolve, reject) => {
if (!channel.joinable) {
throw new Error('VOICE_JOIN_CHANNEL', channel.full);
}
let connection = this.connections.get(channel.guild.id);
if (connection) {
if (connection.channel.id !== channel.id) {
this.connections.get(channel.guild.id).updateChannel(channel);
}
resolve(connection);
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);
}
connection.once('failed', reason => {
this.connections.delete(channel.guild.id);
reject(reason);
});
connection.on('error', reject);
connection.once('authenticated', () => {
connection.once('ready', () => {
resolve(connection);
connection.removeListener('error', reject);
});
connection.once('disconnect', () => this.connections.delete(channel.guild.id));
});
});
}
}
module.exports = ClientVoiceManager;

View File

@ -0,0 +1,111 @@
'use strict';
const EventEmitter = require('events');
const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer');
const PlayInterface = require('./util/PlayInterface');
const { Events } = require('../../util/Constants');
/**
* A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency.
*
* Example usage:
* ```js
* const broadcast = client.voice.createBroadcast();
* broadcast.play('./music.mp3');
* // Play "music.mp3" in all voice connections that the client is in
* for (const connection of client.voice.connections.values()) {
* connection.play(broadcast);
* }
* ```
* @implements {PlayInterface}
* @extends {EventEmitter}
*/
class VoiceBroadcast extends EventEmitter {
constructor(client) {
super();
/**
* The client that created the broadcast
* @type {Client}
*/
this.client = client;
/**
* 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;
}
/**
* Play an audio resource.
* @param {ReadableStream|string} resource The resource to play.
* @param {StreamOptions} [options] The options to play.
* @example
* // Play a local audio file
* broadcast.play('/home/hydrabolt/audio.mp3', { volume: 0.5 });
* @example
* // Play a ReadableStream
* broadcast.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' }));
* @example
* // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html
* broadcast.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
* @returns {BroadcastDispatcher}
*/
play() {
return null;
}
/**
* 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);
module.exports = VoiceBroadcast;

View File

@ -0,0 +1,526 @@
'use strict';
const EventEmitter = require('events');
const VoiceUDP = require('./networking/VoiceUDPClient');
const VoiceWebSocket = require('./networking/VoiceWebSocket');
const AudioPlayer = require('./player/AudioPlayer');
const VoiceReceiver = require('./receiver/Receiver');
const PlayInterface = require('./util/PlayInterface');
const Silence = require('./util/Silence');
const { Error } = require('../../errors');
const { OPCodes, VoiceOPCodes, VoiceStatus, Events } = require('../../util/Constants');
const Speaking = require('../../util/Speaking');
const Util = require('../../util/Util');
// Workaround for Discord now requiring silence to be sent before being able to receive audio
class SingleSilence extends Silence {
_read() {
super._read();
this.push(null);
}
}
const SUPPORTED_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'];
/**
* Represents a connection to a guild's voice server.
* ```js
* // Obtained using:
* voiceChannel.join()
* .then(connection => {
*
* });
* ```
* @extends {EventEmitter}
* @implements {PlayInterface}
*/
class VoiceConnection extends EventEmitter {
constructor(voiceManager, channel) {
super();
/**
* The voice manager that instantiated this connection
* @type {ClientVoiceManager}
*/
this.voiceManager = voiceManager;
/**
* The voice channel this connection is currently serving
* @type {VoiceChannel}
*/
this.channel = channel;
/**
* The current status of the voice connection
* @type {VoiceStatus}
*/
this.status = VoiceStatus.AUTHENTICATING;
/**
* Our current speaking state
* @type {Readonly<Speaking>}
*/
this.speaking = new Speaking().freeze();
/**
* The authentication data needed to connect to the voice server
* @type {Object}
* @private
*/
this.authentication = {};
/**
* The audio player for this voice connection
* @type {AudioPlayer}
*/
this.player = new AudioPlayer(this);
this.player.on('debug', m => {
/**
* Debug info from the connection.
* @event VoiceConnection#debug
* @param {string} message The debug message
*/
this.emit('debug', `audio player - ${m}`);
});
this.player.on('error', e => {
/**
* Warning info from the connection.
* @event VoiceConnection#warn
* @param {string|Error} warning The warning
*/
this.emit('warn', e);
});
this.once('closing', () => this.player.destroy());
/**
* Map SSRC values to user IDs
* @type {Map<number, Snowflake>}
* @private
*/
this.ssrcMap = new Map();
/**
* Tracks which users are talking
* @type {Map<Snowflake, Readonly<Speaking>>}
* @private
*/
this._speaking = new Map();
/**
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
* @type {Object}
* @private
*/
this.sockets = {};
/**
* The voice receiver of this connection
* @type {VoiceReceiver}
*/
this.receiver = new VoiceReceiver(this);
}
/**
* The client that instantiated this connection
* @type {Client}
* @readonly
*/
get client() {
return this.voiceManager.client;
}
/**
* The current stream dispatcher (if any)
* @type {?StreamDispatcher}
* @readonly
*/
get dispatcher() {
return this.player.dispatcher;
}
/**
* Sets whether the voice connection should display as "speaking", "soundshare" or "none".
* @param {BitFieldResolvable} value The new speaking state
*/
setSpeaking(value) {
if (this.speaking.equals(value)) return;
if (this.status !== VoiceStatus.CONNECTED) return;
this.speaking = new Speaking(value).freeze();
this.sockets.ws
.sendPacket({
op: VoiceOPCodes.SPEAKING,
d: {
speaking: this.speaking.bitfield,
delay: 0,
ssrc: this.authentication.ssrc,
},
})
.catch(e => {
this.emit('debug', e);
});
}
/**
* The voice state of this connection
* @type {?VoiceState}
*/
get voice() {
return this.channel.guild.voice;
}
/**
* Sends a request to the main gateway to join a voice channel.
* @param {Object} [options] The options to provide
* @returns {Promise<Shard>}
* @private
*/
sendVoiceStateUpdate(options = {}) {
options = Util.mergeDefault(
{
guild_id: this.channel.guild.id,
channel_id: this.channel.id,
self_mute: this.voice ? this.voice.selfMute : false,
self_deaf: this.voice ? this.voice.selfDeaf : false,
},
options,
);
this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`);
return this.channel.guild.shard.send(
{
op: OPCodes.VOICE_STATE_UPDATE,
d: options,
},
true,
);
}
/**
* Set the token and endpoint required to connect to the voice servers.
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @returns {void}
* @private
*/
setTokenAndEndpoint(token, endpoint) {
this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`);
if (!endpoint) {
// Signifies awaiting endpoint stage
return;
}
if (!token) {
this.authenticateFailed('VOICE_TOKEN_ABSENT');
return;
}
endpoint = endpoint.match(/([^:]*)/)[0];
this.emit('debug', `Endpoint resolved as ${endpoint}`);
if (!endpoint) {
this.authenticateFailed('VOICE_INVALID_ENDPOINT');
return;
}
if (this.status === VoiceStatus.AUTHENTICATING) {
this.authentication.token = token;
this.authentication.endpoint = endpoint;
this.checkAuthenticated();
} else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) {
this.reconnect(token, endpoint);
}
}
/**
* Sets the Session ID for the connection.
* @param {string} sessionID The voice session ID
* @private
*/
setSessionID(sessionID) {
this.emit('debug', `Setting sessionID ${sessionID} (stored as "${this.authentication.sessionID}")`);
if (!sessionID) {
this.authenticateFailed('VOICE_SESSION_ABSENT');
return;
}
if (this.status === VoiceStatus.AUTHENTICATING) {
this.authentication.sessionID = sessionID;
this.checkAuthenticated();
} else if (sessionID !== this.authentication.sessionID) {
this.authentication.sessionID = sessionID;
/**
* Emitted when a new session ID is received.
* @event VoiceConnection#newSession
* @private
*/
this.emit('newSession', sessionID);
}
}
/**
* Checks whether the voice connection is authenticated.
* @private
*/
checkAuthenticated() {
const { token, endpoint, sessionID } = this.authentication;
this.emit('debug', `Authenticated with sessionID ${sessionID}`);
if (token && endpoint && sessionID) {
this.status = VoiceStatus.CONNECTING;
/**
* Emitted when we successfully initiate a voice connection.
* @event VoiceConnection#authenticated
*/
this.emit('authenticated');
this.connect();
}
}
/**
* Invoked when we fail to initiate a voice connection.
* @param {string} reason The reason for failure
* @private
*/
authenticateFailed(reason) {
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.
* @event VoiceConnection#failed
* @param {Error} error The encountered error
*/
this.emit('failed', new Error(reason));
} else {
/**
* Emitted whenever the connection encounters an error.
* @event VoiceConnection#error
* @param {Error} error The encountered error
*/
this.emit('error', new Error(reason));
}
this.status = VoiceStatus.DISCONNECTED;
}
/**
* Move to a different voice channel in the same guild.
* @param {VoiceChannel} channel The channel to move to
* @private
*/
updateChannel(channel) {
this.channel = channel;
this.sendVoiceStateUpdate();
}
/**
* Attempts to authenticate to the voice server.
* @private
*/
authenticate() {
this.sendVoiceStateUpdate();
this.connectTimeout = this.client.setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15000);
}
/**
* Attempts to reconnect to the voice server (typically after a region change).
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @private
*/
reconnect(token, endpoint) {
this.authentication.token = token;
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
*/
this.emit('reconnecting');
this.connect();
}
/**
* Disconnects the voice connection, causing a disconnect and closing event to be emitted.
*/
disconnect() {
this.emit('closing');
this.emit('debug', 'disconnect() triggered');
this.client.clearTimeout(this.connectTimeout);
const conn = this.voiceManager.connections.get(this.channel.guild.id);
if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id);
this.sendVoiceStateUpdate({
channel_id: null,
});
this._disconnect();
}
/**
* Internally disconnects (doesn't send disconnect packet).
* @private
*/
_disconnect() {
this.cleanup();
this.status = VoiceStatus.DISCONNECTED;
/**
* Emitted when the voice connection disconnects.
* @event VoiceConnection#disconnect
*/
this.emit('disconnect');
}
/**
* Cleans up after disconnect.
* @private
*/
cleanup() {
this.player.destroy();
this.speaking = new Speaking().freeze();
const { ws, udp } = this.sockets;
this.emit('debug', 'Connection clean up');
if (ws) {
ws.removeAllListeners('error');
ws.removeAllListeners('ready');
ws.removeAllListeners('sessionDescription');
ws.removeAllListeners('speaking');
ws.shutdown();
}
if (udp) udp.removeAllListeners('error');
this.sockets.ws = null;
this.sockets.udp = null;
}
/**
* Connect the voice connection.
* @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');
}
if (this.sockets.ws) this.sockets.ws.shutdown();
if (this.sockets.udp) this.sockets.udp.shutdown();
this.sockets.ws = new VoiceWebSocket(this);
this.sockets.udp = new VoiceUDP(this);
const { ws, udp } = this.sockets;
ws.on('debug', msg => this.emit('debug', msg));
udp.on('debug', msg => this.emit('debug', msg));
ws.on('error', err => this.emit('error', err));
udp.on('error', err => this.emit('error', err));
ws.on('ready', this.onReady.bind(this));
ws.on('sessionDescription', this.onSessionDescription.bind(this));
ws.on('startSpeaking', this.onStartSpeaking.bind(this));
this.sockets.ws.connect();
}
/**
* Invoked when the voice websocket is ready.
* @param {Object} data The received data
* @private
*/
onReady(data) {
Object.assign(this.authentication, data);
for (let mode of data.modes) {
if (SUPPORTED_MODES.includes(mode)) {
this.authentication.mode = mode;
this.emit('debug', `Selecting the ${mode} mode`);
break;
}
}
this.sockets.udp.createUDPSocket(data.ip);
}
/**
* Invoked when a session description is received.
* @param {Object} data The received data
* @private
*/
onSessionDescription(data) {
Object.assign(this.authentication, data);
this.status = VoiceStatus.CONNECTED;
const ready = () => {
this.client.clearTimeout(this.connectTimeout);
this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`);
/**
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
* the connection will already be ready.
* @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', volume: false });
dispatcher.once('finish', ready);
}
}
onStartSpeaking({ user_id, ssrc, speaking }) {
this.ssrcMap.set(+ssrc, {
...(this.ssrcMap.get(+ssrc) || {}),
userID: user_id,
speaking: speaking,
});
}
/**
* Invoked when a speaking event is received.
* @param {Object} data The received data
* @private
*/
onSpeaking({ user_id, speaking }) {
speaking = new Speaking(speaking).freeze();
const guild = this.channel.guild;
const user = this.client.users.cache.get(user_id);
const old = this._speaking.get(user_id);
this._speaking.set(user_id, speaking);
/**
* Emitted whenever a user changes speaking state.
* @event VoiceConnection#speaking
* @param {User} user The user that has changed speaking state
* @param {Readonly<Speaking>} speaking The speaking state of the user
*/
if (this.status === VoiceStatus.CONNECTED) {
this.emit('speaking', user, speaking);
if (!speaking.has(Speaking.FLAGS.SPEAKING)) {
this.receiver.packets._stoppedSpeaking(user_id);
}
}
if (guild && user && !speaking.equals(old)) {
const member = guild.member(user);
if (member) {
/**
* Emitted once a guild member changes speaking state.
* @event Client#guildMemberSpeaking
* @param {GuildMember} member The member that started/stopped speaking
* @param {Readonly<Speaking>} speaking The speaking state of the member
*/
this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking);
}
}
}
play() {} // eslint-disable-line no-empty-function
}
PlayInterface.applyToClass(VoiceConnection);
module.exports = VoiceConnection;

View File

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

View File

@ -0,0 +1,354 @@
'use strict';
const { Writable } = require('stream');
const secretbox = require('../util/Secretbox');
const Silence = require('../util/Silence');
const VolumeInterface = require('../util/VolumeInterface');
const FRAME_LENGTH = 20;
const CHANNELS = 2;
const TIMESTAMP_INC = (48000 / 100) * CHANNELS;
const MAX_NONCE_SIZE = 2 ** 32 - 1;
const nonce = Buffer.alloc(24);
/**
* @external WritableStream
* @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable}
*/
/**
* The class that sends voice packet data to the voice connection.
* ```js
* // Obtained using:
* voiceChannel.join().then(connection => {
* // You can play a file or a stream here:
* const dispatcher = connection.play('/home/hydrabolt/audio.mp3');
* });
* ```
* @implements {VolumeInterface}
* @extends {WritableStream}
*/
class StreamDispatcher extends Writable {
constructor(player, { seek = 0, volume = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, streams) {
const streamOptions = { seek, volume, fec, plp, bitrate, highWaterMark };
super(streamOptions);
/**
* The Audio Player that controls this dispatcher
* @type {AudioPlayer}
*/
this.player = player;
this.streamOptions = streamOptions;
this.streams = streams;
this.streams.silence = new Silence();
this._nonce = 0;
this._nonceBuffer = Buffer.alloc(24);
/**
* The time that the stream was paused at (null if not paused)
* @type {?number}
*/
this.pausedSince = null;
this._writeCallback = null;
/**
* The broadcast controlling this dispatcher, if any
* @type {?VoiceBroadcast}
*/
this.broadcast = this.streams.broadcast || null;
this._pausedTime = 0;
this._silentPausedTime = 0;
this.count = 0;
this.on('finish', () => {
this._cleanup();
this._setSpeaking(0);
});
this.setVolume(volume);
this.setBitrate(bitrate);
if (typeof fec !== 'undefined') this.setFEC(fec);
if (typeof plp !== 'undefined') this.setPLP(plp);
const streamError = (type, err) => {
/**
* Emitted when the dispatcher encounters an error.
* @event StreamDispatcher#error
*/
if (type && err) {
err.message = `${type} stream: ${err.message}`;
this.emit(this.player.dispatcher === this ? 'error' : 'debug', err);
}
this.destroy();
};
this.on('error', () => streamError());
if (this.streams.input) this.streams.input.on('error', err => streamError('input', err));
if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', err => streamError('ffmpeg', err));
if (this.streams.opus) this.streams.opus.on('error', err => streamError('opus', err));
if (this.streams.volume) this.streams.volume.on('error', err => streamError('volume', err));
}
get _sdata() {
return this.player.streamingData;
}
_write(chunk, enc, done) {
if (!this.startTime) {
/**
* Emitted once the stream has started to play.
* @event StreamDispatcher#start
*/
this.emit('start');
this.startTime = Date.now();
}
this._playChunk(chunk);
this._step(done);
}
_destroy(err, cb) {
this._cleanup();
super._destroy(err, cb);
}
_cleanup() {
if (this.player.dispatcher === this) this.player.dispatcher = null;
const { streams } = this;
if (streams.broadcast) streams.broadcast.delete(this);
if (streams.opus) streams.opus.destroy();
if (streams.ffmpeg) streams.ffmpeg.destroy();
}
/**
* Pauses playback
* @param {boolean} [silence=false] Whether to play silence while paused to prevent audio glitches
*/
pause(silence = false) {
if (this.paused) return;
if (this.streams.opus) this.streams.opus.unpipe(this);
if (silence) {
this.streams.silence.pipe(this);
this._silence = true;
} else {
this._setSpeaking(0);
}
this.pausedSince = Date.now();
}
/**
* Whether or not playback is paused
* @type {boolean}
* @readonly
*/
get paused() {
return Boolean(this.pausedSince);
}
/**
* Total time that this dispatcher has been paused in milliseconds
* @type {number}
* @readonly
*/
get pausedTime() {
return this._silentPausedTime + this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0);
}
/**
* Resumes playback
*/
resume() {
if (!this.pausedSince) return;
this.streams.silence.unpipe(this);
if (this.streams.opus) this.streams.opus.pipe(this);
if (this._silence) {
this._silentPausedTime += Date.now() - this.pausedSince;
this._silence = false;
} else {
this._pausedTime += Date.now() - this.pausedSince;
}
this.pausedSince = null;
if (typeof this._writeCallback === 'function') this._writeCallback();
}
/**
* The time (in milliseconds) that the dispatcher has actually been playing audio for
* @type {number}
* @readonly
*/
get streamTime() {
return this.count * FRAME_LENGTH;
}
/**
* The time (in milliseconds) that the dispatcher has been playing audio for, taking into account skips and pauses
* @type {number}
* @readonly
*/
get totalStreamTime() {
return Date.now() - this.startTime;
}
/**
* Set the bitrate of the current Opus encoder if using a compatible Opus stream.
* @param {number} value New bitrate, in kbps
* If set to 'auto', the voice channel's bitrate will be used
* @returns {boolean} true if the bitrate has been successfully changed.
*/
setBitrate(value) {
if (!value || !this.bitrateEditable) return false;
const bitrate = value === 'auto' ? this.player.voiceConnection.channel.bitrate : value;
this.streams.opus.setBitrate(bitrate * 1000);
return true;
}
/**
* Sets the expected packet loss percentage if using a compatible Opus stream.
* @param {number} value between 0 and 1
* @returns {boolean} Returns true if it was successfully set.
*/
setPLP(value) {
if (!this.bitrateEditable) return false;
this.streams.opus.setPLP(value);
return true;
}
/**
* Enables or disables forward error correction if using a compatible Opus stream.
* @param {boolean} enabled true to enable
* @returns {boolean} Returns true if it was successfully set.
*/
setFEC(enabled) {
if (!this.bitrateEditable) return false;
this.streams.opus.setFEC(enabled);
return true;
}
_step(done) {
this._writeCallback = () => {
this._writeCallback = null;
done();
};
if (!this.streams.broadcast) {
const next = FRAME_LENGTH + this.count * FRAME_LENGTH - (Date.now() - this.startTime - this._pausedTime);
setTimeout(() => {
if ((!this.pausedSince || this._silence) && this._writeCallback) this._writeCallback();
}, next);
}
this._sdata.sequence++;
this._sdata.timestamp += TIMESTAMP_INC;
if (this._sdata.sequence >= 2 ** 16) this._sdata.sequence = 0;
if (this._sdata.timestamp >= 2 ** 32) this._sdata.timestamp = 0;
this.count++;
}
_final(callback) {
this._writeCallback = null;
callback();
}
_playChunk(chunk) {
if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secret_key) return;
this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk));
}
_encrypt(buffer) {
const { secret_key, mode } = this.player.voiceConnection.authentication;
if (mode === 'xsalsa20_poly1305_lite') {
this._nonce++;
if (this._nonce > MAX_NONCE_SIZE) this._nonce = 0;
this._nonceBuffer.writeUInt32BE(this._nonce, 0);
return [secretbox.methods.close(buffer, this._nonceBuffer, secret_key), this._nonceBuffer.slice(0, 4)];
} else if (mode === 'xsalsa20_poly1305_suffix') {
const random = secretbox.methods.random(24);
return [secretbox.methods.close(buffer, random, secret_key), random];
} else {
return [secretbox.methods.close(buffer, nonce, secret_key)];
}
}
_createPacket(sequence, timestamp, buffer) {
const packetBuffer = Buffer.alloc(12);
packetBuffer[0] = 0x80;
packetBuffer[1] = 0x78;
packetBuffer.writeUIntBE(sequence, 2, 2);
packetBuffer.writeUIntBE(timestamp, 4, 4);
packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4);
packetBuffer.copy(nonce, 0, 0, 12);
return Buffer.concat([packetBuffer, ...this._encrypt(buffer)]);
}
_sendPacket(packet) {
/**
* Emitted whenever the dispatcher has debug information.
* @event StreamDispatcher#debug
* @param {string} info The debug info
*/
this._setSpeaking(1);
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) {
if (typeof this.player.voiceConnection !== 'undefined') {
this.player.voiceConnection.setSpeaking(value);
}
/**
* Emitted when the dispatcher starts/stops speaking.
* @event StreamDispatcher#speaking
* @param {boolean} value Whether or not the dispatcher is speaking
*/
this.emit('speaking', value);
}
get volumeEditable() {
return Boolean(this.streams.volume);
}
/**
* Whether or not the Opus bitrate of this stream is editable
* @type {boolean}
* @readonly
*/
get bitrateEditable() {
return this.streams.opus && this.streams.opus.setBitrate;
}
// Volume
get volume() {
return this.streams.volume ? this.streams.volume.volume : 1;
}
setVolume(value) {
if (!this.streams.volume) return false;
/**
* Emitted when the volume of this dispatcher changes.
* @event StreamDispatcher#volumeChange
* @param {number} oldVolume The old volume of this dispatcher
* @param {number} newVolume The new volume of this dispatcher
*/
this.emit('volumeChange', this.volume, value);
this.streams.volume.setVolume(value);
return true;
}
// Volume stubs for docs
/* eslint-disable no-empty-function*/
get volumeDecibels() {}
get volumeLogarithmic() {}
setVolumeDecibels() {}
setVolumeLogarithmic() {}
}
VolumeInterface.applyToClass(StreamDispatcher);
module.exports = StreamDispatcher;

View File

@ -0,0 +1,154 @@
'use strict';
const udp = require('dgram');
const EventEmitter = require('events');
const { Error } = require('../../../errors');
const { VoiceOPCodes } = require('../../../util/Constants');
/**
* Represents a UDP client for a Voice Connection.
* @extends {EventEmitter}
* @private
*/
class VoiceConnectionUDPClient extends EventEmitter {
constructor(voiceConnection) {
super();
/**
* The voice connection that this UDP client serves
* @type {VoiceConnection}
*/
this.voiceConnection = voiceConnection;
/**
* The UDP socket
* @type {?Socket}
*/
this.socket = null;
/**
* The address of the Discord voice server
* @type {?string}
*/
this.discordAddress = null;
/**
* The local IP address
* @type {?string}
*/
this.localAddress = null;
/**
* The local port
* @type {?string}
*/
this.localPort = null;
this.voiceConnection.on('closing', this.shutdown.bind(this));
}
shutdown() {
this.emit('debug', `[UDP] shutdown requested`);
if (this.socket) {
this.socket.removeAllListeners('message');
try {
this.socket.close();
} finally {
this.socket = null;
}
}
}
/**
* The port of the Discord voice server
* @type {number}
* @readonly
*/
get discordPort() {
return this.voiceConnection.authentication.port;
}
/**
* Send a packet to the UDP client.
* @param {Object} packet The packet to send
* @returns {Promise<Object>}
*/
send(packet) {
return new Promise((resolve, reject) => {
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) {
this.emit('debug', `[UDP] >> ERROR: ${error}`);
reject(error);
} else {
resolve(packet);
}
});
});
}
async createUDPSocket(address) {
this.discordAddress = address;
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;
}
this.localAddress = packet.address;
this.localPort = packet.port;
this.voiceConnection.sockets.ws.sendPacket({
op: VoiceOPCodes.SELECT_PROTOCOL,
d: {
protocol: 'udp',
data: {
address: packet.address,
port: packet.port,
mode: this.voiceConnection.authentication.mode,
},
},
});
this.emit('debug', `[UDP] << ${JSON.stringify(packet)}`);
socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer));
});
const blankMessage = Buffer.alloc(70);
blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4);
this.emit('debug', `Sending IP discovery packet: [${[...blankMessage]}]`);
await this.send(blankMessage);
this.emit('debug', `Successfully sent IP discovery packet`);
}
}
function parseLocalPacket(message) {
try {
const packet = Buffer.from(message);
let address = '';
for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]);
const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
return { address, port };
} catch (error) {
return { error };
}
}
module.exports = VoiceConnectionUDPClient;

View File

@ -0,0 +1,268 @@
'use strict';
const EventEmitter = require('events');
const WebSocket = require('../../../WebSocket');
const { Error } = require('../../../errors');
const { OPCodes, VoiceOPCodes } = require('../../../util/Constants');
/**
* Represents a Voice Connection's WebSocket.
* @extends {EventEmitter}
* @private
*/
class VoiceWebSocket extends EventEmitter {
constructor(connection) {
super();
/**
* The Voice Connection that this WebSocket serves
* @type {VoiceConnection}
*/
this.connection = connection;
/**
* How many connection attempts have been made
* @type {number}
*/
this.attempts = 0;
this.dead = false;
this.connection.on('closing', this.shutdown.bind(this));
}
/**
* The client of this voice WebSocket
* @type {Client}
* @readonly
*/
get client() {
return this.connection.client;
}
shutdown() {
this.emit('debug', `[WS] shutdown requested`);
this.dead = true;
this.reset();
}
/**
* 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;
}
this.clearHeartbeat();
}
/**
* Starts connecting to the Voice WebSocket Server.
*/
connect() {
this.emit('debug', `[WS] connect requested`);
if (this.dead) return;
if (this.ws) this.reset();
if (this.attempts >= 5) {
this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
return;
}
this.attempts++;
/**
* The actual WebSocket used to connect to the Voice WebSocket Server.
* @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);
this.ws.onerror = this.onError.bind(this);
}
/**
* Sends data to the WebSocket if it is open.
* @param {string} data The data to send to the WebSocket
* @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);
});
});
}
/**
* JSON.stringify's a packet and then sends it to the WebSocket Server.
* @param {Object} packet The packet to send
* @returns {Promise<string>}
*/
sendPacket(packet) {
try {
packet = JSON.stringify(packet);
} catch (error) {
return Promise.reject(error);
}
return this.send(packet);
}
/**
* Called whenever the WebSocket opens.
*/
onOpen() {
this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`);
this.sendPacket({
op: OPCodes.DISPATCH,
d: {
server_id: this.connection.channel.guild.id,
user_id: this.client.user.id,
token: this.connection.authentication.token,
session_id: this.connection.authentication.sessionID,
},
}).catch(() => {
this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
});
}
/**
* Called whenever a message is received from the WebSocket.
* @param {MessageEvent} event The message event that was received
* @returns {void}
*/
onMessage(event) {
try {
return this.onPacket(WebSocket.unpack(event.data, 'json'));
} catch (error) {
return this.onError(error);
}
}
/**
* 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);
}
/**
* Called whenever an error occurs with the WebSocket.
* @param {Error} error The error that occurred
*/
onError(error) {
this.emit('debug', `[WS] Error: ${error}`);
this.emit('error', error);
}
/**
* Called whenever a valid packet is received from the WebSocket.
* @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);
break;
case VoiceOPCodes.READY:
/**
* Emitted once the voice WebSocket receives the ready packet.
* @param {Object} packet The received packet
* @event VoiceWebSocket#ready
*/
this.emit('ready', packet.d);
break;
/* eslint-disable no-case-declarations */
case VoiceOPCodes.SESSION_DESCRIPTION:
packet.d.secret_key = new Uint8Array(packet.d.secret_key);
/**
* Emitted once the Voice Websocket receives a description of this voice session.
* @param {Object} packet The received packet
* @event VoiceWebSocket#sessionDescription
*/
this.emit('sessionDescription', packet.d);
break;
case VoiceOPCodes.CLIENT_CONNECT:
this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
userID: packet.d.user_id,
speaking: 0,
hasVideo: Boolean(packet.d.video_ssrc),
});
break;
case VoiceOPCodes.CLIENT_DISCONNECT:
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);
if (streamInfo) {
this.connection.receiver.packets.streams.delete(packet.d.user_id);
streamInfo.stream.push(null);
}
break;
case VoiceOPCodes.SPEAKING:
/**
* Emitted whenever a speaking packet is received.
* @param {Object} data
* @event VoiceWebSocket#startSpeaking
*/
this.emit('startSpeaking', packet.d);
break;
default:
/**
* Emitted when an unhandled packet is received.
* @param {Object} packet
* @event VoiceWebSocket#unknownPacket
*/
this.emit('unknownPacket', packet);
break;
}
}
/**
* Sets an interval at which to send a heartbeat packet to the WebSocket.
* @param {number} interval The interval at which to send a heartbeat packet
*/
setHeartbeat(interval) {
if (!interval || isNaN(interval)) {
this.onError(new Error('VOICE_INVALID_HEARTBEAT'));
return;
}
if (this.heartbeatInterval) {
/**
* Emitted whenever the voice WebSocket encounters a non-fatal error.
* @param {string} warn The warning
* @event VoiceWebSocket#warn
*/
this.emit('warn', 'A voice heartbeat interval is being overwritten');
this.client.clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval);
}
/**
* Clears a heartbeat interval, if one exists.
*/
clearHeartbeat() {
if (!this.heartbeatInterval) {
this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
return;
}
this.client.clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
/**
* Sends a heartbeat packet.
*/
sendHeartbeat() {
this.sendPacket({ op: VoiceOPCodes.HEARTBEAT, d: Math.floor(Math.random() * 10e10) }).catch(() => {
this.emit('warn', 'Tried to send heartbeat, but connection is not open');
this.clearHeartbeat();
});
}
}
module.exports = VoiceWebSocket;

View File

@ -0,0 +1,27 @@
'use strict';
const BasePlayer = require('./BasePlayer');
/**
* An Audio Player for a Voice Connection.
* @private
* @extends {BasePlayer}
*/
class AudioPlayer extends BasePlayer {
constructor(voiceConnection) {
super();
/**
* The voice connection that the player serves
* @type {VoiceConnection}
*/
this.voiceConnection = voiceConnection;
}
playBroadcast(broadcast, options) {
const dispatcher = this.createDispatcher(options, { broadcast });
broadcast.add(dispatcher);
return dispatcher;
}
}
module.exports = AudioPlayer;

View File

@ -0,0 +1,92 @@
'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'];
/**
* An Audio Player for a Voice Connection.
* @private
* @extends {EventEmitter}
*/
class BasePlayer extends EventEmitter {
constructor() {
super();
this.dispatcher = null;
this.streamingData = {
channels: 2,
sequence: 0,
timestamp: 0,
};
}
destroy() {
this.destroyDispatcher();
}
destroyDispatcher() {
if (this.dispatcher) {
this.dispatcher.destroy();
this.dispatcher = null;
}
}
playUnknown(input, options) {
this.destroyDispatcher();
const isStream = input instanceof ReadableStream;
const args = isStream ? FFMPEG_ARGUMENTS.slice() : ['-i', input, ...FFMPEG_ARGUMENTS];
if (options.seek) args.unshift('-ss', String(options.seek));
const ffmpeg = new prism.FFmpeg({ args });
const streams = { ffmpeg };
if (isStream) {
streams.input = input;
input.pipe(ffmpeg);
}
return this.playPCMStream(ffmpeg, options, streams);
}
playPCMStream(stream, options, streams = {}) {
this.destroyDispatcher();
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);
}
streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 });
stream.pipe(streams.volume).pipe(opus);
return this.playOpusStream(opus, options, streams);
}
playOpusStream(stream, options, streams = {}) {
this.destroyDispatcher();
streams.opus = stream;
if (options.volume !== false && !streams.input) {
streams.input = stream;
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 });
streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 });
streams.opus = stream
.pipe(decoder)
.pipe(streams.volume)
.pipe(new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }));
}
const dispatcher = this.createDispatcher(options, streams);
streams.opus.pipe(dispatcher);
return dispatcher;
}
createDispatcher(options, streams, broadcast) {
this.destroyDispatcher();
const dispatcher = (this.dispatcher = new StreamDispatcher(this, options, streams, broadcast));
return dispatcher;
}
}
module.exports = BasePlayer;

View File

@ -0,0 +1,28 @@
'use strict';
const BasePlayer = require('./BasePlayer');
const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher');
/**
* An Audio Player for a Voice Connection.
* @private
* @extends {BasePlayer}
*/
class AudioPlayer extends BasePlayer {
constructor(broadcast) {
super();
/**
* The broadcast that the player serves
* @type {VoiceBroadcast}
*/
this.broadcast = broadcast;
}
createDispatcher(options, streams) {
this.destroyDispatcher();
const dispatcher = (this.dispatcher = new BroadcastDispatcher(this, options, streams));
return dispatcher;
}
}
module.exports = AudioPlayer;

View File

@ -0,0 +1,143 @@
'use strict';
const EventEmitter = require('events');
const Speaking = require('../../../util/Speaking');
const secretbox = require('../util/Secretbox');
const { SILENCE_FRAME } = require('../util/Silence');
// 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) {
super();
this.nonce = Buffer.alloc(24);
this.receiver = receiver;
this.streams = new Map();
this.speakingTimeouts = new Map();
}
get connection() {
return this.receiver.connection;
}
_stoppedSpeaking(userID) {
const streamInfo = this.streams.get(userID);
if (streamInfo && streamInfo.end === 'silence') {
this.streams.delete(userID);
streamInfo.stream.push(null);
}
}
makeStream(user, end) {
if (this.streams.has(user)) return this.streams.get(user).stream;
const stream = new Readable();
stream.on('end', () => this.streams.delete(user));
this.streams.set(user, { stream, end });
return stream;
}
parseBuffer(buffer) {
const { secret_key, mode } = this.receiver.connection.authentication;
// Choose correct nonce depending on encryption
let end;
if (mode === 'xsalsa20_poly1305_lite') {
buffer.copy(this.nonce, 0, buffer.length - 4);
end = buffer.length - 4;
} else if (mode === 'xsalsa20_poly1305_suffix') {
buffer.copy(this.nonce, 0, buffer.length - 24);
end = buffer.length - 24;
} else {
buffer.copy(this.nonce, 0, 0, 12);
}
// Open packet
let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key);
if (!packet) return new Error('Failed to decrypt voice packet');
packet = Buffer.from(packet);
// Strip RTP Header Extensions (one-byte only)
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++) {
const byte = packet[offset];
offset++;
if (byte === 0) continue;
offset += 1 + (0b1111 & (byte >> 4));
}
// Skip over undocumented Discord byte
offset++;
packet = packet.slice(offset);
}
return packet;
}
push(buffer) {
const ssrc = buffer.readUInt32BE(8);
const userStat = this.connection.ssrcMap.get(ssrc);
if (!userStat) return;
let opusPacket;
const streamInfo = this.streams.get(userStat.userID);
// If the user is in video, we need to check if the packet is just silence
if (userStat.hasVideo) {
opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
// Only emit an error if we were actively receiving packets from this user
if (streamInfo) {
this.emit('error', opusPacket);
return;
}
}
if (SILENCE_FRAME.equals(opusPacket)) {
// If this is a silence frame, pretend we never received it
return;
}
}
let speakingTimeout = this.speakingTimeouts.get(ssrc);
if (typeof speakingTimeout === 'undefined') {
// Ensure at least the speaking bit is set.
// As the object is by reference, it's only needed once per client re-connect.
if (userStat.speaking === 0) {
userStat.speaking = Speaking.FLAGS.SPEAKING;
}
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();
}
if (streamInfo) {
const { stream } = streamInfo;
if (!opusPacket) {
opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
this.emit('error', opusPacket);
return;
}
}
stream.push(opusPacket);
}
}
}
module.exports = PacketHandler;

View File

@ -0,0 +1,58 @@
'use strict';
const EventEmitter = require('events');
const prism = require('prism-media');
const PacketHandler = require('./PacketHandler');
const { Error } = require('../../../errors');
/**
* Receives audio packets from a voice connection.
* @example
* const receiver = connection.createReceiver();
* // opusStream is a ReadableStream - that means you could play it back to a voice channel if you wanted to!
* const opusStream = receiver.createStream(user);
*/
class VoiceReceiver extends EventEmitter {
constructor(connection) {
super();
this.connection = connection;
this.packets = new PacketHandler(this);
/**
* Emitted whenever there is a warning
* @event VoiceReceiver#debug
* @param {Error|string} error The error or message to debug
*/
this.packets.on('error', err => this.emit('debug', err));
}
/**
* Options passed to `VoiceReceiver#createStream`.
* @typedef {Object} ReceiveStreamOptions
* @property {string} [mode='opus'] The mode for audio output. This defaults to opus, meaning discord.js won't decode
* the packets for you. You can set this to 'pcm' so that the stream's output will be 16-bit little-endian stereo
* audio
* @property {string} [end='silence'] When the stream should be destroyed. If `silence`, this will be when the user
* stops talking. Otherwise, if `manual`, this should be handled by you.
*/
/**
* Creates a new audio receiving stream. If a stream already exists for a user, then that stream will be returned
* rather than generating a new one.
* @param {UserResolvable} user The user to start listening to.
* @param {ReceiveStreamOptions} options Options.
* @returns {ReadableStream}
*/
createStream(user, { mode = 'opus', end = 'silence' } = {}) {
user = this.connection.client.users.resolve(user);
if (!user) throw new Error('VOICE_USER_MISSING');
const stream = this.packets.makeStream(user.id, end);
if (mode === 'pcm') {
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 });
stream.pipe(decoder);
return decoder;
}
return stream;
}
}
module.exports = VoiceReceiver;

View File

@ -0,0 +1,94 @@
'use strict';
const { Readable } = require('stream');
const prism = require('prism-media');
const { Error } = require('../../../errors');
/**
* Options that can be passed to stream-playing methods:
* @typedef {Object} StreamOptions
* @property {StreamType} [type='unknown'] The type of stream.
* @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} [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.
* If set to 'auto', the voice channel's bitrate will be used
* @property {number} [highWaterMark=12] The maximum number of opus packets to make and store before they are
* actually needed. See https://nodejs.org/en/docs/guides/backpressuring-in-streams/. Setting this value to
* 1 means that changes in volume will be more instant.
*/
/**
* An option passed as part of `StreamOptions` specifying the type of the stream.
* * `unknown`: The default type, streams/input will be passed through to ffmpeg before encoding.
* Will play most streams.
* * `converted`: Play a stream of 16bit signed stereo PCM data, skipping ffmpeg.
* * `opus`: Play a stream of opus packets, skipping ffmpeg. You lose the ability to alter volume.
* * `ogg/opus`: Play an ogg file with the opus encoding, skipping ffmpeg. You lose the ability to alter volume.
* * `webm/opus`: Play a webm file with opus audio, skipping ffmpeg. You lose the ability to alter volume.
* @typedef {string} StreamType
*/
/**
* An interface class to allow you to play audio over VoiceConnections and VoiceBroadcasts.
*/
class PlayInterface {
constructor(player) {
this.player = player;
}
/**
* Play an audio resource.
* @param {VoiceBroadcast|ReadableStream|string} resource The resource to play.
* @param {StreamOptions} [options] The options to play.
* @example
* // Play a local audio file
* connection.play('/home/hydrabolt/audio.mp3', { volume: 0.5 });
* @example
* // Play a ReadableStream
* connection.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { quality: 'highestaudio' }));
* @example
* // Play a voice broadcast
* const broadcast = client.voice.createBroadcast();
* broadcast.play('/home/hydrabolt/audio.mp3');
* connection.play(broadcast);
* @example
* // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html
* connection.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
* @returns {StreamDispatcher}
*/
play(resource, options = {}) {
const VoiceBroadcast = require('../VoiceBroadcast');
if (resource instanceof VoiceBroadcast) {
if (!this.player.playBroadcast) throw new Error('VOICE_PLAY_INTERFACE_NO_BROADCAST');
return this.player.playBroadcast(resource, options);
}
if (resource instanceof Readable || typeof resource === 'string') {
const type = options.type || 'unknown';
if (type === 'unknown') {
return this.player.playUnknown(resource, options);
} else if (type === 'converted') {
return this.player.playPCMStream(resource, options);
} else if (type === 'opus') {
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.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.opus.WebmDemuxer()), options);
}
}
throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE');
}
static applyToClass(structure) {
for (const prop of ['play']) {
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop));
}
}
}
module.exports = PlayInterface;

View File

@ -0,0 +1,32 @@
'use strict';
const libs = {
sodium: sodium => ({
open: sodium.api.crypto_secretbox_open_easy,
close: sodium.api.crypto_secretbox_easy,
random: n => sodium.randombytes_buf(n),
}),
'libsodium-wrappers': sodium => ({
open: sodium.crypto_secretbox_open_easy,
close: sodium.crypto_secretbox_easy,
random: n => sodium.randombytes_buf(n),
}),
tweetnacl: tweetnacl => ({
open: tweetnacl.secretbox.open,
close: tweetnacl.secretbox,
random: n => tweetnacl.randomBytes(n),
}),
};
exports.methods = {};
(async () => {
for (const libName of Object.keys(libs)) {
try {
const lib = require(libName);
if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; // eslint-disable-line no-await-in-loop
exports.methods = libs[libName](lib);
break;
} catch {} // eslint-disable-line no-empty
}
})();

View File

@ -0,0 +1,15 @@
'use strict';
const { Readable } = require('stream');
const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
class Silence extends Readable {
_read() {
this.push(SILENCE_FRAME);
}
}
Silence.SILENCE_FRAME = SILENCE_FRAME;
module.exports = Silence;

View File

@ -0,0 +1,103 @@
'use strict';
const EventEmitter = require('events');
/**
* An interface class for volume transformation.
* @extends {EventEmitter}
*/
class VolumeInterface extends EventEmitter {
constructor({ volume = 1 } = {}) {
super();
this.setVolume(volume);
}
/**
* Whether or not the volume of this stream is editable
* @type {boolean}
* @readonly
*/
get volumeEditable() {
return true;
}
/**
* The current volume of the stream
* @type {number}
* @readonly
*/
get volume() {
return this._volume;
}
/**
* The current volume of the stream in decibels
* @type {number}
* @readonly
*/
get volumeDecibels() {
return Math.log10(this.volume) * 20;
}
/**
* The current volume of the stream from a logarithmic scale
* @type {number}
* @readonly
*/
get volumeLogarithmic() {
return Math.pow(this.volume, 1 / 1.660964);
}
applyVolume(buffer, volume) {
volume = volume || this._volume;
if (volume === 1) return buffer;
const out = Buffer.alloc(buffer.length);
for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) break;
const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i))));
out.writeInt16LE(uint, i);
}
return out;
}
/**
* Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
* @param {number} volume The volume that you want to set
*/
setVolume(volume) {
/**
* Emitted when the volume of this interface changes.
* @event VolumeInterface#volumeChange
* @param {number} oldVolume The old volume of this interface
* @param {number} newVolume The new volume of this interface
*/
this.emit('volumeChange', this._volume, volume);
this._volume = volume;
}
/**
* Sets the volume in decibels.
* @param {number} db The decibels
*/
setVolumeDecibels(db) {
this.setVolume(Math.pow(10, db / 20));
}
/**
* Sets the volume so that a perceived value of 0.5 is half the perceived volume etc.
* @param {number} value The value for the volume
*/
setVolumeLogarithmic(value) {
this.setVolume(Math.pow(value, 1.660964));
}
}
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));
}
};

View File

@ -0,0 +1,437 @@
'use strict';
const EventEmitter = require('events');
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];
/**
* The WebSocket manager for this client.
* <info>This class forwards raw dispatch events,
* read more about it here {@link https://discord.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
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The gateway this manager uses
* @type {?string}
*/
this.gateway = null;
/**
* 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}
* @property {number} total Total number of identifies available
* @property {number} remaining Number of identifies remaining
* @property {number} reset_after Number of milliseconds after which the limit resets
*/
this.sessionStartLimit = null;
}
/**
* The average ping of all WebSocketShards
* @type {number}
* @readonly
*/
get ping() {
const sum = this.shards.reduce((a, b) => a + b.ping, 0);
return sum / this.shards.size;
}
/**
* Emits a debug message.
* @param {string} message The debug message
* @param {?WebSocketShard} [shard] The shard that emitted this message, if any
* @private
*/
debug(message, shard) {
this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`);
}
/**
* 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 = null;
}
/**
* 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.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 });
}
/**
* 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
*/
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 && 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.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();
}
/**
* Causes the client to be marked as ready and emits the ready event.
* @private
*/
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();
}
}
module.exports = WebSocketManager;

View File

@ -0,0 +1,771 @@
'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 = null;
/**
* 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
* @name WebSocketShard#ratelimit
* @type {Object}
* @private
*/
Object.defineProperty(this, 'ratelimit', {
value: {
queue: [],
total: 120,
remaining: 120,
time: 60e3,
timer: null,
},
});
/**
* The WebSocket connection for the current shard
* @name WebSocketShard#connection
* @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
* @name WebSocketShard#inflate
* @type {?Inflate}
* @private
*/
Object.defineProperty(this, 'inflate', { value: null, writable: true });
/**
* The HELLO timeout
* @name WebSocketShard#helloTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
/**
* If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
* @type {boolean}
* @private
*/
Object.defineProperty(this, 'eventsAttached', { value: false, writable: true });
/**
* A set of guild IDs this shard expects to receive
* @name WebSocketShard#expectedGuilds
* @type {?Set<string>}
* @private
*/
Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
/**
* The ready timeout
* @name WebSocketShard#readyTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/**
* Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @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.debug('[RECONNECT] Discord asked us to 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 = null;
// 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', true);
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 = null;
}
// 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 = null;
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 = null;
}
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 = null;
}
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://discord.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 = null;
}
// 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;

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ChannelCreate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ChannelDelete.handle(packet.d);
};

View 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);
}
};

View 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);
}
};

View 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);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildBanRemove.handle(packet.d);
};

View 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);
}
}
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildDelete.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildEmojisUpdate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildIntegrationsUpdate.handle(packet.d);
};

View File

@ -0,0 +1,30 @@
'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.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
* @param {Object} chunk Properties of the received chunk
* @param {number} chunk.index Index of the received chunk
* @param {number} chunk.count Number of chunks the client should receive
* @param {?string} chunk.nonce Nonce for this chunk
*/
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild, {
count: data.chunk_count,
index: data.chunk_index,
nonce: data.nonce,
});
};

View 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);
}
}
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet, shard) => {
client.actions.GuildMemberRemove.handle(packet.d, shard);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet, shard) => {
client.actions.GuildMemberUpdate.handle(packet.d, shard);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleCreate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleDelete.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleUpdate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildUpdate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InviteCreate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InviteDelete.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageCreate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageDelete.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageDeleteBulk.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionAdd.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionRemove.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionRemoveAll.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageReactionRemoveEmoji.handle(packet.d);
};

View File

@ -0,0 +1,16 @@
'use strict';
const { Events } = require('../../../util/Constants');
module.exports = (client, packet) => {
const { old, updated } = client.actions.MessageUpdate.handle(packet.d);
if (old && updated) {
/**
* Emitted whenever a message is updated - e.g. embed or content change.
* @event Client#messageUpdate
* @param {Message} oldMessage The message before the update
* @param {Message} newMessage The message after the update
*/
client.emit(Events.MESSAGE_UPDATE, old, updated);
}
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.PresenceUpdate.handle(packet.d);
};

View File

@ -0,0 +1,21 @@
'use strict';
let ClientUser;
module.exports = (client, { d: data }, shard) => {
if (client.user) {
client.user._patch(data.user);
} else {
if (!ClientUser) ClientUser = require('../../../structures/ClientUser');
const clientUser = new ClientUser(client, data.user);
client.user = clientUser;
client.users.cache.set(clientUser.id, clientUser);
}
for (const guild of data.guilds) {
guild.shardID = shard.id;
client.guilds.add(guild);
}
shard.checkReady();
};

View File

@ -0,0 +1,14 @@
'use strict';
const { Events } = require('../../../util/Constants');
module.exports = (client, packet, shard) => {
const replayed = shard.sequence - shard.closeSequence;
/**
* Emitted when a shard resumes successfully.
* @event Client#shardResume
* @param {number} id The shard ID that resumed
* @param {number} replayedEvents The amount of replayed events
*/
client.emit(Events.SHARD_RESUME, shard.id, replayed);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.TypingStart.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.UserUpdate.handle(packet.d);
};

View File

@ -0,0 +1,6 @@
'use strict';
module.exports = (client, packet) => {
client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`);
client.voice.onVoiceServer(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.VoiceStateUpdate.handle(packet.d);
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.WebhooksUpdate.handle(packet.d);
};

View File

@ -0,0 +1,13 @@
'use strict';
const { WSEvents } = require('../../../util/Constants');
const handlers = {};
for (const name of Object.keys(WSEvents)) {
try {
handlers[name] = require(`./${name}.js`);
} catch {} // eslint-disable-line no-empty
}
module.exports = handlers;

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