mirror of
https://github.com/ION606/archivebot.git
synced 2026-06-05 23:46:13 +00:00
Initial commit
This commit is contained in:
+191
@@ -0,0 +1,191 @@
|
||||
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 - 2021 Noel Buechler
|
||||
Copyright 2015 - 2021 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.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
<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/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&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="Tests status" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## 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 16.6.0 or newer is required.**
|
||||
|
||||
```sh-session
|
||||
npm install discord.js
|
||||
yarn add discord.js
|
||||
pnpm add discord.js
|
||||
```
|
||||
|
||||
### 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`)
|
||||
- [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`)
|
||||
- [@discordjs/voice](https://github.com/discordjs/voice) for interacting with the Discord Voice API (`npm install @discordjs/voice`)
|
||||
|
||||
## Example usage
|
||||
|
||||
Install all required dependencies:
|
||||
```sh-session
|
||||
npm install discord.js @discordjs/rest discord-api-types
|
||||
yarn add discord.js @discordjs/rest discord-api-types
|
||||
pnpm add discord.js @discordjs/rest discord-api-types
|
||||
```
|
||||
|
||||
Register a slash command against the Discord API:
|
||||
```js
|
||||
const { REST } = require('@discordjs/rest');
|
||||
const { Routes } = require('discord-api-types/v9');
|
||||
|
||||
const commands = [{
|
||||
name: 'ping',
|
||||
description: 'Replies with Pong!'
|
||||
}];
|
||||
|
||||
const rest = new REST({ version: '9' }).setToken('token');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
|
||||
{ body: commands },
|
||||
);
|
||||
|
||||
console.log('Successfully reloaded application (/) commands.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
Afterwards we can create a quite simple example bot:
|
||||
```js
|
||||
const { Client, Intents } = require('discord.js');
|
||||
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
});
|
||||
|
||||
client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isCommand()) return;
|
||||
|
||||
if (interaction.commandName === 'ping') {
|
||||
await interaction.reply('Pong!');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('token');
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
|
||||
- [Documentation](https://discord.js.org/#/docs)
|
||||
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
|
||||
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
|
||||
- [discord.js Discord server](https://discord.gg/djs)
|
||||
- [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://discord.com/developers/docs/topics/community-resources#libraries)
|
||||
|
||||
### 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/main/.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/djs).
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "discord.js",
|
||||
"version": "13.6.0",
|
||||
"description": "A powerful library for interacting with the Discord API",
|
||||
"scripts": {
|
||||
"test": "npm run lint && npm run docs:test && npm run lint:typings && npm run test:typescript",
|
||||
"test:typescript": "tsc --noEmit && tsd",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint:typings": "tslint typings/index.d.ts",
|
||||
"format": "prettier --write src/**/*.js typings/**/*.ts",
|
||||
"prepare": "is-ci || husky install",
|
||||
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
|
||||
"docs:test": "docgen --source src --custom docs/index.yml",
|
||||
"prepublishOnly": "npm run test",
|
||||
"changelog": "git cliff --prepend CHANGELOG.md -l"
|
||||
},
|
||||
"main": "./src/index.js",
|
||||
"types": "./typings/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"typings"
|
||||
],
|
||||
"directories": {
|
||||
"lib": "src",
|
||||
"test": "test"
|
||||
},
|
||||
"contributors": [
|
||||
"Crawl <icrawltogo@gmail.com>",
|
||||
"Amish Shah <amishshah.2k@gmail.com>",
|
||||
"Vlad Frangu <kingdgrizzle@gmail.com>",
|
||||
"SpaceEEC <spaceeec@yahoo.com>",
|
||||
"Antonio Roman <kyradiscord@gmail.com>"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"discord",
|
||||
"api",
|
||||
"bot",
|
||||
"client",
|
||||
"node",
|
||||
"discordapp"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/discordjs/discord.js.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/discordjs/discord.js/issues"
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^0.11.0",
|
||||
"@discordjs/collection": "^0.4.0",
|
||||
"@sapphire/async-queue": "^1.1.9",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/ws": "^8.2.2",
|
||||
"discord-api-types": "^0.26.0",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ws": "^8.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.0.1",
|
||||
"@commitlint/config-angular": "^16.0.0",
|
||||
"@discordjs/docgen": "^0.11.0",
|
||||
"@favware/npm-deprecate": "^1.0.4",
|
||||
"@types/node": "^16.11.12",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"dtslint": "^4.2.1",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^27.4.5",
|
||||
"lint-staged": "^12.1.4",
|
||||
"prettier": "^2.5.1",
|
||||
"tsd": "^0.19.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.6.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
let erlpack;
|
||||
const { Buffer } = require('node:buffer');
|
||||
|
||||
try {
|
||||
erlpack = require('erlpack');
|
||||
if (!erlpack.pack) erlpack = null;
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
|
||||
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);
|
||||
return ws;
|
||||
};
|
||||
|
||||
for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) exports[state] = exports.WebSocket[state];
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('node:events');
|
||||
const RESTManager = require('../rest/RESTManager');
|
||||
const Options = require('../util/Options');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* The base class for all clients.
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class BaseClient extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The options the client was instantiated with
|
||||
* @type {ClientOptions}
|
||||
*/
|
||||
this.options = Util.mergeDefault(Options.createDefault(), options);
|
||||
|
||||
/**
|
||||
* The REST manager of the client
|
||||
* @type {RESTManager}
|
||||
* @private
|
||||
*/
|
||||
this.rest = new RESTManager(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* API shortcut
|
||||
* @type {Object}
|
||||
* @readonly
|
||||
* @private
|
||||
*/
|
||||
get api() {
|
||||
return this.rest.api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys all assets used by the base client.
|
||||
* @returns {void}
|
||||
*/
|
||||
destroy() {
|
||||
if (this.rest.sweepInterval) clearInterval(this.rest.sweepInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Emitted for general debugging information.
|
||||
* @event BaseClient#debug
|
||||
* @param {string} info The debug information
|
||||
*/
|
||||
+621
@@ -0,0 +1,621 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('node:process');
|
||||
const { setInterval } = require('node:timers');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
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 BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
|
||||
const ChannelManager = require('../managers/ChannelManager');
|
||||
const GuildManager = require('../managers/GuildManager');
|
||||
const UserManager = require('../managers/UserManager');
|
||||
const ShardClientUtil = require('../sharding/ShardClientUtil');
|
||||
const ClientPresence = require('../structures/ClientPresence');
|
||||
const GuildPreview = require('../structures/GuildPreview');
|
||||
const GuildTemplate = require('../structures/GuildTemplate');
|
||||
const Invite = require('../structures/Invite');
|
||||
const { Sticker } = require('../structures/Sticker');
|
||||
const StickerPack = require('../structures/StickerPack');
|
||||
const VoiceRegion = require('../structures/VoiceRegion');
|
||||
const Webhook = require('../structures/Webhook');
|
||||
const Widget = require('../structures/Widget');
|
||||
const { Events, InviteScopes, Status } = require('../util/Constants');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const Intents = require('../util/Intents');
|
||||
const Options = require('../util/Options');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const Sweepers = require('../util/Sweepers');
|
||||
|
||||
/**
|
||||
* 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(options);
|
||||
|
||||
const data = require('node:worker_threads').workerData ?? process.env;
|
||||
const defaults = Options.createDefault();
|
||||
|
||||
if (this.options.shards === defaults.shards) {
|
||||
if ('SHARDS' in data) {
|
||||
this.options.shards = JSON.parse(data.SHARDS);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.shardCount === defaults.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();
|
||||
|
||||
/**
|
||||
* Functions called when a cache is garbage collected or the Client is destroyed
|
||||
* @type {Set<Function>}
|
||||
* @private
|
||||
*/
|
||||
this._cleanups = new Set();
|
||||
|
||||
/**
|
||||
* The finalizers used to cleanup items.
|
||||
* @type {FinalizationRegistry}
|
||||
* @private
|
||||
*/
|
||||
this._finalizers = new FinalizationRegistry(this._finalize.bind(this));
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @type {ClientVoiceManager}
|
||||
*/
|
||||
this.voice = new ClientVoiceManager(this);
|
||||
|
||||
/**
|
||||
* Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
|
||||
* @type {?ShardClientUtil}
|
||||
*/
|
||||
this.shard = 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);
|
||||
|
||||
/**
|
||||
* The sweeping functions and their intervals used to periodically sweep caches
|
||||
* @type {Sweepers}
|
||||
*/
|
||||
this.sweepers = new Sweepers(this, this.options.sweepers);
|
||||
|
||||
/**
|
||||
* The presence of the Client
|
||||
* @private
|
||||
* @type {ClientPresence}
|
||||
*/
|
||||
this.presence = new ClientPresence(this, this.options.presence);
|
||||
|
||||
Object.defineProperty(this, 'token', { writable: true });
|
||||
if (!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;
|
||||
|
||||
/**
|
||||
* The application of this bot
|
||||
* @type {?ClientApplication}
|
||||
*/
|
||||
this.application = 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) {
|
||||
process.emitWarning(
|
||||
'The message sweeping client options are deprecated, use the global sweepers instead.',
|
||||
'DeprecationWarning',
|
||||
);
|
||||
this.sweepMessageInterval = setInterval(
|
||||
this.sweepMessages.bind(this),
|
||||
this.options.messageSweepInterval * 1_000,
|
||||
).unref();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All custom emojis that the client has access to, mapped by their ids
|
||||
* @type {BaseGuildEmojiManager}
|
||||
* @readonly
|
||||
*/
|
||||
get emojis() {
|
||||
const emojis = new BaseGuildEmojiManager(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?.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 = 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the client has logged in, indicative of being able to access
|
||||
* properties such as `user` and `application`.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReady() {
|
||||
return this.ws.status === Status.READY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out, terminates the connection to Discord, and destroys the client.
|
||||
* @returns {void}
|
||||
*/
|
||||
destroy() {
|
||||
super.destroy();
|
||||
|
||||
for (const fn of this._cleanups) fn();
|
||||
this._cleanups.clear();
|
||||
|
||||
if (this.sweepMessageInterval) clearInterval(this.sweepMessageInterval);
|
||||
|
||||
this.sweepers.destroy();
|
||||
this.ws.destroy();
|
||||
this.token = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used when fetching an invite from Discord.
|
||||
* @typedef {Object} ClientFetchInviteOptions
|
||||
* @property {Snowflake} [guildScheduledEventId] The id of the guild scheduled event to include with
|
||||
* the invite
|
||||
*/
|
||||
|
||||
/**
|
||||
* Obtains an invite from Discord.
|
||||
* @param {InviteResolvable} invite Invite code or URL
|
||||
* @param {ClientFetchInviteOptions} [options] Options for fetching the invite
|
||||
* @returns {Promise<Invite>}
|
||||
* @example
|
||||
* client.fetchInvite('https://discord.gg/djs')
|
||||
* .then(invite => console.log(`Obtained invite with code: ${invite.code}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchInvite(invite, options) {
|
||||
const code = DataResolver.resolveInviteCode(invite);
|
||||
const data = await this.api.invites(code).get({
|
||||
query: { with_counts: true, with_expiration: true, guild_scheduled_event_id: options?.guildScheduledEventId },
|
||||
});
|
||||
return 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);
|
||||
*/
|
||||
async fetchGuildTemplate(template) {
|
||||
const code = DataResolver.resolveGuildTemplateCode(template);
|
||||
const data = await this.api.guilds.templates(code).get();
|
||||
return new GuildTemplate(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a webhook from Discord.
|
||||
* @param {Snowflake} id The webhook's id
|
||||
* @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);
|
||||
*/
|
||||
async fetchWebhook(id, token) {
|
||||
const data = await this.api.webhooks(id, token).get();
|
||||
return new Webhook(this, { token, ...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);
|
||||
*/
|
||||
async fetchVoiceRegions() {
|
||||
const apiRegions = await this.api.voice.regions.get();
|
||||
const regions = new Collection();
|
||||
for (const region of apiRegions) regions.set(region.id, new VoiceRegion(region));
|
||||
return regions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a sticker from Discord.
|
||||
* @param {Snowflake} id The sticker's id
|
||||
* @returns {Promise<Sticker>}
|
||||
* @example
|
||||
* client.fetchSticker('id')
|
||||
* .then(sticker => console.log(`Obtained sticker with name: ${sticker.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchSticker(id) {
|
||||
const data = await this.api.stickers(id).get();
|
||||
return new Sticker(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the list of sticker packs available to Nitro subscribers from Discord.
|
||||
* @returns {Promise<Collection<Snowflake, StickerPack>>}
|
||||
* @example
|
||||
* client.fetchPremiumStickerPacks()
|
||||
* .then(packs => console.log(`Available sticker packs are: ${packs.map(pack => pack.name).join(', ')}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchPremiumStickerPacks() {
|
||||
const data = await this.api('sticker-packs').get();
|
||||
return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)]));
|
||||
}
|
||||
/**
|
||||
* A last ditch cleanup function for garbage collection.
|
||||
* @param {Function} options.cleanup The function called to GC
|
||||
* @param {string} [options.message] The message to send after a successful GC
|
||||
* @param {string} [options.name] The name of the item being GCed
|
||||
* @private
|
||||
*/
|
||||
_finalize({ cleanup, message, name }) {
|
||||
try {
|
||||
cleanup();
|
||||
this._cleanups.delete(cleanup);
|
||||
if (message) {
|
||||
this.emit(Events.DEBUG, message);
|
||||
}
|
||||
} catch {
|
||||
this.emit(Events.DEBUG, `Garbage collection failed on ${name ?? 'an unknown item'}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 messages = this.sweepers.sweepMessages(Sweepers.outdatedMessageSweepFilter(lifetime)());
|
||||
this.emit(Events.DEBUG, `Swept ${messages} messages older than ${lifetime} seconds`);
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
async fetchGuildPreview(guild) {
|
||||
const id = this.guilds.resolveId(guild);
|
||||
if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
|
||||
const data = await this.api.guilds(id).preview.get();
|
||||
return new GuildPreview(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the widget data of a guild from Discord, available for guilds with the widget enabled.
|
||||
* @param {GuildResolvable} guild The guild to fetch the widget data for
|
||||
* @returns {Promise<Widget>}
|
||||
*/
|
||||
async fetchGuildWidget(guild) {
|
||||
const id = this.guilds.resolveId(guild);
|
||||
if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
|
||||
const data = await this.api.guilds(id, 'widget.json').get();
|
||||
return new Widget(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link Client#generateInvite}.
|
||||
* @typedef {Object} InviteGenerationOptions
|
||||
* @property {InviteScope[]} scopes Scopes that should be requested
|
||||
* @property {PermissionResolvable} [permissions] Permissions to request
|
||||
* @property {GuildResolvable} [guild] Guild to preselect
|
||||
* @property {boolean} [disableGuildSelect] Whether to disable the guild selection
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates a link that can be used to invite the bot to a guild.
|
||||
* @param {InviteGenerationOptions} [options={}] Options for the invite
|
||||
* @returns {string}
|
||||
* @example
|
||||
* const link = client.generateInvite({
|
||||
* scopes: ['applications.commands'],
|
||||
* });
|
||||
* console.log(`Generated application invite link: ${link}`);
|
||||
* @example
|
||||
* const link = client.generateInvite({
|
||||
* permissions: [
|
||||
* Permissions.FLAGS.SEND_MESSAGES,
|
||||
* Permissions.FLAGS.MANAGE_GUILD,
|
||||
* Permissions.FLAGS.MENTION_EVERYONE,
|
||||
* ],
|
||||
* scopes: ['bot'],
|
||||
* });
|
||||
* console.log(`Generated bot invite link: ${link}`);
|
||||
*/
|
||||
generateInvite(options = {}) {
|
||||
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
|
||||
if (!this.application) throw new Error('CLIENT_NOT_READY', 'generate an invite link');
|
||||
|
||||
const query = new URLSearchParams({
|
||||
client_id: this.application.id,
|
||||
});
|
||||
|
||||
const { scopes } = options;
|
||||
if (typeof scopes === 'undefined') {
|
||||
throw new TypeError('INVITE_MISSING_SCOPES');
|
||||
}
|
||||
if (!Array.isArray(scopes)) {
|
||||
throw new TypeError('INVALID_TYPE', 'scopes', 'Array of Invite Scopes', true);
|
||||
}
|
||||
if (!scopes.some(scope => ['bot', 'applications.commands'].includes(scope))) {
|
||||
throw new TypeError('INVITE_MISSING_SCOPES');
|
||||
}
|
||||
const invalidScope = scopes.find(scope => !InviteScopes.includes(scope));
|
||||
if (invalidScope) {
|
||||
throw new TypeError('INVALID_ELEMENT', 'Array', 'scopes', invalidScope);
|
||||
}
|
||||
query.set('scope', scopes.join(' '));
|
||||
|
||||
if (options.permissions) {
|
||||
const permissions = Permissions.resolve(options.permissions);
|
||||
if (permissions) query.set('permissions', permissions);
|
||||
}
|
||||
|
||||
if (options.disableGuildSelect) {
|
||||
query.set('disable_guild_select', true);
|
||||
}
|
||||
|
||||
if (options.guild) {
|
||||
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.intents === 'undefined') {
|
||||
throw new TypeError('CLIENT_MISSING_INTENTS');
|
||||
} else {
|
||||
options.intents = Intents.resolve(options.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.makeCache !== 'function') {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function');
|
||||
}
|
||||
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.sweepers !== 'object' || options.sweepers === null) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object');
|
||||
}
|
||||
if (typeof options.invalidRequestWarningInterval !== 'number' || isNaN(options.invalidRequestWarningInterval)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'invalidRequestWarningInterval', 'a number');
|
||||
}
|
||||
if (!Array.isArray(options.partials)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
|
||||
}
|
||||
if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number');
|
||||
}
|
||||
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.restGlobalRateLimit !== 'number' || isNaN(options.restGlobalRateLimit)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'restGlobalRateLimit', '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');
|
||||
}
|
||||
if (typeof options.failIfNotExists !== 'boolean') {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'failIfNotExists', 'a boolean');
|
||||
}
|
||||
if (!Array.isArray(options.userAgentSuffix)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'userAgentSuffix', 'an array of strings');
|
||||
}
|
||||
if (
|
||||
typeof options.rejectOnRateLimit !== 'undefined' &&
|
||||
!(typeof options.rejectOnRateLimit === 'function' || Array.isArray(options.rejectOnRateLimit))
|
||||
) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'rejectOnRateLimit', 'an array or a function');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Client;
|
||||
|
||||
/**
|
||||
* Emitted for general warnings.
|
||||
* @event Client#warn
|
||||
* @param {string} info The warning
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Collection
|
||||
* @see {@link https://discord.js.org/#/docs/collection/main/class/Collection}
|
||||
*/
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const BaseClient = require('./BaseClient');
|
||||
const { Error } = require('../errors');
|
||||
const Webhook = require('../structures/Webhook');
|
||||
|
||||
/**
|
||||
* The webhook client.
|
||||
* @implements {Webhook}
|
||||
* @extends {BaseClient}
|
||||
*/
|
||||
class WebhookClient extends BaseClient {
|
||||
/**
|
||||
* The data for the webhook client containing either an id and token or just a URL
|
||||
* @typedef {Object} WebhookClientData
|
||||
* @property {Snowflake} [id] The id of the webhook
|
||||
* @property {string} [token] The token of the webhook
|
||||
* @property {string} [url] The full URL for the webhook client
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {WebhookClientData} data The data of the webhook
|
||||
* @param {ClientOptions} [options] Options for the client
|
||||
*/
|
||||
constructor(data, options) {
|
||||
super(options);
|
||||
Object.defineProperty(this, 'client', { value: this });
|
||||
let { id, token } = data;
|
||||
|
||||
if ('url' in data) {
|
||||
const url = data.url.match(
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
/^https?:\/\/(?:canary|ptb)?\.?discord\.com\/api\/webhooks(?:\/v[0-9]\d*)?\/([^\/]+)\/([^\/]+)/i,
|
||||
);
|
||||
|
||||
if (!url || url.length <= 1) throw new Error('WEBHOOK_URL_INVALID');
|
||||
|
||||
[, id, token] = url;
|
||||
}
|
||||
|
||||
this.id = id;
|
||||
Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true });
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by Webhook
|
||||
/* eslint-disable no-empty-function */
|
||||
send() {}
|
||||
sendSlackMessage() {}
|
||||
fetchMessage() {}
|
||||
edit() {}
|
||||
editMessage() {}
|
||||
delete() {}
|
||||
deleteMessage() {}
|
||||
get createdTimestamp() {}
|
||||
get createdAt() {}
|
||||
get url() {}
|
||||
}
|
||||
|
||||
Webhook.applyToClass(WebhookClient);
|
||||
|
||||
module.exports = WebhookClient;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
'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 ?? data.user ?? { 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?.id,
|
||||
},
|
||||
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?.id === this.client.user.id,
|
||||
},
|
||||
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?.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);
|
||||
}
|
||||
|
||||
getScheduledEvent(data, guild) {
|
||||
const id = data.guild_scheduled_event_id ?? data.id;
|
||||
return this.getPayload(
|
||||
{ id, guild_id: data.guild_id ?? guild.id },
|
||||
guild.scheduledEvents,
|
||||
id,
|
||||
PartialTypes.GUILD_SCHEDULED_EVENT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GenericAction;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
class ActionsManager {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
||||
this.register(require('./ChannelCreate'));
|
||||
this.register(require('./ChannelDelete'));
|
||||
this.register(require('./ChannelUpdate'));
|
||||
this.register(require('./GuildBanAdd'));
|
||||
this.register(require('./GuildBanRemove'));
|
||||
this.register(require('./GuildChannelsPositionUpdate'));
|
||||
this.register(require('./GuildDelete'));
|
||||
this.register(require('./GuildEmojiCreate'));
|
||||
this.register(require('./GuildEmojiDelete'));
|
||||
this.register(require('./GuildEmojiUpdate'));
|
||||
this.register(require('./GuildEmojisUpdate'));
|
||||
this.register(require('./GuildIntegrationsUpdate'));
|
||||
this.register(require('./GuildMemberRemove'));
|
||||
this.register(require('./GuildMemberUpdate'));
|
||||
this.register(require('./GuildRoleCreate'));
|
||||
this.register(require('./GuildRoleDelete'));
|
||||
this.register(require('./GuildRoleUpdate'));
|
||||
this.register(require('./GuildRolesPositionUpdate'));
|
||||
this.register(require('./GuildScheduledEventCreate'));
|
||||
this.register(require('./GuildScheduledEventDelete'));
|
||||
this.register(require('./GuildScheduledEventUpdate'));
|
||||
this.register(require('./GuildScheduledEventUserAdd'));
|
||||
this.register(require('./GuildScheduledEventUserRemove'));
|
||||
this.register(require('./GuildStickerCreate'));
|
||||
this.register(require('./GuildStickerDelete'));
|
||||
this.register(require('./GuildStickerUpdate'));
|
||||
this.register(require('./GuildStickersUpdate'));
|
||||
this.register(require('./GuildUpdate'));
|
||||
this.register(require('./InteractionCreate'));
|
||||
this.register(require('./InviteCreate'));
|
||||
this.register(require('./InviteDelete'));
|
||||
this.register(require('./MessageCreate'));
|
||||
this.register(require('./MessageDelete'));
|
||||
this.register(require('./MessageDeleteBulk'));
|
||||
this.register(require('./MessageReactionAdd'));
|
||||
this.register(require('./MessageReactionRemove'));
|
||||
this.register(require('./MessageReactionRemoveAll'));
|
||||
this.register(require('./MessageReactionRemoveEmoji'));
|
||||
this.register(require('./MessageUpdate'));
|
||||
this.register(require('./PresenceUpdate'));
|
||||
this.register(require('./StageInstanceCreate'));
|
||||
this.register(require('./StageInstanceDelete'));
|
||||
this.register(require('./StageInstanceUpdate'));
|
||||
this.register(require('./ThreadCreate'));
|
||||
this.register(require('./ThreadDelete'));
|
||||
this.register(require('./ThreadListSync'));
|
||||
this.register(require('./ThreadMemberUpdate'));
|
||||
this.register(require('./ThreadMembersUpdate'));
|
||||
this.register(require('./TypingStart'));
|
||||
this.register(require('./UserUpdate'));
|
||||
this.register(require('./VoiceStateUpdate'));
|
||||
this.register(require('./WebhooksUpdate'));
|
||||
}
|
||||
|
||||
register(Action) {
|
||||
this[Action.name.replace(/Action$/, '')] = new Action(this.client);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ActionsManager;
|
||||
+23
@@ -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 guild channel is created.
|
||||
* @event Client#channelCreate
|
||||
* @param {GuildChannel} channel The channel that was created
|
||||
*/
|
||||
client.emit(Events.CHANNEL_CREATE, channel);
|
||||
}
|
||||
return { channel };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChannelCreateAction;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedChannels } = require('../../structures/Channel');
|
||||
const DMChannel = require('../../structures/DMChannel');
|
||||
const { deletedMessages } = require('../../structures/Message');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ChannelDeleteAction extends Action {
|
||||
constructor(client) {
|
||||
super(client);
|
||||
this.deleted = new Map();
|
||||
}
|
||||
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = client.channels.cache.get(data.id);
|
||||
|
||||
if (channel) {
|
||||
client.channels._remove(channel.id);
|
||||
deletedChannels.add(channel);
|
||||
if (channel.messages && !(channel instanceof DMChannel)) {
|
||||
for (const message of channel.messages.cache.values()) {
|
||||
deletedMessages.add(message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
'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] !== 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);
|
||||
channel = newChannel;
|
||||
this.client.channels.cache.set(channel.id, channel);
|
||||
}
|
||||
|
||||
return {
|
||||
old,
|
||||
updated: channel,
|
||||
};
|
||||
} else {
|
||||
client.channels._add(data);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChannelUpdateAction;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildBanAdd extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
|
||||
/**
|
||||
* Emitted whenever a member is banned from a guild.
|
||||
* @event Client#guildBanAdd
|
||||
* @param {GuildBan} ban The ban that occurred
|
||||
*/
|
||||
if (guild) client.emit(Events.GUILD_BAN_ADD, guild.bans._add(data));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildBanAdd;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const GuildBan = require('../../structures/GuildBan');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildBanRemove extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
|
||||
/**
|
||||
* Emitted whenever a member is unbanned from a guild.
|
||||
* @event Client#guildBanRemove
|
||||
* @param {GuildBan} ban The ban that was removed
|
||||
*/
|
||||
if (guild) {
|
||||
const ban = guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild);
|
||||
guild.bans.cache.delete(ban.user.id);
|
||||
client.emit(Events.GUILD_BAN_REMOVE, ban);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildBanRemove;
|
||||
+21
@@ -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;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
const { setTimeout } = require('node:timers');
|
||||
const Action = require('./Action');
|
||||
const { deletedGuilds } = require('../../structures/Guild');
|
||||
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) {
|
||||
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);
|
||||
client.voice.adapters.get(data.id)?.destroy();
|
||||
|
||||
// Delete guild
|
||||
client.guilds.cache.delete(guild.id);
|
||||
deletedGuilds.add(guild);
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout).unref();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildDeleteAction;
|
||||
+20
@@ -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;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedEmojis } = require('../../structures/Emoji');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildEmojiDeleteAction extends Action {
|
||||
handle(emoji) {
|
||||
emoji.guild.emojis.cache.delete(emoji.id);
|
||||
deletedEmojis.add(emoji);
|
||||
/**
|
||||
* 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;
|
||||
+20
@@ -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;
|
||||
+34
@@ -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?.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;
|
||||
+19
@@ -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;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedGuildMembers } = require('../../structures/GuildMember');
|
||||
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) {
|
||||
deletedGuildMembers.add(member);
|
||||
guild.members.cache.delete(member.id);
|
||||
/**
|
||||
* Emitted whenever a member leaves a guild, or is kicked.
|
||||
* @event Client#guildMemberRemove
|
||||
* @param {GuildMember} member The member that has left/been kicked from the guild
|
||||
*/
|
||||
if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member);
|
||||
}
|
||||
guild.voiceStates.cache.delete(data.user.id);
|
||||
}
|
||||
return { guild, member };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildMemberRemoveAction;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
'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.
|
||||
* @event Client#guildMemberUpdate
|
||||
* @param {GuildMember} oldMember The member before the update
|
||||
* @param {GuildMember} newMember The member after the update
|
||||
*/
|
||||
if (shard.status === Status.READY && !member.equals(old)) 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;
|
||||
+25
@@ -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;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedRoles } = require('../../structures/Role');
|
||||
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);
|
||||
deletedRoles.add(role);
|
||||
/**
|
||||
* 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;
|
||||
+39
@@ -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;
|
||||
+21
@@ -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;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildScheduledEventCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
if (guild) {
|
||||
const guildScheduledEvent = guild.scheduledEvents._add(data);
|
||||
|
||||
/**
|
||||
* Emitted whenever a guild scheduled event is created.
|
||||
* @event Client#guildScheduledEventCreate
|
||||
* @param {GuildScheduledEvent} guildScheduledEvent The created guild scheduled event
|
||||
*/
|
||||
client.emit(Events.GUILD_SCHEDULED_EVENT_CREATE, guildScheduledEvent);
|
||||
|
||||
return { guildScheduledEvent };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildScheduledEventCreateAction;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildScheduledEventDeleteAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
|
||||
if (guild) {
|
||||
const guildScheduledEvent = this.getScheduledEvent(data, guild);
|
||||
if (guildScheduledEvent) {
|
||||
guild.scheduledEvents.cache.delete(guildScheduledEvent.id);
|
||||
|
||||
/**
|
||||
* Emitted whenever a guild scheduled event is deleted.
|
||||
* @event Client#guildScheduledEventDelete
|
||||
* @param {GuildScheduledEvent} guildScheduledEvent The deleted guild scheduled event
|
||||
*/
|
||||
client.emit(Events.GUILD_SCHEDULED_EVENT_DELETE, guildScheduledEvent);
|
||||
|
||||
return { guildScheduledEvent };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildScheduledEventDeleteAction;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildScheduledEventUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
|
||||
if (guild) {
|
||||
const oldGuildScheduledEvent = guild.scheduledEvents.cache.get(data.id)?._clone() ?? null;
|
||||
const newGuildScheduledEvent = guild.scheduledEvents._add(data);
|
||||
|
||||
/**
|
||||
* Emitted whenever a guild scheduled event gets updated.
|
||||
* @event Client#guildScheduledEventUpdate
|
||||
* @param {?GuildScheduledEvent} oldGuildScheduledEvent The guild scheduled event object before the update
|
||||
* @param {GuildScheduledEvent} newGuildScheduledEvent The guild scheduled event object after the update
|
||||
*/
|
||||
client.emit(Events.GUILD_SCHEDULED_EVENT_UPDATE, oldGuildScheduledEvent, newGuildScheduledEvent);
|
||||
|
||||
return { oldGuildScheduledEvent, newGuildScheduledEvent };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildScheduledEventUpdateAction;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildScheduledEventUserAddAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
|
||||
if (guild) {
|
||||
const guildScheduledEvent = this.getScheduledEvent(data, guild);
|
||||
const user = this.getUser(data);
|
||||
|
||||
if (guildScheduledEvent && user) {
|
||||
/**
|
||||
* Emitted whenever a user subscribes to a guild scheduled event
|
||||
* @event Client#guildScheduledEventUserAdd
|
||||
* @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
|
||||
* @param {User} user The user who subscribed
|
||||
*/
|
||||
client.emit(Events.GUILD_SCHEDULED_EVENT_USER_ADD, guildScheduledEvent, user);
|
||||
|
||||
return { guildScheduledEvent, user };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildScheduledEventUserAddAction;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildScheduledEventUserRemoveAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
|
||||
if (guild) {
|
||||
const guildScheduledEvent = this.getScheduledEvent(data, guild);
|
||||
const user = this.getUser(data);
|
||||
|
||||
if (guildScheduledEvent && user) {
|
||||
/**
|
||||
* Emitted whenever a user unsubscribes from a guild scheduled event
|
||||
* @event Client#guildScheduledEventUserRemove
|
||||
* @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
|
||||
* @param {User} user The user who unsubscribed
|
||||
*/
|
||||
client.emit(Events.GUILD_SCHEDULED_EVENT_USER_REMOVE, guildScheduledEvent, user);
|
||||
|
||||
return { guildScheduledEvent, user };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildScheduledEventUserRemoveAction;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildStickerCreateAction extends Action {
|
||||
handle(guild, createdSticker) {
|
||||
const already = guild.stickers.cache.has(createdSticker.id);
|
||||
const sticker = guild.stickers._add(createdSticker);
|
||||
/**
|
||||
* Emitted whenever a custom sticker is created in a guild.
|
||||
* @event Client#stickerCreate
|
||||
* @param {Sticker} sticker The sticker that was created
|
||||
*/
|
||||
if (!already) this.client.emit(Events.GUILD_STICKER_CREATE, sticker);
|
||||
return { sticker };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildStickerCreateAction;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedStickers } = require('../../structures/Sticker');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildStickerDeleteAction extends Action {
|
||||
handle(sticker) {
|
||||
sticker.guild.stickers.cache.delete(sticker.id);
|
||||
deletedStickers.add(sticker);
|
||||
/**
|
||||
* Emitted whenever a custom sticker is deleted in a guild.
|
||||
* @event Client#stickerDelete
|
||||
* @param {Sticker} sticker The sticker that was deleted
|
||||
*/
|
||||
this.client.emit(Events.GUILD_STICKER_DELETE, sticker);
|
||||
return { sticker };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildStickerDeleteAction;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildStickerUpdateAction extends Action {
|
||||
handle(current, data) {
|
||||
const old = current._update(data);
|
||||
/**
|
||||
* Emitted whenever a custom sticker is updated in a guild.
|
||||
* @event Client#stickerUpdate
|
||||
* @param {Sticker} oldSticker The old sticker
|
||||
* @param {Sticker} newSticker The new sticker
|
||||
*/
|
||||
this.client.emit(Events.GUILD_STICKER_UPDATE, old, current);
|
||||
return { sticker: current };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildStickerUpdateAction;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
|
||||
class GuildStickersUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const guild = this.client.guilds.cache.get(data.guild_id);
|
||||
if (!guild?.stickers) return;
|
||||
|
||||
const deletions = new Map(guild.stickers.cache);
|
||||
|
||||
for (const sticker of data.stickers) {
|
||||
// Determine type of sticker event
|
||||
const cachedSticker = guild.stickers.cache.get(sticker.id);
|
||||
if (cachedSticker) {
|
||||
deletions.delete(sticker.id);
|
||||
if (!cachedSticker.equals(sticker)) {
|
||||
// Sticker updated
|
||||
this.client.actions.GuildStickerUpdate.handle(cachedSticker, sticker);
|
||||
}
|
||||
} else {
|
||||
// Sticker added
|
||||
this.client.actions.GuildStickerCreate.handle(guild, sticker);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sticker of deletions.values()) {
|
||||
// Sticker deleted
|
||||
this.client.actions.GuildStickerDelete.handle(sticker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildStickersUpdateAction;
|
||||
+33
@@ -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;
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('node:process');
|
||||
const Action = require('./Action');
|
||||
const AutocompleteInteraction = require('../../structures/AutocompleteInteraction');
|
||||
const ButtonInteraction = require('../../structures/ButtonInteraction');
|
||||
const CommandInteraction = require('../../structures/CommandInteraction');
|
||||
const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction');
|
||||
const SelectMenuInteraction = require('../../structures/SelectMenuInteraction');
|
||||
const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction');
|
||||
const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants');
|
||||
|
||||
let deprecationEmitted = false;
|
||||
|
||||
class InteractionCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
// Resolve and cache partial channels for Interaction#channel getter
|
||||
this.getChannel(data);
|
||||
|
||||
let InteractionType;
|
||||
switch (data.type) {
|
||||
case InteractionTypes.APPLICATION_COMMAND:
|
||||
switch (data.data.type) {
|
||||
case ApplicationCommandTypes.CHAT_INPUT:
|
||||
InteractionType = CommandInteraction;
|
||||
break;
|
||||
case ApplicationCommandTypes.USER:
|
||||
InteractionType = UserContextMenuInteraction;
|
||||
break;
|
||||
case ApplicationCommandTypes.MESSAGE:
|
||||
InteractionType = MessageContextMenuInteraction;
|
||||
break;
|
||||
default:
|
||||
client.emit(
|
||||
Events.DEBUG,
|
||||
`[INTERACTION] Received application command interaction with unknown type: ${data.data.type}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case InteractionTypes.MESSAGE_COMPONENT:
|
||||
switch (data.data.component_type) {
|
||||
case MessageComponentTypes.BUTTON:
|
||||
InteractionType = ButtonInteraction;
|
||||
break;
|
||||
case MessageComponentTypes.SELECT_MENU:
|
||||
InteractionType = SelectMenuInteraction;
|
||||
break;
|
||||
default:
|
||||
client.emit(
|
||||
Events.DEBUG,
|
||||
`[INTERACTION] Received component interaction with unknown type: ${data.data.component_type}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE:
|
||||
InteractionType = AutocompleteInteraction;
|
||||
break;
|
||||
default:
|
||||
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const interaction = new InteractionType(client, data);
|
||||
|
||||
/**
|
||||
* Emitted when an interaction is created.
|
||||
* @event Client#interactionCreate
|
||||
* @param {Interaction} interaction The interaction which was created
|
||||
*/
|
||||
client.emit(Events.INTERACTION_CREATE, interaction);
|
||||
|
||||
/**
|
||||
* Emitted when an interaction is created.
|
||||
* @event Client#interaction
|
||||
* @param {Interaction} interaction The interaction which was created
|
||||
* @deprecated Use {@link Client#event:interactionCreate} instead
|
||||
*/
|
||||
if (client.emit('interaction', interaction) && !deprecationEmitted) {
|
||||
deprecationEmitted = true;
|
||||
process.emitWarning('The interaction event is deprecated. Use interactionCreate instead', 'DeprecationWarning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InteractionCreateAction;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
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 = guild.invites._add(inviteData);
|
||||
|
||||
/**
|
||||
* Emitted when an invite is created.
|
||||
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
|
||||
* or `MANAGE_CHANNELS` 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;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'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) return false;
|
||||
|
||||
const inviteData = Object.assign(data, { channel, guild });
|
||||
const invite = new Invite(client, inviteData);
|
||||
guild.invites.cache.delete(invite.code);
|
||||
|
||||
/**
|
||||
* Emitted when an invite is deleted.
|
||||
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
|
||||
* or `MANAGE_CHANNELS` 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;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('node:process');
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
let deprecationEmitted = false;
|
||||
|
||||
class MessageCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = this.getChannel(data);
|
||||
if (channel) {
|
||||
if (!channel.isText()) return {};
|
||||
|
||||
const existing = channel.messages.cache.get(data.id);
|
||||
if (existing) return { message: existing };
|
||||
const message = channel.messages._add(data);
|
||||
channel.lastMessageId = data.id;
|
||||
|
||||
/**
|
||||
* Emitted whenever a message is created.
|
||||
* @event Client#messageCreate
|
||||
* @param {Message} message The created message
|
||||
*/
|
||||
client.emit(Events.MESSAGE_CREATE, message);
|
||||
|
||||
/**
|
||||
* Emitted whenever a message is created.
|
||||
* @event Client#message
|
||||
* @param {Message} message The created message
|
||||
* @deprecated Use {@link Client#event:messageCreate} instead
|
||||
*/
|
||||
if (client.emit('message', message) && !deprecationEmitted) {
|
||||
deprecationEmitted = true;
|
||||
process.emitWarning('The message event is deprecated. Use messageCreate instead', 'DeprecationWarning');
|
||||
}
|
||||
|
||||
return { message };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageCreateAction;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedMessages } = require('../../structures/Message');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class MessageDeleteAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = this.getChannel(data);
|
||||
let message;
|
||||
if (channel) {
|
||||
if (!channel.isText()) return {};
|
||||
|
||||
message = this.getMessage(data, channel);
|
||||
if (message) {
|
||||
channel.messages.cache.delete(message.id);
|
||||
deletedMessages.add(message);
|
||||
/**
|
||||
* 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;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Action = require('./Action');
|
||||
const { deletedMessages } = require('../../structures/Message');
|
||||
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) {
|
||||
if (!channel.isText()) return {};
|
||||
|
||||
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) {
|
||||
deletedMessages.add(message);
|
||||
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;
|
||||
+55
@@ -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: '�', id: null },
|
||||
channel_id: 'id',
|
||||
// If originating from a guild
|
||||
guild_id: 'id',
|
||||
member: { ..., user: { ... } } }
|
||||
*/
|
||||
|
||||
class MessageReactionAdd extends Action {
|
||||
handle(data, fromStructure = false) {
|
||||
if (!data.emoji) return false;
|
||||
|
||||
const user = this.getUserFromMember(data);
|
||||
if (!user) return false;
|
||||
|
||||
// Verify channel
|
||||
const channel = this.getChannel(data);
|
||||
if (!channel || !channel.isText()) return false;
|
||||
|
||||
// Verify message
|
||||
const message = this.getMessage(data, channel);
|
||||
if (!message) return false;
|
||||
|
||||
// Verify reaction
|
||||
const includePartial = this.client.options.partials.includes(PartialTypes.REACTION);
|
||||
if (message.partial && !includePartial) return false;
|
||||
const reaction = message.reactions._add({
|
||||
emoji: data.emoji,
|
||||
count: message.partial ? null : 0,
|
||||
me: user.id === this.client.user.id,
|
||||
});
|
||||
if (!reaction) return false;
|
||||
reaction._add(user);
|
||||
if (fromStructure) return { message, reaction, user };
|
||||
/**
|
||||
* Emitted whenever a reaction is added to a cached message.
|
||||
* @event Client#messageReactionAdd
|
||||
* @param {MessageReaction} messageReaction The reaction object
|
||||
* @param {User} user The user that applied the guild or reaction emoji
|
||||
*/
|
||||
this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user);
|
||||
|
||||
return { message, reaction, user };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageReactionAdd;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
/*
|
||||
{ user_id: 'id',
|
||||
message_id: 'id',
|
||||
emoji: { name: '�', 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.isText()) 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;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
'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.isText()) return false;
|
||||
|
||||
// Verify message
|
||||
const message = this.getMessage(data, channel);
|
||||
if (!message) return false;
|
||||
|
||||
// Copy removed reactions to emit for the event.
|
||||
const removed = message.reactions.cache.clone();
|
||||
|
||||
message.reactions.cache.clear();
|
||||
this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message, removed);
|
||||
|
||||
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
|
||||
* @param {Collection<string|Snowflake, MessageReaction>} reactions The cached message reactions that were removed.
|
||||
*/
|
||||
|
||||
module.exports = MessageReactionRemoveAll;
|
||||
+28
@@ -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.isText()) 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;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
|
||||
class MessageUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const channel = this.getChannel(data);
|
||||
if (channel) {
|
||||
if (!channel.isText()) return {};
|
||||
|
||||
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._update(data);
|
||||
return {
|
||||
old,
|
||||
updated: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageUpdateAction;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
'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?.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;
|
||||
|
||||
const oldPresence = guild.presences.cache.get(user.id)?._clone() ?? null;
|
||||
let member = guild.members.cache.get(user.id);
|
||||
if (!member && data.status !== 'offline') {
|
||||
member = guild.members._add({
|
||||
user,
|
||||
deaf: false,
|
||||
mute: false,
|
||||
});
|
||||
this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member);
|
||||
}
|
||||
const newPresence = guild.presences._add(Object.assign(data, { guild }));
|
||||
if (this.client.listenerCount(Events.PRESENCE_UPDATE) && !newPresence.equals(oldPresence)) {
|
||||
/**
|
||||
* 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, newPresence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PresenceUpdateAction;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class StageInstanceCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = this.getChannel(data);
|
||||
|
||||
if (channel) {
|
||||
const stageInstance = channel.guild.stageInstances._add(data);
|
||||
|
||||
/**
|
||||
* Emitted whenever a stage instance is created.
|
||||
* @event Client#stageInstanceCreate
|
||||
* @param {StageInstance} stageInstance The created stage instance
|
||||
*/
|
||||
client.emit(Events.STAGE_INSTANCE_CREATE, stageInstance);
|
||||
|
||||
return { stageInstance };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StageInstanceCreateAction;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedStageInstances } = require('../../structures/StageInstance');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class StageInstanceDeleteAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = this.getChannel(data);
|
||||
|
||||
if (channel) {
|
||||
const stageInstance = channel.guild.stageInstances._add(data);
|
||||
if (stageInstance) {
|
||||
channel.guild.stageInstances.cache.delete(stageInstance.id);
|
||||
deletedStageInstances.add(stageInstance);
|
||||
|
||||
/**
|
||||
* Emitted whenever a stage instance is deleted.
|
||||
* @event Client#stageInstanceDelete
|
||||
* @param {StageInstance} stageInstance The deleted stage instance
|
||||
*/
|
||||
client.emit(Events.STAGE_INSTANCE_DELETE, stageInstance);
|
||||
|
||||
return { stageInstance };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StageInstanceDeleteAction;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class StageInstanceUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = this.getChannel(data);
|
||||
|
||||
if (channel) {
|
||||
const oldStageInstance = channel.guild.stageInstances.cache.get(data.id)?._clone() ?? null;
|
||||
const newStageInstance = channel.guild.stageInstances._add(data);
|
||||
|
||||
/**
|
||||
* Emitted whenever a stage instance gets updated - e.g. change in topic or privacy level
|
||||
* @event Client#stageInstanceUpdate
|
||||
* @param {?StageInstance} oldStageInstance The stage instance before the update
|
||||
* @param {StageInstance} newStageInstance The stage instance after the update
|
||||
*/
|
||||
client.emit(Events.STAGE_INSTANCE_UPDATE, oldStageInstance, newStageInstance);
|
||||
|
||||
return { oldStageInstance, newStageInstance };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StageInstanceUpdateAction;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ThreadCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const existing = client.channels.cache.has(data.id);
|
||||
const thread = client.channels._add(data);
|
||||
if (!existing && thread) {
|
||||
/**
|
||||
* Emitted whenever a thread is created or when the client user is added to a thread.
|
||||
* @event Client#threadCreate
|
||||
* @param {ThreadChannel} thread The thread that was created
|
||||
*/
|
||||
client.emit(Events.THREAD_CREATE, thread);
|
||||
}
|
||||
return { thread };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThreadCreateAction;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { deletedChannels } = require('../../structures/Channel');
|
||||
const { deletedMessages } = require('../../structures/Message');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ThreadDeleteAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const thread = client.channels.cache.get(data.id);
|
||||
|
||||
if (thread) {
|
||||
client.channels._remove(thread.id);
|
||||
deletedChannels.add(thread);
|
||||
for (const message of thread.messages.cache.values()) {
|
||||
deletedMessages.add(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a thread is deleted.
|
||||
* @event Client#threadDelete
|
||||
* @param {ThreadChannel} thread The thread that was deleted
|
||||
*/
|
||||
client.emit(Events.THREAD_DELETE, thread);
|
||||
}
|
||||
|
||||
return { thread };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThreadDeleteAction;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ThreadListSyncAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
if (!guild) return {};
|
||||
|
||||
if (data.channel_ids) {
|
||||
for (const id of data.channel_ids) {
|
||||
const channel = client.channels.resolve(id);
|
||||
if (channel) this.removeStale(channel);
|
||||
}
|
||||
} else {
|
||||
for (const channel of guild.channels.cache.values()) {
|
||||
this.removeStale(channel);
|
||||
}
|
||||
}
|
||||
|
||||
const syncedThreads = data.threads.reduce((coll, rawThread) => {
|
||||
const thread = client.channels._add(rawThread);
|
||||
return coll.set(thread.id, thread);
|
||||
}, new Collection());
|
||||
|
||||
for (const rawMember of Object.values(data.members)) {
|
||||
// Discord sends the thread id as id in this object
|
||||
const thread = client.channels.cache.get(rawMember.id);
|
||||
if (thread) {
|
||||
thread.members._add(rawMember);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever the client user gains access to a text or news channel that contains threads
|
||||
* @event Client#threadListSync
|
||||
* @param {Collection<Snowflake, ThreadChannel>} threads The threads that were synced
|
||||
*/
|
||||
client.emit(Events.THREAD_LIST_SYNC, syncedThreads);
|
||||
|
||||
return {
|
||||
syncedThreads,
|
||||
};
|
||||
}
|
||||
|
||||
removeStale(channel) {
|
||||
channel.threads?.cache.forEach(thread => {
|
||||
if (!thread.archived) {
|
||||
this.client.channels._remove(thread.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThreadListSyncAction;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ThreadMemberUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
// Discord sends the thread id as id in this object
|
||||
const thread = client.channels.cache.get(data.id);
|
||||
if (thread) {
|
||||
const member = thread.members.cache.get(data.user_id);
|
||||
if (!member) {
|
||||
const newMember = thread.members._add(data);
|
||||
return { newMember };
|
||||
}
|
||||
const old = member._update(data);
|
||||
/**
|
||||
* Emitted whenever the client user's thread member is updated.
|
||||
* @event Client#threadMemberUpdate
|
||||
* @param {ThreadMember} oldMember The member before the update
|
||||
* @param {ThreadMember} newMember The member after the update
|
||||
*/
|
||||
client.emit(Events.THREAD_MEMBER_UPDATE, old, member);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThreadMemberUpdateAction;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class ThreadMembersUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const thread = client.channels.cache.get(data.id);
|
||||
if (thread) {
|
||||
const old = thread.members.cache.clone();
|
||||
thread.memberCount = data.member_count;
|
||||
|
||||
data.added_members?.forEach(rawMember => {
|
||||
thread.members._add(rawMember);
|
||||
});
|
||||
|
||||
data.removed_member_ids?.forEach(memberId => {
|
||||
thread.members.cache.delete(memberId);
|
||||
});
|
||||
|
||||
/**
|
||||
* Emitted whenever members are added or removed from a thread. Requires `GUILD_MEMBERS` privileged intent
|
||||
* @event Client#threadMembersUpdate
|
||||
* @param {Collection<Snowflake, ThreadMember>} oldMembers The members before the update
|
||||
* @param {Collection<Snowflake, ThreadMember>} newMembers The members after the update
|
||||
*/
|
||||
client.emit(Events.THREAD_MEMBERS_UPDATE, old, thread.members.cache);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThreadMembersUpdateAction;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const Typing = require('../../structures/Typing');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class TypingStart extends Action {
|
||||
handle(data) {
|
||||
const channel = this.getChannel(data);
|
||||
if (!channel) return;
|
||||
|
||||
if (!channel.isText()) {
|
||||
this.client.emit(Events.WARN, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = this.getUserFromMember(data);
|
||||
if (user) {
|
||||
/**
|
||||
* Emitted whenever a user starts typing in a channel.
|
||||
* @event Client#typingStart
|
||||
* @param {Typing} typing The typing state
|
||||
*/
|
||||
this.client.emit(Events.TYPING_START, new Typing(channel, user, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TypingStart;
|
||||
+35
@@ -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 = data.id === client.user.id ? client.user : 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;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const VoiceState = require('../../structures/VoiceState');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class VoiceStateUpdate extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
if (guild) {
|
||||
// Update the state
|
||||
const oldState =
|
||||
guild.voiceStates.cache.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?.user && data.member.joined_at) {
|
||||
member = guild.members._add(data.member);
|
||||
}
|
||||
|
||||
// Emit event
|
||||
if (member?.user.id === client.user.id) {
|
||||
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
|
||||
client.voice.onVoiceStateUpdate(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
|
||||
* @event Client#voiceStateUpdate
|
||||
* @param {VoiceState} oldState The voice state before the update
|
||||
* @param {VoiceState} newState The voice state after the update
|
||||
*/
|
||||
client.emit(Events.VOICE_STATE_UPDATE, oldState, newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VoiceStateUpdate;
|
||||
+19
@@ -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 channel has its webhooks changed.
|
||||
* @event Client#webhookUpdate
|
||||
* @param {TextChannel|NewsChannel} channel The channel that had a webhook update
|
||||
*/
|
||||
if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebhooksUpdate;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
|
||||
/**
|
||||
* Maps guild ids to voice adapters created for use with @discordjs/voice.
|
||||
* @type {Map<Snowflake, Object>}
|
||||
*/
|
||||
this.adapters = new Map();
|
||||
|
||||
client.on(Events.SHARD_DISCONNECT, (_, shardId) => {
|
||||
for (const [guildId, adapter] of this.adapters.entries()) {
|
||||
if (client.guilds.cache.get(guildId)?.shardId === shardId) {
|
||||
adapter.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onVoiceServer(payload) {
|
||||
this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
|
||||
}
|
||||
|
||||
onVoiceStateUpdate(payload) {
|
||||
if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
|
||||
this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientVoiceManager;
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('node:events');
|
||||
const { setImmediate } = require('node:timers');
|
||||
const { setTimeout: sleep } = require('node:timers/promises');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { RPCErrorCodes } = require('discord-api-types/v9');
|
||||
const WebSocketShard = require('./WebSocketShard');
|
||||
const PacketHandlers = require('./handlers');
|
||||
const { Error } = require('../../errors');
|
||||
const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants');
|
||||
|
||||
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 = [
|
||||
RPCErrorCodes.UnknownError,
|
||||
RPCErrorCodes.InvalidPermissions,
|
||||
RPCErrorCodes.InvalidClientId,
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 {Status}
|
||||
*/
|
||||
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 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 Error(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;
|
||||
});
|
||||
|
||||
const { total, remaining } = 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)));
|
||||
|
||||
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<Snowflake>} 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 === 1_000 ? 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();
|
||||
} 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?.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) {
|
||||
throw new Error(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 sleep(5_000);
|
||||
return this.createShards();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles reconnects for this manager.
|
||||
* @private
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async reconnect() {
|
||||
if (this.reconnecting || this.status !== Status.READY) return false;
|
||||
this.reconnecting = true;
|
||||
try {
|
||||
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 sleep(5_000);
|
||||
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: 1_000, reset: true, emit: false, log: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
setImmediate(() => {
|
||||
this.handlePacket(item.packet, item.shard);
|
||||
}).unref();
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
checkShardsReady() {
|
||||
if (this.status === Status.READY) return;
|
||||
if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
* @param {Client} client The client
|
||||
*/
|
||||
this.client.emit(Events.CLIENT_READY, this.client);
|
||||
|
||||
this.handlePacket();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketManager;
|
||||
+786
@@ -0,0 +1,786 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('node:events');
|
||||
const { setTimeout, setInterval } = require('node:timers');
|
||||
const WebSocket = require('../../WebSocket');
|
||||
const { Status, Events, ShardEvents, Opcodes, WSEvents } = require('../../util/Constants');
|
||||
const Intents = require('../../util/Intents');
|
||||
|
||||
const STATUS_KEYS = Object.keys(Status);
|
||||
const CONNECTION_STATE = Object.keys(WebSocket.WebSocket);
|
||||
|
||||
let zlib;
|
||||
|
||||
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 shard's id
|
||||
* @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?.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?.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] Took ${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);
|
||||
} catch (err) {
|
||||
this.manager.client.emit(Events.SHARD_ERROR, err, this.id);
|
||||
return;
|
||||
}
|
||||
this.manager.client.emit(Events.RAW, packet, this.id);
|
||||
if (packet.op === Opcodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id);
|
||||
this.onPacket(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever an error occurs with the WebSocket.
|
||||
* @param {ErrorEvent} event The error that occurred
|
||||
* @private
|
||||
*/
|
||||
onError(event) {
|
||||
const 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: 4_000 });
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
const hasGuildsIntent = new Intents(this.manager.client.options.intents).has(Intents.FLAGS.GUILDS);
|
||||
// Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds
|
||||
// * The timeout is 15 seconds by default
|
||||
// * This can be optionally changed in the client options via the `waitGuildTimeout` option
|
||||
// * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds.
|
||||
|
||||
const { waitGuildTimeout } = this.manager.client.options;
|
||||
|
||||
this.readyTimeout = setTimeout(
|
||||
() => {
|
||||
this.debug(
|
||||
`Shard ${hasGuildsIntent ? 'did' : 'will'} not receive any more guild packets` +
|
||||
`${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${
|
||||
this.expectedGuilds.size
|
||||
}`,
|
||||
);
|
||||
|
||||
this.readyTimeout = null;
|
||||
|
||||
this.status = Status.READY;
|
||||
|
||||
this.emit(ShardEvents.ALL_READY, this.expectedGuilds);
|
||||
},
|
||||
hasGuildsIntent ? waitGuildTimeout : 0,
|
||||
).unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HELLO packet timeout.
|
||||
* @param {number} [time] If set to -1, it will clear the hello timeout
|
||||
* @private
|
||||
*/
|
||||
setHelloTimeout(time) {
|
||||
if (time === -1) {
|
||||
if (this.helloTimeout) {
|
||||
this.debug('Clearing the HELLO timeout.');
|
||||
clearTimeout(this.helloTimeout);
|
||||
this.helloTimeout = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.debug('Setting a HELLO timeout for 20s.');
|
||||
this.helloTimeout = setTimeout(() => {
|
||||
this.debug('Did not receive HELLO in time. Destroying and connecting again.');
|
||||
this.destroy({ reset: true, closeCode: 4009 });
|
||||
}, 20_000).unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.debug(`Setting a heartbeat interval for ${time}ms.`);
|
||||
// Sanity checks
|
||||
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), time).unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
intents: Intents.resolve(client.options.intents),
|
||||
token: client.token,
|
||||
shard: [this.id, Number(client.options.shardCount)],
|
||||
};
|
||||
|
||||
this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: ${d.intents}`);
|
||||
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?.readyState !== WebSocket.OPEN) {
|
||||
this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`);
|
||||
this.destroy({ closeCode: 4_000 });
|
||||
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 = setTimeout(() => {
|
||||
this.ratelimit.remaining = this.ratelimit.total;
|
||||
this.processQueue();
|
||||
}, this.ratelimit.time).unref();
|
||||
}
|
||||
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 = 1_000, 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 rate limit data
|
||||
this.ratelimit.remaining = this.ratelimit.total;
|
||||
this.ratelimit.queue.length = 0;
|
||||
if (this.ratelimit.timer) {
|
||||
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;
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands;
|
||||
if (!commandManager) return;
|
||||
|
||||
const command = commandManager._add(data, data.application_id === client.application.id);
|
||||
|
||||
/**
|
||||
* Emitted when a guild application command is created.
|
||||
* @event Client#applicationCommandCreate
|
||||
* @param {ApplicationCommand} command The command which was created
|
||||
* @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information.
|
||||
*/
|
||||
client.emit(Events.APPLICATION_COMMAND_CREATE, command);
|
||||
};
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands;
|
||||
if (!commandManager) return;
|
||||
|
||||
const isOwn = data.application_id === client.application.id;
|
||||
const command = commandManager._add(data, isOwn);
|
||||
if (isOwn) commandManager.cache.delete(data.id);
|
||||
|
||||
/**
|
||||
* Emitted when a guild application command is deleted.
|
||||
* @event Client#applicationCommandDelete
|
||||
* @param {ApplicationCommand} command The command which was deleted
|
||||
* @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information.
|
||||
*/
|
||||
client.emit(Events.APPLICATION_COMMAND_DELETE, command);
|
||||
};
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands;
|
||||
if (!commandManager) return;
|
||||
|
||||
const oldCommand = commandManager.cache.get(data.id)?._clone() ?? null;
|
||||
const newCommand = commandManager._add(data, data.application_id === client.application.id);
|
||||
|
||||
/**
|
||||
* Emitted when a guild application command is updated.
|
||||
* @event Client#applicationCommandUpdate
|
||||
* @param {?ApplicationCommand} oldCommand The command before the update
|
||||
* @param {ApplicationCommand} newCommand The command after the update
|
||||
* @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information.
|
||||
*/
|
||||
client.emit(Events.APPLICATION_COMMAND_UPDATE, oldCommand, newCommand);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.ChannelCreate.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.ChannelDelete.handle(packet.d);
|
||||
};
|
||||
+22
@@ -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 = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
|
||||
|
||||
if (channel) {
|
||||
// Discord sends null for last_pin_timestamp if the last pinned message was removed
|
||||
channel.lastPinTimestamp = time;
|
||||
|
||||
/**
|
||||
* Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event,
|
||||
* not much information can be provided easily here - you need to manually check the pins yourself.
|
||||
* @event Client#channelPinsUpdate
|
||||
* @param {TextBasedChannels} channel The channel that the pins update occurred in
|
||||
* @param {Date} time The time of the pins update
|
||||
*/
|
||||
client.emit(Events.CHANNEL_PINS_UPDATE, channel, time);
|
||||
}
|
||||
};
|
||||
+16
@@ -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);
|
||||
}
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildBanAdd.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildBanRemove.handle(packet.d);
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const { Events, Status } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (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);
|
||||
}
|
||||
} 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
|
||||
*/
|
||||
client.emit(Events.GUILD_CREATE, guild);
|
||||
}
|
||||
}
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildDelete.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildEmojisUpdate.handle(packet.d);
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildIntegrationsUpdate.handle(packet.d);
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/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 }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the properties of a guild members chunk
|
||||
* @typedef {Object} GuildMembersChunk
|
||||
* @property {number} index Index of the received chunk
|
||||
* @property {number} count Number of chunks the client should receive
|
||||
* @property {?string} nonce Nonce for this chunk
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 {GuildMembersChunk} chunk Properties of the received chunk
|
||||
*/
|
||||
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild, {
|
||||
count: data.chunk_count,
|
||||
index: data.chunk_index,
|
||||
nonce: data.nonce,
|
||||
});
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const { Events, Status } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }, shard) => {
|
||||
const guild = client.guilds.cache.get(data.guild_id);
|
||||
if (guild) {
|
||||
guild.memberCount++;
|
||||
const member = guild.members._add(data);
|
||||
if (shard.status === Status.READY) {
|
||||
/**
|
||||
* Emitted whenever a user joins a guild.
|
||||
* @event Client#guildMemberAdd
|
||||
* @param {GuildMember} member The member that has joined a guild
|
||||
*/
|
||||
client.emit(Events.GUILD_MEMBER_ADD, member);
|
||||
}
|
||||
}
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet, shard) => {
|
||||
client.actions.GuildMemberRemove.handle(packet.d, shard);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet, shard) => {
|
||||
client.actions.GuildMemberUpdate.handle(packet.d, shard);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildRoleCreate.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildRoleDelete.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildRoleUpdate.handle(packet.d);
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildScheduledEventCreate.handle(packet.d);
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildScheduledEventDelete.handle(packet.d);
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildScheduledEventUpdate.handle(packet.d);
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildScheduledEventUserAdd.handle(packet.d);
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildScheduledEventUserRemove.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildStickersUpdate.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.GuildUpdate.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.InteractionCreate.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.InviteCreate.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.InviteDelete.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessageCreate.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessageDelete.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessageDeleteBulk.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessageReactionAdd.handle(packet.d);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessageReactionRemove.handle(packet.d);
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessageReactionRemoveAll.handle(packet.d);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user