Compare commits

...

12 Commits

Author SHA1 Message Date
MythEclipse
66e212677c fix: update TypeScript module settings to Node16 and adjust root directory
Some checks failed
Lint / ESLint (push) Has been cancelled
2026-05-15 07:16:20 +07:00
MythEclipse
6e947f299e feat: update rawDataTypes with new API components and types; add biome configuration file
Some checks failed
Lint / ESLint (push) Has been cancelled
2026-05-15 06:26:08 +07:00
Elysia
bf38318902 sob
Some checks failed
Lint / ESLint (push) Has been cancelled
2025-10-11 17:43:36 +07:00
Elysia
f5a0515731 chore: update UA & ws prop 2025-10-11 17:28:41 +07:00
Elysia
b519264a69 feat: more invite flags 2025-10-11 17:18:50 +07:00
Elysia
d9d11bd6b7 fix: Ensure discriminator detection respects webhooks too
#11062 djs
2025-10-11 17:13:27 +07:00
Elysia
4d4fa1b17d fix(GuildChannel): account for everyone base permissions
backport #11053
2025-09-13 18:13:14 +07:00
Elysia
2fe0ad486d fix: Remove trailing color references
#11007
2025-09-13 18:11:53 +07:00
Elysia
f0ec7d977c types(Invite): Approximate fields should be nullable
#10997
2025-09-13 18:05:19 +07:00
Elysia
6fbb62ac64 feat: support user guilds
backport #10995
2025-09-13 18:04:34 +07:00
Elysia
ad64e2be11 feat(MessageManager): New pinned messages routes
backport #10993 ?
Note: This commit does not fully implement the features of the new endpoint; it only updates the endpoint and automatically processes data to be compatible with the old endpoint.
2025-09-13 17:55:52 +07:00
Elysia
0d45d3ddb9 feat(User): add collectibles
#10939
2025-09-13 17:44:51 +07:00
212 changed files with 8552 additions and 3123 deletions

View File

@@ -1,290 +0,0 @@
{
"extends": [
"eslint:recommended",
"plugin:prettier/recommended"
],
"plugins": [
"import"
],
"parserOptions": {
"ecmaVersion": 13
},
"env": {
"es2021": true,
"node": true
},
"ignorePatterns": [
"**/src/util/Voice.js",
"**/examples/**"
],
"rules": {
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"index",
"sibling",
"parent"
],
"alphabetize": {
"order": "asc"
}
}
],
"prettier/prettier": [
2,
{
"printWidth": 120,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf",
"arrowParens": "avoid"
}
],
"strict": [
"error",
"global"
],
// "no-await-in-loop": "warn",
"no-compare-neg-zero": "error",
"no-template-curly-in-string": "error",
"no-unsafe-negation": "error",
"valid-jsdoc": [
"error",
{
"requireReturn": false,
"requireReturnDescription": false,
"prefer": {
"return": "returns",
"arg": "param"
},
"preferType": {
"String": "string",
"Number": "number",
"Boolean": "boolean",
"Symbol": "symbol",
"object": "Object",
"function": "Function",
"array": "Array",
"date": "Date",
"error": "Error",
"null": "void"
}
}
],
"accessor-pairs": "warn",
"array-callback-return": "error",
"consistent-return": "error",
"curly": [
"error",
"multi-line",
"consistent"
],
"dot-location": [
"error",
"property"
],
"dot-notation": "error",
"eqeqeq": "off",
// "no-empty-function": "error",
"no-floating-decimal": "error",
"no-implied-eval": "error",
"no-invalid-this": "error",
"no-lone-blocks": "error",
"no-multi-spaces": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-new": "error",
"no-octal-escape": "error",
"no-return-assign": "error",
"no-return-await": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-useless-call": "error",
"no-useless-concat": "error",
"no-useless-escape": "error",
"no-useless-return": "error",
"no-void": "error",
// "no-warning-comments": "warn",
"prefer-promise-reject-errors": "error",
"require-await": "off",
"wrap-iife": "error",
"yoda": "error",
"no-label-var": "error",
// "no-shadow": "error",
"no-undef-init": "error",
"callback-return": "error",
"getter-return": "off",
"handle-callback-err": "error",
"no-mixed-requires": "error",
"no-new-require": "error",
"no-path-concat": "error",
"array-bracket-spacing": "error",
"block-spacing": "error",
"brace-style": [
"error",
"1tbs",
{
"allowSingleLine": true
}
],
"capitalized-comments": [
"error",
"always",
{
"ignoreConsecutiveComments": true
}
],
"comma-dangle": [
"error",
"always-multiline"
],
"comma-spacing": "error",
"comma-style": "error",
"computed-property-spacing": "error",
"consistent-this": [
"error",
"$this"
],
"eol-last": "error",
"func-names": "error",
"func-name-matching": "error",
"func-style": [
"error",
"declaration",
{
"allowArrowFunctions": true
}
],
"key-spacing": "error",
"keyword-spacing": "error",
"max-depth": "error",
"max-len": [
"off",
120,
2
],
"max-nested-callbacks": [
"error",
{
"max": 4
}
],
"max-statements-per-line": [
"error",
{
"max": 2
}
],
"new-cap": "off",
"newline-per-chained-call": [
"error",
{
"ignoreChainWithDepth": 4
}
],
"no-array-constructor": "error",
"no-inline-comments": "off",
"no-lonely-if": "error",
"no-multiple-empty-lines": [
"error",
{
"max": 2,
"maxEOF": 1,
"maxBOF": 0
}
],
"no-new-object": "error",
"no-spaced-func": "error",
"no-trailing-spaces": "error",
"no-unneeded-ternary": "error",
"no-whitespace-before-property": "error",
"nonblock-statement-body-position": "error",
"object-curly-spacing": [
"error",
"always"
],
"operator-assignment": "error",
"padded-blocks": [
"error",
"never"
],
"quote-props": [
"error",
"as-needed"
],
"quotes": [
"error",
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"semi-spacing": "error",
"semi": "error",
"space-before-blocks": "error",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"template-tag-spacing": "error",
"unicode-bom": "error",
"arrow-body-style": "error",
"arrow-parens": [
"error",
"as-needed"
],
"arrow-spacing": "error",
"no-duplicate-imports": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"prefer-arrow-callback": "error",
"prefer-numeric-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"rest-spread-spacing": "error",
"template-curly-spacing": "error",
"yield-star-spacing": "error",
"no-restricted-globals": [
"error",
{
"name": "Buffer",
"message": "Import Buffer from `node:buffer` instead"
},
{
"name": "process",
"message": "Import process from `node:process` instead"
},
{
"name": "setTimeout",
"message": "Import setTimeout from `node:timers` instead"
},
{
"name": "setInterval",
"message": "Import setInterval from `node:timers` instead"
},
{
"name": "setImmediate",
"message": "Import setImmediate from `node:timers` instead"
}
],
"linebreak-style": 0
}
}

View File

@@ -1,7 +0,0 @@
{
"singleQuote": true,
"printWidth": 120,
"trailingComma": "all",
"endOfLine": "lf",
"arrowParens": "avoid"
}

View File

@@ -1,3 +1,10 @@
> [!IMPORTANT]
> ## Project Archival
>
> **This project is no longer actively maintained and this repository has been archived.**
>
> You can read the full announcement [here](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/discussions/1743)
<div align="center"> <div align="center">
<br /> <br />
<p> <p>
@@ -30,11 +37,6 @@
> [!CAUTION] > [!CAUTION]
> **Using this on a user account is prohibited by the [Discord TOS](https://discord.com/terms) and can lead to the account block.** > **Using this on a user account is prohibited by the [Discord TOS](https://discord.com/terms) and can lead to the account block.**
## Project Status
> [!IMPORTANT]
> `discord.js-selfbot-v13` is currently in maintenance mode. New features are not actively being added but existing features and new versions of discord are supported as possible. There are some major architectural changes which need to be added to improve the stability and security of the project. I don't have as much spare time as I did when I started this project, so there is not currently any plan for these improvements.
### <strong>[Document Website](https://discordjs-self-v13.netlify.app/)</strong> ### <strong>[Document Website](https://discordjs-self-v13.netlify.app/)</strong>
### <strong>[Example Code](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/tree/main/examples)</strong> ### <strong>[Example Code](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/tree/main/examples)</strong>

29
biome.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"files": {
"includes": ["src/**/*.js", "typings/**/*.ts", "*.json"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "always"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"style": {
"useNodejsImportProtocol": "warn"
},
"suspicious": {
"noExplicitAny": "warn"
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
// No longer using 2captcha since the website no longer supports hCaptcha, which Discord uses.
const Captcha = require('2captcha'); const Captcha = require('2captcha');
const Discord = require('../src/index'); const Discord = require('../src/index');

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
// No longer using 2captcha since the website no longer supports hCaptcha, which Discord uses.
const Captcha = require('2captcha'); const Captcha = require('2captcha');
const Discord = require('../src/index'); const Discord = require('../src/index');

View File

@@ -1,23 +1,19 @@
{ {
"name": "discord.js-selfbot-v13", "name": "discord.js-selfbot-v13",
"version": "3.7.0", "version": "3.7.1",
"description": "An unofficial discord.js fork for creating selfbots", "description": "An unofficial discord.js fork for creating selfbots",
"main": "./src/index.js", "main": "./src/index.js",
"types": "./typings/index.d.ts", "types": "./typings/index.d.ts",
"scripts": { "scripts": {
"all": "npm run build && npm publish", "all": "npm run build && npm publish",
"test": "npm run lint:all && npm run docs:test && npm run test:typescript", "test": "npm run lint && npm run test:typescript && npm run docs:test",
"fix:all": "npm run lint:fix && npm run lint:typings:fix && npm run format", "fix:all": "npm run format",
"test:typescript": "tsc --noEmit && tsd", "test:typescript": "tsc --noEmit && tsd",
"lint": "eslint .", "lint": "biome check . --diagnostic-level=error",
"lint:fix": "eslint . --fix", "format": "biome format --write .",
"lint:typings": "tslint typings/index.d.ts",
"lint:typings:fix": "tslint typings/index.d.ts --fix",
"format": "prettier --write src/**/*.js typings/**/*.ts",
"lint:all": "npm run lint && npm run lint:typings",
"docs": "docgen --source src --custom docs/index.yml --output docs/main.json", "docs": "docgen --source src --custom docs/index.yml --output docs/main.json",
"docs:test": "docgen --source src --custom docs/index.yml", "docs:test": "docgen --source src --custom docs/index.yml",
"build": "npm run lint:fix && npm run lint:typings:fix && npm run format && npm run docs" "build": "npm run format && npm run docs"
}, },
"files": [ "files": [
"src", "src",
@@ -51,39 +47,33 @@
}, },
"homepage": "https://github.com/aiko-chan-ai/discord.js-selfbot-v13#readme", "homepage": "https://github.com/aiko-chan-ai/discord.js-selfbot-v13#readme",
"dependencies": { "dependencies": {
"@discordjs/builders": "^1.6.3", "@discordjs/builders": "^1.13.0",
"@discordjs/collection": "^2.1.1", "@discordjs/collection": "^2.1.1",
"@sapphire/async-queue": "^1.5.5", "@sapphire/async-queue": "^1.5.5",
"@sapphire/shapeshift": "^4.0.0", "@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.15", "discord-api-types": "^0.38.38",
"fetch-cookie": "^3.1.0", "fetch-cookie": "^3.1.0",
"find-process": "^2.0.0", "find-process": "^2.0.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"prism-media": "^1.3.5", "prism-media": "^2.0.0-alpha.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"tough-cookie": "^5.1.2", "tough-cookie": "^5.1.2",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"undici": "^7.11.0", "undici": "^7.16.0",
"werift-rtp": "^0.8.4", "werift-rtp": "^0.8.4",
"ws": "^8.16.0" "ws": "^8.20.0"
}, },
"engines": { "engines": {
"node": ">=20.18" "node": ">=20.18"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "latest",
"@discordjs/docgen": "^0.11.1", "@discordjs/docgen": "^0.11.1",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/node": "^22.10.7", "@types/node": "^25.8.0",
"@types/ws": "^8.5.10", "@types/ws": "^8.18.1",
"dtslint": "^4.2.1", "patch-package": "^8.0.1",
"eslint": "^8.39.0", "tsd": "^0.33.0",
"eslint-config-prettier": "^8.8.0", "typescript": "^5.9.3"
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"patch-package": "^8.0.0",
"prettier": "^2.8.8",
"tsd": "^0.32.0",
"tslint": "^6.1.3",
"typescript": "^5.5.4"
} }
} }

View File

@@ -36,4 +36,5 @@ exports.create = (gateway, query = {}, ...args) => {
return ws; return ws;
}; };
for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) exports[state] = exports.WebSocket[state]; for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'])
exports[state] = exports.WebSocket[state];

View File

@@ -228,7 +228,9 @@ class Client extends BaseClient {
get emojis() { get emojis() {
const emojis = new BaseGuildEmojiManager(this); const emojis = new BaseGuildEmojiManager(this);
for (const guild of this.guilds.cache.values()) { 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); if (guild.available)
for (const emoji of guild.emojis.cache.values())
emojis.cache.set(emoji.id, emoji);
} }
return emojis; return emojis;
} }
@@ -312,7 +314,13 @@ class Client extends BaseClient {
const initial = await this.api.auth.login.post({ const initial = await this.api.auth.login.post({
auth: false, auth: false,
versioned: true, versioned: true,
data: { gift_code_sku_id: null, login_source: null, undelete: false, login: email, password }, data: {
gift_code_sku_id: null,
login_source: null,
undelete: false,
login: email,
password,
},
}); });
if ('token' in initial) { if ('token' in initial) {
@@ -323,7 +331,12 @@ class Client extends BaseClient {
const totp = await this.api.auth.mfa.totp.post({ const totp = await this.api.auth.mfa.totp.post({
auth: false, auth: false,
versioned: true, versioned: true,
data: { gift_code_sku_id: null, login_source: null, code: otp, ticket: initial.ticket }, data: {
gift_code_sku_id: null,
login_source: null,
code: otp,
ticket: initial.ticket,
},
}); });
if ('token' in totp) { if ('token' in totp) {
return this.login(totp.token); return this.login(totp.token);
@@ -393,7 +406,10 @@ class Client extends BaseClient {
async fetchInvite(invite, options) { async fetchInvite(invite, options) {
const code = DataResolver.resolveInviteCode(invite); const code = DataResolver.resolveInviteCode(invite);
const data = await this.api.invites(code).get({ const data = await this.api.invites(code).get({
query: { with_counts: true, guild_scheduled_event_id: options?.guildScheduledEventId }, query: {
with_counts: true,
guild_scheduled_event_id: options?.guildScheduledEventId,
},
}); });
return new Invite(this, data); return new Invite(this, data);
} }
@@ -439,7 +455,8 @@ class Client extends BaseClient {
async fetchVoiceRegions() { async fetchVoiceRegions() {
const apiRegions = await this.api.voice.regions.get(); const apiRegions = await this.api.voice.regions.get();
const regions = new Collection(); const regions = new Collection();
for (const region of apiRegions) regions.set(region.id, new VoiceRegion(region)); for (const region of apiRegions)
regions.set(region.id, new VoiceRegion(region));
return regions; return regions;
} }
@@ -467,7 +484,9 @@ class Client extends BaseClient {
*/ */
async fetchPremiumStickerPacks() { async fetchPremiumStickerPacks() {
const data = await this.api('sticker-packs').get(); const data = await this.api('sticker-packs').get();
return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)])); return new Collection(
data.sticker_packs.map((p) => [p.id, new StickerPack(this, p)]),
);
} }
/** /**
* A last ditch cleanup function for garbage collection. * A last ditch cleanup function for garbage collection.
@@ -484,7 +503,10 @@ class Client extends BaseClient {
this.emit(Events.DEBUG, message); this.emit(Events.DEBUG, message);
} }
} catch { } catch {
this.emit(Events.DEBUG, `Garbage collection failed on ${name ?? 'an unknown item'}.`); this.emit(
Events.DEBUG,
`Garbage collection failed on ${name ?? 'an unknown item'}.`,
);
} }
} }
@@ -509,8 +531,13 @@ class Client extends BaseClient {
return -1; return -1;
} }
const messages = this.sweepers.sweepMessages(Sweepers.outdatedMessageSweepFilter(lifetime)()); const messages = this.sweepers.sweepMessages(
this.emit(Events.DEBUG, `Swept ${messages} messages older than ${lifetime} seconds`); Sweepers.outdatedMessageSweepFilter(lifetime)(),
);
this.emit(
Events.DEBUG,
`Swept ${messages} messages older than ${lifetime} seconds`,
);
return messages; return messages;
} }
@@ -545,7 +572,7 @@ class Client extends BaseClient {
*/ */
async refreshAttachmentURL(...urls) { async refreshAttachmentURL(...urls) {
// Clean up the URLs // Clean up the URLs
urls = urls.map(url => { urls = urls.map((url) => {
const urlObject = new URL(url); const urlObject = new URL(url);
// Clean query // Clean query
urlObject.search = ''; urlObject.search = '';
@@ -583,7 +610,7 @@ class Client extends BaseClient {
* @returns {void} The `sleep` function is returning a Promise. * @returns {void} The `sleep` function is returning a Promise.
*/ */
sleep(timeout) { sleep(timeout) {
return new Promise(r => setTimeout(r, timeout)); return new Promise((r) => setTimeout(r, timeout));
} }
toJSON() { toJSON() {
@@ -615,13 +642,18 @@ class Client extends BaseClient {
* @example * @example
* await client.acceptInvite('https://discord.gg/genshinimpact', { bypassOnboarding: true, bypassVerify: true }) * await client.acceptInvite('https://discord.gg/genshinimpact', { bypassOnboarding: true, bypassVerify: true })
*/ */
async acceptInvite(invite, options = { bypassOnboarding: true, bypassVerify: true }) { async acceptInvite(
invite,
options = { bypassOnboarding: true, bypassVerify: true },
) {
// ! throw new Error('METHOD_WARNING'); // ! throw new Error('METHOD_WARNING');
const code = DataResolver.resolveInviteCode(invite); const code = DataResolver.resolveInviteCode(invite);
if (!code) throw new Error('INVITE_RESOLVE_CODE'); if (!code) throw new Error('INVITE_RESOLVE_CODE');
const i = await this.fetchInvite(code); const i = await this.fetchInvite(code);
if (i.guild?.id && this.guilds.cache.has(i.guild?.id)) return this.guilds.cache.get(i.guild?.id); if (i.guild?.id && this.guilds.cache.has(i.guild?.id))
if (this.channels.cache.has(i.channelId)) return this.channels.cache.get(i.channelId); return this.guilds.cache.get(i.guild?.id);
if (this.channels.cache.has(i.channelId))
return this.channels.cache.get(i.channelId);
const data = await this.api.invites(code).post({ const data = await this.api.invites(code).post({
DiscordContext: { location: 'Markdown Link' }, DiscordContext: { location: 'Markdown Link' },
data: { data: {
@@ -637,10 +669,11 @@ class Client extends BaseClient {
return guild; return guild;
} }
if (options.bypassOnboarding) { if (options.bypassOnboarding) {
const onboardingData = await this.api.guilds[i.guild?.id].onboarding.get(); const onboardingData =
await this.api.guilds[i.guild?.id].onboarding.get();
// Onboarding // Onboarding
if (onboardingData.enabled) { if (onboardingData.enabled) {
const prompts = onboardingData.prompts.filter(o => o.in_onboarding); const prompts = onboardingData.prompts.filter((o) => o.in_onboarding);
if (prompts.length) { if (prompts.length) {
const onboarding_prompts_seen = {}; const onboarding_prompts_seen = {};
const onboarding_responses = []; const onboarding_responses = [];
@@ -648,10 +681,11 @@ class Client extends BaseClient {
const currentDate = Date.now(); const currentDate = Date.now();
prompts.forEach(prompt => { prompts.forEach((prompt) => {
onboarding_prompts_seen[prompt.id] = currentDate; onboarding_prompts_seen[prompt.id] = currentDate;
if (prompt.required) onboarding_responses.push(prompt.options[0].id); if (prompt.required)
prompt.options.forEach(option => { onboarding_responses.push(prompt.options[0].id);
prompt.options.forEach((option) => {
onboarding_responses_seen[option.id] = currentDate; onboarding_responses_seen[option.id] = currentDate;
}); });
}); });
@@ -663,7 +697,10 @@ class Client extends BaseClient {
onboarding_responses_seen, onboarding_responses_seen,
}, },
}); });
this.emit(Events.DEBUG, `[Invite > Guild ${i.guild?.id}] Bypassed onboarding`); this.emit(
Events.DEBUG,
`[Invite > Guild ${i.guild?.id}] Bypassed onboarding`,
);
} }
} }
} }
@@ -671,23 +708,36 @@ class Client extends BaseClient {
if (data.show_verification_form && options.bypassVerify) { if (data.show_verification_form && options.bypassVerify) {
// Check Guild // Check Guild
if (i.guild.verificationLevel == 'VERY_HIGH' && !this.user.phone) { if (i.guild.verificationLevel == 'VERY_HIGH' && !this.user.phone) {
this.emit(Events.DEBUG, `[Invite > Guild ${i.guild?.id}] Cannot bypass verify (Phone required)`); this.emit(
Events.DEBUG,
`[Invite > Guild ${i.guild?.id}] Cannot bypass verify (Phone required)`,
);
return this.guilds.cache.get(i.guild?.id); return this.guilds.cache.get(i.guild?.id);
} }
if (i.guild.verificationLevel !== 'NONE' && !this.user.email) { if (i.guild.verificationLevel !== 'NONE' && !this.user.email) {
this.emit(Events.DEBUG, `[Invite > Guild ${i.guild?.id}] Cannot bypass verify (Email required)`); this.emit(
Events.DEBUG,
`[Invite > Guild ${i.guild?.id}] Cannot bypass verify (Email required)`,
);
return this.guilds.cache.get(i.guild?.id); return this.guilds.cache.get(i.guild?.id);
} }
const getForm = await this.api const getForm = await this.api
.guilds(i.guild?.id) .guilds(i.guild?.id)
['member-verification'].get({ query: { with_guild: false, invite_code: this.code } }) ['member-verification'].get({
query: { with_guild: false, invite_code: this.code },
})
.catch(() => {}); .catch(() => {});
if (getForm && getForm.form_fields[0]) { if (getForm && getForm.form_fields[0]) {
const form = Object.assign(getForm.form_fields[0], { response: true }); const form = Object.assign(getForm.form_fields[0], {
await this.api response: true,
.guilds(i.guild?.id) });
.requests['@me'].put({ data: { form_fields: [form], version: getForm.version } }); await this.api.guilds(i.guild?.id).requests['@me'].put({
this.emit(Events.DEBUG, `[Invite > Guild ${i.guild?.id}] Bypassed verify`); data: { form_fields: [form], version: getForm.version },
});
this.emit(
Events.DEBUG,
`[Invite > Guild ${i.guild?.id}] Bypassed verify`,
);
} }
} }
return guild; return guild;
@@ -706,14 +756,21 @@ class Client extends BaseClient {
redeemNitro(nitro, channel, paymentSourceId) { redeemNitro(nitro, channel, paymentSourceId) {
if (typeof nitro !== 'string') throw new Error('INVALID_NITRO'); if (typeof nitro !== 'string') throw new Error('INVALID_NITRO');
const nitroCode = const nitroCode =
nitro.match(/(discord.gift|discord.com|discordapp.com\/gifts)\/(\w{16,25})/) || nitro.match(
nitro.match(/(discord\.gift\/|discord\.com\/gifts\/|discordapp\.com\/gifts\/)(\w+)/); /(discord.gift|discord.com|discordapp.com\/gifts)\/(\w{16,25})/,
) ||
nitro.match(
/(discord\.gift\/|discord\.com\/gifts\/|discordapp\.com\/gifts\/)(\w+)/,
);
if (!nitroCode) return false; if (!nitroCode) return false;
const code = nitroCode[2]; const code = nitroCode[2];
channel = this.channels.resolveId(channel); channel = this.channels.resolveId(channel);
return this.api.entitlements['gift-codes'](code).redeem.post({ return this.api.entitlements['gift-codes'](code).redeem.post({
auth: true, auth: true,
data: { channel_id: channel || null, payment_source_id: paymentSourceId || null }, data: {
channel_id: channel || null,
payment_source_id: paymentSourceId || null,
},
}); });
} }
@@ -739,7 +796,11 @@ class Client extends BaseClient {
authorizeURL(urlOAuth2, options = {}) { authorizeURL(urlOAuth2, options = {}) {
// ! throw new Error('METHOD_WARNING'); // ! throw new Error('METHOD_WARNING');
const url = new URL(urlOAuth2); const url = new URL(urlOAuth2);
if (!/^https:\/\/(?:canary\.|ptb\.)?discord\.com(?:\/api(?:\/v\d{1,2})?)?\/oauth2\/authorize\?/.test(urlOAuth2)) { if (
!/^https:\/\/(?:canary\.|ptb\.)?discord\.com(?:\/api(?:\/v\d{1,2})?)?\/oauth2\/authorize\?/.test(
urlOAuth2,
)
) {
throw new Error('INVALID_URL', urlOAuth2); throw new Error('INVALID_URL', urlOAuth2);
} }
const searchParams = Object.fromEntries(url.searchParams); const searchParams = Object.fromEntries(url.searchParams);
@@ -778,7 +839,7 @@ class Client extends BaseClient {
with_guild: false, with_guild: false,
}, },
}) })
.then(rawData => { .then((rawData) => {
const installTypes = rawData.integration_types_config['1']; const installTypes = rawData.integration_types_config['1'];
if (installTypes) { if (installTypes) {
return this.api.oauth2.authorize.post({ return this.api.oauth2.authorize.post({
@@ -808,8 +869,8 @@ class Client extends BaseClient {
if (type === 'application') { if (type === 'application') {
return this.api.oauth2.tokens return this.api.oauth2.tokens
.get() .get()
.then(data => data.find(o => o.application.id == id)) .then((data) => data.find((o) => o.application.id == id))
.then(o => this.api.oauth2.tokens(o.id).delete()); .then((o) => this.api.oauth2.tokens(o.id).delete());
} else { } else {
return this.api.oauth2.tokens(id).delete(); return this.api.oauth2.tokens(id).delete();
} }
@@ -828,7 +889,7 @@ class Client extends BaseClient {
* @returns {Promise<Collection<Snowflake, AuthorizedApplicationData>>} * @returns {Promise<Collection<Snowflake, AuthorizedApplicationData>>}
*/ */
authorizedApplications() { authorizedApplications() {
return this.api.oauth2.tokens.get().then(data => { return this.api.oauth2.tokens.get().then((data) => {
const results = new Collection(); const results = new Collection();
for (const o of data) { for (const o of data) {
const application = new Application(this, o.application); const application = new Application(this, o.application);
@@ -864,50 +925,124 @@ class Client extends BaseClient {
if (typeof options.makeCache !== 'function') { if (typeof options.makeCache !== 'function') {
throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function'); throw new TypeError('CLIENT_INVALID_OPTION', 'makeCache', 'a function');
} }
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number'); 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)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number'); typeof options.messageSweepInterval !== 'number' ||
isNaN(options.messageSweepInterval)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'messageSweepInterval',
'a number',
);
} }
if (typeof options.sweepers !== 'object' || options.sweepers === null) { if (typeof options.sweepers !== 'object' || options.sweepers === null) {
throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object'); throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object');
} }
if (typeof options.invalidRequestWarningInterval !== 'number' || isNaN(options.invalidRequestWarningInterval)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'invalidRequestWarningInterval', 'a number'); typeof options.invalidRequestWarningInterval !== 'number' ||
isNaN(options.invalidRequestWarningInterval)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'invalidRequestWarningInterval',
'a number',
);
} }
if (!Array.isArray(options.partials)) { if (!Array.isArray(options.partials)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
} }
if (typeof options.DMChannelVoiceStatusSync !== 'number' || isNaN(options.DMChannelVoiceStatusSync)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'DMChannelVoiceStatusSync', 'a number'); typeof options.DMChannelVoiceStatusSync !== 'number' ||
isNaN(options.DMChannelVoiceStatusSync)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'DMChannelVoiceStatusSync',
'a number',
);
} }
if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number'); typeof options.waitGuildTimeout !== 'number' ||
isNaN(options.waitGuildTimeout)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'waitGuildTimeout',
'a number',
);
} }
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); typeof options.restWsBridgeTimeout !== 'number' ||
isNaN(options.restWsBridgeTimeout)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'restWsBridgeTimeout',
'a number',
);
} }
if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number'); typeof options.restRequestTimeout !== 'number' ||
isNaN(options.restRequestTimeout)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'restRequestTimeout',
'a number',
);
} }
if (typeof options.restGlobalRateLimit !== 'number' || isNaN(options.restGlobalRateLimit)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'restGlobalRateLimit', 'a number'); typeof options.restGlobalRateLimit !== 'number' ||
isNaN(options.restGlobalRateLimit)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'restGlobalRateLimit',
'a number',
);
} }
if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { if (
throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); typeof options.restSweepInterval !== 'number' ||
isNaN(options.restSweepInterval)
) {
throw new TypeError(
'CLIENT_INVALID_OPTION',
'restSweepInterval',
'a number',
);
} }
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number'); throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
} }
if (typeof options.failIfNotExists !== 'boolean') { if (typeof options.failIfNotExists !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'failIfNotExists', 'a boolean'); throw new TypeError(
'CLIENT_INVALID_OPTION',
'failIfNotExists',
'a boolean',
);
} }
if ( if (
typeof options.rejectOnRateLimit !== 'undefined' && typeof options.rejectOnRateLimit !== 'undefined' &&
!(typeof options.rejectOnRateLimit === 'function' || Array.isArray(options.rejectOnRateLimit)) !(
typeof options.rejectOnRateLimit === 'function' ||
Array.isArray(options.rejectOnRateLimit)
)
) { ) {
throw new TypeError('CLIENT_INVALID_OPTION', 'rejectOnRateLimit', 'an array or a function'); throw new TypeError(
'CLIENT_INVALID_OPTION',
'rejectOnRateLimit',
'an array or a function',
);
} }
if (typeof options.TOTPKey === 'string') { if (typeof options.TOTPKey === 'string') {
// Convert to base32 if not already // Convert to base32 if not already

View File

@@ -39,7 +39,11 @@ class WebhookClient extends BaseClient {
} }
this.id = id; this.id = id;
Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true }); Object.defineProperty(this, 'token', {
value: token,
writable: true,
configurable: true,
});
} }
// These are here only for documentation purposes - they are implemented by Webhook // These are here only for documentation purposes - they are implemented by Webhook

View File

@@ -38,14 +38,20 @@ class GenericAction {
if (!('recipients' in data)) { if (!('recipients' in data)) {
// Try to resolve the recipient, but do not add the client user. // Try to resolve the recipient, but do not add the client user.
const recipient = data.author ?? data.user ?? { id: data.user_id }; const recipient = data.author ?? data.user ?? { id: data.user_id };
if (recipient.id !== this.client.user.id) payloadData.recipients = [recipient]; if (recipient.id !== this.client.user.id)
payloadData.recipients = [recipient];
} }
if (id !== undefined) payloadData.id = id; if (id !== undefined) payloadData.id = id;
return ( return (
data[this.client.actions.injectedChannel] ?? data[this.client.actions.injectedChannel] ??
this.getPayload({ ...data, ...payloadData }, this.client.channels, id, PartialTypes.CHANNEL) this.getPayload(
{ ...data, ...payloadData },
this.client.channels,
id,
PartialTypes.CHANNEL,
)
); );
} }
@@ -82,12 +88,20 @@ class GenericAction {
} }
getMember(data, guild) { getMember(data, guild) {
return this.getPayload(data, guild.members, data.user.id, PartialTypes.GUILD_MEMBER); return this.getPayload(
data,
guild.members,
data.user.id,
PartialTypes.GUILD_MEMBER,
);
} }
getUser(data) { getUser(data) {
const id = data.user_id; const id = data.user_id;
return data[this.client.actions.injectedUser] ?? this.getPayload({ id }, this.client.users, id, PartialTypes.USER); return (
data[this.client.actions.injectedUser] ??
this.getPayload({ id }, this.client.users, id, PartialTypes.USER)
);
} }
getUserFromMember(data) { getUserFromMember(data) {

View File

@@ -17,7 +17,10 @@ class AutoModerationActionExecutionAction extends Action {
* @param {AutoModerationActionExecution} autoModerationActionExecution The data of the execution * @param {AutoModerationActionExecution} autoModerationActionExecution The data of the execution
* @deprecated This event is not received by user accounts. * @deprecated This event is not received by user accounts.
*/ */
client.emit(Events.AUTO_MODERATION_ACTION_EXECUTION, new AutoModerationActionExecution(data, guild)); client.emit(
Events.AUTO_MODERATION_ACTION_EXECUTION,
new AutoModerationActionExecution(data, guild),
);
} }
return {}; return {};

View File

@@ -9,7 +9,8 @@ class AutoModerationRuleUpdateAction extends Action {
const guild = client.guilds.cache.get(data.guild_id); const guild = client.guilds.cache.get(data.guild_id);
if (guild) { if (guild) {
const oldAutoModerationRule = guild.autoModerationRules.cache.get(data.id)?._clone() ?? null; const oldAutoModerationRule =
guild.autoModerationRules.cache.get(data.id)?._clone() ?? null;
const newAutoModerationRule = guild.autoModerationRules._add(data); const newAutoModerationRule = guild.autoModerationRules._add(data);
/** /**
@@ -20,7 +21,11 @@ class AutoModerationRuleUpdateAction extends Action {
* @param {AutoModerationRule} newAutoModerationRule The auto moderation rule after the update * @param {AutoModerationRule} newAutoModerationRule The auto moderation rule after the update
* @deprecated This event is not received by user accounts. * @deprecated This event is not received by user accounts.
*/ */
client.emit(Events.AUTO_MODERATION_RULE_UPDATE, oldAutoModerationRule, newAutoModerationRule); client.emit(
Events.AUTO_MODERATION_RULE_UPDATE,
oldAutoModerationRule,
newAutoModerationRule,
);
} }
return {}; return {};

View File

@@ -21,7 +21,8 @@ class ChannelUpdateAction extends Action {
} }
if (channel.isText() && newChannel.isText()) { if (channel.isText() && newChannel.isText()) {
for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message); for (const [id, message] of channel.messages.cache)
newChannel.messages.cache.set(id, message);
} }
channel = newChannel; channel = newChannel;

View File

@@ -15,7 +15,8 @@ class GuildBanRemove extends Action {
* @param {GuildBan} ban The ban that was removed * @param {GuildBan} ban The ban that was removed
*/ */
if (guild) { if (guild) {
const ban = guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild); const ban =
guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild);
guild.bans.cache.delete(ban.user.id); guild.bans.cache.delete(ban.user.id);
client.emit(Events.GUILD_BAN_REMOVE, ban); client.emit(Events.GUILD_BAN_REMOVE, ban);
} }

View File

@@ -34,7 +34,8 @@ class GuildDeleteAction extends Action {
}; };
} }
for (const channel of guild.channels.cache.values()) this.client.channels._remove(channel.id); for (const channel of guild.channels.cache.values())
this.client.channels._remove(channel.id);
client.voice.adapters.get(data.id)?.destroy(); client.voice.adapters.get(data.id)?.destroy();
// Delete guild // Delete guild
@@ -58,7 +59,10 @@ class GuildDeleteAction extends Action {
} }
scheduleForDeletion(id) { scheduleForDeletion(id) {
setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout).unref(); setTimeout(
() => this.deleted.delete(id),
this.client.options.restWsBridgeTimeout,
).unref();
} }
} }

View File

@@ -21,7 +21,8 @@ class GuildMemberRemoveAction extends Action {
* @param {GuildMember} member The member that has left/been kicked from the guild * @param {GuildMember} member The member that has left/been kicked from the guild
* @deprecated See {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/issues/197 this issue} for more information. * @deprecated See {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/issues/197 this issue} for more information.
*/ */
if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); if (shard.status === Status.READY)
client.emit(Events.GUILD_MEMBER_REMOVE, member);
} }
guild.presences.cache.delete(data.user.id); guild.presences.cache.delete(data.user.id);
guild.voiceStates.cache.delete(data.user.id); guild.voiceStates.cache.delete(data.user.id);

View File

@@ -27,7 +27,8 @@ class GuildMemberUpdateAction extends Action {
* @param {GuildMember} newMember The member after the update * @param {GuildMember} newMember The member after the update
* @deprecated See {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/issues/197 this issue} for more information. * @deprecated See {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/issues/197 this issue} for more information.
*/ */
if (shard.status === Status.READY && !member.equals(old)) client.emit(Events.GUILD_MEMBER_UPDATE, old, member); if (shard.status === Status.READY && !member.equals(old))
client.emit(Events.GUILD_MEMBER_UPDATE, old, member);
} else { } else {
const newMember = guild.members._add(data); const newMember = guild.members._add(data);
/** /**

View File

@@ -9,7 +9,8 @@ class GuildScheduledEventUpdateAction extends Action {
const guild = client.guilds.cache.get(data.guild_id); const guild = client.guilds.cache.get(data.guild_id);
if (guild) { if (guild) {
const oldGuildScheduledEvent = guild.scheduledEvents.cache.get(data.id)?._clone() ?? null; const oldGuildScheduledEvent =
guild.scheduledEvents.cache.get(data.id)?._clone() ?? null;
const newGuildScheduledEvent = guild.scheduledEvents._add(data); const newGuildScheduledEvent = guild.scheduledEvents._add(data);
/** /**
@@ -18,7 +19,11 @@ class GuildScheduledEventUpdateAction extends Action {
* @param {?GuildScheduledEvent} oldGuildScheduledEvent The guild scheduled event object before the update * @param {?GuildScheduledEvent} oldGuildScheduledEvent The guild scheduled event object before the update
* @param {GuildScheduledEvent} newGuildScheduledEvent The guild scheduled event object after the update * @param {GuildScheduledEvent} newGuildScheduledEvent The guild scheduled event object after the update
*/ */
client.emit(Events.GUILD_SCHEDULED_EVENT_UPDATE, oldGuildScheduledEvent, newGuildScheduledEvent); client.emit(
Events.GUILD_SCHEDULED_EVENT_UPDATE,
oldGuildScheduledEvent,
newGuildScheduledEvent,
);
return { oldGuildScheduledEvent, newGuildScheduledEvent }; return { oldGuildScheduledEvent, newGuildScheduledEvent };
} }

View File

@@ -19,7 +19,11 @@ class GuildScheduledEventUserAddAction extends Action {
* @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event * @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
* @param {User} user The user who subscribed * @param {User} user The user who subscribed
*/ */
client.emit(Events.GUILD_SCHEDULED_EVENT_USER_ADD, guildScheduledEvent, user); client.emit(
Events.GUILD_SCHEDULED_EVENT_USER_ADD,
guildScheduledEvent,
user,
);
return { guildScheduledEvent, user }; return { guildScheduledEvent, user };
} }

View File

@@ -19,7 +19,11 @@ class GuildScheduledEventUserRemoveAction extends Action {
* @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event * @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
* @param {User} user The user who unsubscribed * @param {User} user The user who unsubscribed
*/ */
client.emit(Events.GUILD_SCHEDULED_EVENT_USER_REMOVE, guildScheduledEvent, user); client.emit(
Events.GUILD_SCHEDULED_EVENT_USER_REMOVE,
guildScheduledEvent,
user,
);
return { guildScheduledEvent, user }; return { guildScheduledEvent, user };
} }

View File

@@ -18,7 +18,8 @@ class MessageCreateAction extends Action {
if (!channel.isText()) return {}; if (!channel.isText()) return {};
const existing = channel.messages.cache.get(data.id); const existing = channel.messages.cache.get(data.id);
if (existing && existing.author?.id !== this.client.user.id) return { message: existing }; if (existing && existing.author?.id !== this.client.user.id)
return { message: existing };
const message = existing ?? channel.messages._add(data); const message = existing ?? channel.messages._add(data);
channel.lastMessageId = data.id; channel.lastMessageId = data.id;
@@ -37,7 +38,10 @@ class MessageCreateAction extends Action {
*/ */
if (client.emit('message', message) && !deprecationEmitted) { if (client.emit('message', message) && !deprecationEmitted) {
deprecationEmitted = true; deprecationEmitted = true;
process.emitWarning('The message event is deprecated. Use messageCreate instead', 'DeprecationWarning'); process.emitWarning(
'The message event is deprecated. Use messageCreate instead',
'DeprecationWarning',
);
} }
return { message }; return { message };

View File

@@ -7,7 +7,10 @@ const { Events } = require('../../util/Constants');
class MessageDeleteAction extends Action { class MessageDeleteAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
});
let message; let message;
if (channel) { if (channel) {
if (!channel.isText()) return {}; if (!channel.isText()) return {};

View File

@@ -35,7 +35,9 @@ class MessageReactionAdd extends Action {
if (!message) return false; if (!message) return false;
// Verify reaction // Verify reaction
const includePartial = this.client.options.partials.includes(PartialTypes.REACTION); const includePartial = this.client.options.partials.includes(
PartialTypes.REACTION,
);
if (message.partial && !includePartial) return false; if (message.partial && !includePartial) return false;
const reaction = message.reactions._add({ const reaction = message.reactions._add({
emoji: data.emoji, emoji: data.emoji,
@@ -59,7 +61,10 @@ class MessageReactionAdd extends Action {
* @param {User} user The user that applied the guild or reaction emoji * @param {User} user The user that applied the guild or reaction emoji
* @param {MessageReactionEventDetails} details Details of adding the reaction * @param {MessageReactionEventDetails} details Details of adding the reaction
*/ */
this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user, { type: data.type, burst: data.burst }); this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user, {
type: data.type,
burst: data.burst,
});
return { message, reaction, user }; return { message, reaction, user };
} }

View File

@@ -41,7 +41,10 @@ class MessageReactionRemove extends Action {
* @param {User} user The user whose emoji or reaction emoji was removed * @param {User} user The user whose emoji or reaction emoji was removed
* @param {MessageReactionEventDetails} details Details of removing the reaction * @param {MessageReactionEventDetails} details Details of removing the reaction
*/ */
this.client.emit(Events.MESSAGE_REACTION_REMOVE, reaction, user, { type: data.type, burst: data.burst }); this.client.emit(Events.MESSAGE_REACTION_REMOVE, reaction, user, {
type: data.type,
burst: data.burst,
});
return { message, reaction, user }; return { message, reaction, user };
} }

View File

@@ -6,7 +6,10 @@ const { Events } = require('../../util/Constants');
class MessageReactionRemoveAll extends Action { class MessageReactionRemoveAll extends Action {
handle(data) { handle(data) {
// Verify channel // Verify channel
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
});
if (!channel || !channel.isText()) return false; if (!channel || !channel.isText()) return false;
// Verify message // Verify message

View File

@@ -5,7 +5,10 @@ const { Events } = require('../../util/Constants');
class MessageReactionRemoveEmoji extends Action { class MessageReactionRemoveEmoji extends Action {
handle(data) { handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
});
if (!channel || !channel.isText()) return false; if (!channel || !channel.isText()) return false;
const message = this.getMessage(data, channel); const message = this.getMessage(data, channel);
@@ -13,7 +16,8 @@ class MessageReactionRemoveEmoji extends Action {
const reaction = this.getReaction(data, message); const reaction = this.getReaction(data, message);
if (!reaction) return false; if (!reaction) return false;
if (!message.partial) message.reactions.cache.delete(reaction.emoji.id ?? reaction.emoji.name); 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. * Emitted when a bot removes an emoji reaction from a cached message.

View File

@@ -4,12 +4,18 @@ const Action = require('./Action');
class MessageUpdateAction extends Action { class MessageUpdateAction extends Action {
handle(data) { handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
});
if (channel) { if (channel) {
if (!channel.isText()) return {}; if (!channel.isText()) return {};
const { id, channel_id, guild_id, author, timestamp, type } = data; const { id, channel_id, guild_id, author, timestamp, type } = data;
const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel); const message = this.getMessage(
{ id, channel_id, guild_id, author, timestamp, type },
channel,
);
if (message) { if (message) {
const old = message._update(data); const old = message._update(data);
return { return {

View File

@@ -8,13 +8,18 @@ class PresenceUpdateAction extends Action {
handle(data) { handle(data) {
let user = this.client.users.cache.get(data.user.id); let user = this.client.users.cache.get(data.user.id);
if (!user && data.user?.username) user = this.client.users._add(data.user); if (!user && data.user?.username) user = this.client.users._add(data.user);
if (!user && ('username' in data.user || this.client.options.partials.includes(PartialTypes.USER))) { if (
!user &&
('username' in data.user ||
this.client.options.partials.includes(PartialTypes.USER))
) {
user = this.client.users._add(data.user); user = this.client.users._add(data.user);
} }
if (!user) return; if (!user) return;
if (data.user?.username) { if (data.user?.username) {
if (!user._equals(data.user)) this.client.actions.UserUpdate.handle(data.user); if (!user._equals(data.user))
this.client.actions.UserUpdate.handle(data.user);
} }
const guild = this.client.guilds.cache.get(data.guild_id); const guild = this.client.guilds.cache.get(data.guild_id);
@@ -31,11 +36,17 @@ class PresenceUpdateAction extends Action {
} }
} }
const oldPresence = (guild || this.client).presences.cache.get(user.id)?._clone() ?? null; const oldPresence =
(guild || this.client).presences.cache.get(user.id)?._clone() ?? null;
const newPresence = (guild || this.client).presences._add(Object.assign(data, { guild })); const newPresence = (guild || this.client).presences._add(
Object.assign(data, { guild }),
);
if (this.client.listenerCount(Events.PRESENCE_UPDATE) && !newPresence.equals(oldPresence)) { if (
this.client.listenerCount(Events.PRESENCE_UPDATE) &&
!newPresence.equals(oldPresence)
) {
/** /**
* Emitted whenever a guild member's presence (e.g. status, activity) is changed. * Emitted whenever a guild member's presence (e.g. status, activity) is changed.
* @event Client#presenceUpdate * @event Client#presenceUpdate

View File

@@ -6,7 +6,10 @@ const { Events } = require('../../util/Constants');
class StageInstanceCreateAction extends Action { class StageInstanceCreateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); const channel = this.getChannel({
id: data.channel_id,
guild_id: data.guild_id,
});
if (channel) { if (channel) {
const stageInstance = channel.guild.stageInstances._add(data); const stageInstance = channel.guild.stageInstances._add(data);

View File

@@ -7,7 +7,10 @@ const { Events } = require('../../util/Constants');
class StageInstanceDeleteAction extends Action { class StageInstanceDeleteAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); const channel = this.getChannel({
id: data.channel_id,
guild_id: data.guild_id,
});
if (channel) { if (channel) {
const stageInstance = channel.guild.stageInstances._add(data); const stageInstance = channel.guild.stageInstances._add(data);

View File

@@ -6,10 +6,14 @@ const { Events } = require('../../util/Constants');
class StageInstanceUpdateAction extends Action { class StageInstanceUpdateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); const channel = this.getChannel({
id: data.channel_id,
guild_id: data.guild_id,
});
if (channel) { if (channel) {
const oldStageInstance = channel.guild.stageInstances.cache.get(data.id)?._clone() ?? null; const oldStageInstance =
channel.guild.stageInstances.cache.get(data.id)?._clone() ?? null;
const newStageInstance = channel.guild.stageInstances._add(data); const newStageInstance = channel.guild.stageInstances._add(data);
/** /**
@@ -18,7 +22,11 @@ class StageInstanceUpdateAction extends Action {
* @param {?StageInstance} oldStageInstance The stage instance before the update * @param {?StageInstance} oldStageInstance The stage instance before the update
* @param {StageInstance} newStageInstance The stage instance after the update * @param {StageInstance} newStageInstance The stage instance after the update
*/ */
client.emit(Events.STAGE_INSTANCE_UPDATE, oldStageInstance, newStageInstance); client.emit(
Events.STAGE_INSTANCE_UPDATE,
oldStageInstance,
newStageInstance,
);
return { oldStageInstance, newStageInstance }; return { oldStageInstance, newStageInstance };
} }

View File

@@ -48,7 +48,7 @@ class ThreadListSyncAction extends Action {
} }
removeStale(channel) { removeStale(channel) {
channel.threads?.cache.forEach(thread => { channel.threads?.cache.forEach((thread) => {
if (!thread.archived) { if (!thread.archived) {
this.client.channels._remove(thread.id); this.client.channels._remove(thread.id);
} }

View File

@@ -11,11 +11,11 @@ class ThreadMembersUpdateAction extends Action {
const old = thread.members.cache.clone(); const old = thread.members.cache.clone();
thread.memberCount = data.member_count; thread.memberCount = data.member_count;
data.added_members?.forEach(rawMember => { data.added_members?.forEach((rawMember) => {
thread.members._add(rawMember); thread.members._add(rawMember);
}); });
data.removed_member_ids?.forEach(memberId => { data.removed_member_ids?.forEach((memberId) => {
thread.members.cache.delete(memberId); thread.members.cache.delete(memberId);
}); });

View File

@@ -6,11 +6,17 @@ const { Events } = require('../../util/Constants');
class TypingStart extends Action { class TypingStart extends Action {
handle(data) { handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
});
if (!channel) return; if (!channel) return;
if (!channel.isText()) { if (!channel.isText()) {
this.client.emit(Events.WARN, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`); this.client.emit(
Events.WARN,
`Discord sent a typing packet to a ${channel.type} channel ${channel.id}`,
);
return; return;
} }

View File

@@ -7,7 +7,10 @@ class UserUpdateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const newUser = data.id === client.user.id ? client.user : client.users.cache.get(data.id); const newUser =
data.id === client.user.id
? client.user
: client.users.cache.get(data.id);
const oldUser = newUser._update(data); const oldUser = newUser._update(data);
if (!oldUser.equals(newUser)) { if (!oldUser.equals(newUser)) {

View File

@@ -11,7 +11,8 @@ class VoiceStateUpdate extends Action {
if (guild) { if (guild) {
// Update the state // Update the state
const oldState = const oldState =
guild.voiceStates.cache.get(data.user_id)?._clone() ?? new VoiceState(guild, { user_id: data.user_id }); guild.voiceStates.cache.get(data.user_id)?._clone() ??
new VoiceState(guild, { user_id: data.user_id });
const newState = guild.voiceStates._add(data); const newState = guild.voiceStates._add(data);
@@ -33,7 +34,8 @@ class VoiceStateUpdate extends Action {
} else { } else {
// Update the state // Update the state
const oldState = const oldState =
client.voiceStates.cache.get(data.user_id)?._clone() ?? new VoiceState({ client }, { user_id: data.user_id }); client.voiceStates.cache.get(data.user_id)?._clone() ??
new VoiceState({ client }, { user_id: data.user_id });
const newState = client.voiceStates._add(data); const newState = client.voiceStates._add(data);
@@ -41,7 +43,10 @@ class VoiceStateUpdate extends Action {
} }
// Emit event // Emit event
if (data.user_id === client.user?.id) { if (data.user_id === client.user?.id) {
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); client.emit(
'debug',
`[VOICE] received voice state update: ${JSON.stringify(data)}`,
);
client.voice.onVoiceStateUpdate(data); client.voice.onVoiceStateUpdate(data);
} }
} }

View File

@@ -61,14 +61,25 @@ class ClientVoiceManager {
onVoiceStateUpdate(payload) { onVoiceStateUpdate(payload) {
const { guild_id, session_id, channel_id } = payload; const { guild_id, session_id, channel_id } = payload;
// @discordjs/voice // @discordjs/voice
if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) { if (
payload.guild_id &&
payload.session_id &&
payload.user_id === this.client.user?.id
) {
this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload); this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
} else if (payload.channel_id && payload.session_id && payload.user_id === this.client.user?.id) { } else if (
payload.channel_id &&
payload.session_id &&
payload.user_id === this.client.user?.id
) {
this.adapters.get(payload.channel_id)?.onVoiceStateUpdate(payload); this.adapters.get(payload.channel_id)?.onVoiceStateUpdate(payload);
} }
// Main lib // Main lib
const connection = this.connection; const connection = this.connection;
this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`); this.client.emit(
'debug',
`[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`,
);
if (!connection) return; if (!connection) return;
if (!channel_id) { if (!channel_id) {
connection._disconnect(); connection._disconnect();
@@ -80,7 +91,10 @@ class ClientVoiceManager {
connection.channel = channel; connection.channel = channel;
connection.setSessionId(session_id); connection.setSessionId(session_id);
} else { } else {
this.client.emit('debug', `[VOICE] disconnecting from guild ${guild_id} as channel ${channel_id} is uncached`); this.client.emit(
'debug',
`[VOICE] disconnecting from guild ${guild_id} as channel ${channel_id} is uncached`,
);
connection.disconnect(); connection.disconnect();
} }
} }
@@ -117,8 +131,11 @@ class ClientVoiceManager {
} else { } else {
connection = new VoiceConnection(this, channel); connection = new VoiceConnection(this, channel);
if (config?.videoCodec) connection.setVideoCodec(config.videoCodec); if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
connection.on('debug', msg => connection.on('debug', (msg) =>
this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`), this.client.emit(
'debug',
`[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`,
),
); );
connection.authenticate({ connection.authenticate({
self_mute: Boolean(config.selfMute), self_mute: Boolean(config.selfMute),
@@ -128,7 +145,7 @@ class ClientVoiceManager {
this.connection = connection; this.connection = connection;
} }
connection.once('failed', reason => { connection.once('failed', (reason) => {
this.connection = null; this.connection = null;
reject(reason); reject(reason);
}); });

View File

@@ -12,7 +12,12 @@ const { parseStreamKey } = require('./util/Function');
const PlayInterface = require('./util/PlayInterface'); const PlayInterface = require('./util/PlayInterface');
const Silence = require('./util/Silence'); const Silence = require('./util/Silence');
const { Error } = require('../../errors'); const { Error } = require('../../errors');
const { Opcodes, VoiceOpcodes, VoiceStatus, Events } = require('../../util/Constants'); const {
Opcodes,
VoiceOpcodes,
VoiceStatus,
Events,
} = require('../../util/Constants');
const Speaking = require('../../util/Speaking'); const Speaking = require('../../util/Speaking');
const Util = require('../../util/Util'); const Util = require('../../util/Util');
@@ -90,9 +95,12 @@ class VoiceConnection extends EventEmitter {
* The audio player for this voice connection * The audio player for this voice connection
* @type {MediaPlayer} * @type {MediaPlayer}
*/ */
this.player = new MediaPlayer(this, this.constructor.name === 'StreamConnection'); this.player = new MediaPlayer(
this,
this.constructor.name === 'StreamConnection',
);
this.player.on('debug', m => { this.player.on('debug', (m) => {
/** /**
* Debug info from the connection. * Debug info from the connection.
* @event VoiceConnection#debug * @event VoiceConnection#debug
@@ -101,7 +109,7 @@ class VoiceConnection extends EventEmitter {
this.emit('debug', `media player - ${m}`); this.emit('debug', `media player - ${m}`);
}); });
this.player.on('error', e => { this.player.on('error', (e) => {
/** /**
* Warning info from the connection. * Warning info from the connection.
* @event VoiceConnection#warn * @event VoiceConnection#warn
@@ -212,7 +220,7 @@ class VoiceConnection extends EventEmitter {
ssrc: this.authentication.ssrc, ssrc: this.authentication.ssrc,
}, },
}) })
.catch(e => { .catch((e) => {
this.emit('debug', e); this.emit('debug', e);
}); });
} }
@@ -223,7 +231,8 @@ class VoiceConnection extends EventEmitter {
* @returns {VoiceConnection} * @returns {VoiceConnection}
*/ */
setVideoCodec(value) { setVideoCodec(value) {
if (!SUPPORTED_CODECS.includes(value)) throw new Error('INVALID_VIDEO_CODEC', SUPPORTED_CODECS); if (!SUPPORTED_CODECS.includes(value))
throw new Error('INVALID_VIDEO_CODEC', SUPPORTED_CODECS);
this.videoCodec = value; this.videoCodec = value;
return this; return this;
} }
@@ -247,7 +256,7 @@ class VoiceConnection extends EventEmitter {
streams: [], streams: [],
}, },
}) })
.catch(e => { .catch((e) => {
this.emit('debug', e); this.emit('debug', e);
}); });
} else { } else {
@@ -277,7 +286,7 @@ class VoiceConnection extends EventEmitter {
], ],
}, },
}) })
.catch(e => { .catch((e) => {
this.emit('debug', e); this.emit('debug', e);
}); });
} }
@@ -310,7 +319,10 @@ class VoiceConnection extends EventEmitter {
options, options,
); );
this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`); this.emit(
'debug',
`Sending voice state update: ${JSON.stringify(options)}`,
);
return this.channel.client.ws.broadcast({ return this.channel.client.ws.broadcast({
op: Opcodes.VOICE_STATE_UPDATE, op: Opcodes.VOICE_STATE_UPDATE,
@@ -349,7 +361,10 @@ class VoiceConnection extends EventEmitter {
this.authentication.token = token; this.authentication.token = token;
this.authentication.endpoint = endpoint; this.authentication.endpoint = endpoint;
this.checkAuthenticated(); this.checkAuthenticated();
} else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) { } else if (
token !== this.authentication.token ||
endpoint !== this.authentication.endpoint
) {
this.reconnect(token, endpoint); this.reconnect(token, endpoint);
} }
} }
@@ -360,7 +375,10 @@ class VoiceConnection extends EventEmitter {
* @private * @private
*/ */
setSessionId(sessionId) { setSessionId(sessionId) {
this.emit('debug', `Setting sessionId ${sessionId} (stored as "${this.authentication.sessionId}")`); this.emit(
'debug',
`Setting sessionId ${sessionId} (stored as "${this.authentication.sessionId}")`,
);
if (!sessionId) { if (!sessionId) {
this.authenticateFailed('VOICE_SESSION_ABSENT'); this.authenticateFailed('VOICE_SESSION_ABSENT');
return; return;
@@ -441,7 +459,10 @@ class VoiceConnection extends EventEmitter {
*/ */
authenticate(options = {}) { authenticate(options = {}) {
this.sendVoiceStateUpdate(options); this.sendVoiceStateUpdate(options);
this.connectTimeout = setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15_000).unref(); this.connectTimeout = setTimeout(
() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'),
15_000,
).unref();
} }
/** /**
@@ -537,10 +558,10 @@ class VoiceConnection extends EventEmitter {
const { ws, udp } = this.sockets; const { ws, udp } = this.sockets;
ws.on('debug', msg => this.emit('debug', msg)); ws.on('debug', (msg) => this.emit('debug', msg));
udp.on('debug', msg => this.emit('debug', msg)); udp.on('debug', (msg) => this.emit('debug', msg));
ws.on('error', err => this.emit('error', err)); ws.on('error', (err) => this.emit('error', err));
udp.on('error', err => this.emit('error', err)); udp.on('error', (err) => this.emit('error', err));
ws.on('ready', this.onReady.bind(this)); ws.on('ready', this.onReady.bind(this));
ws.on('sessionDescription', this.onSessionDescription.bind(this)); ws.on('sessionDescription', this.onSessionDescription.bind(this));
ws.on('startSpeaking', this.onStartSpeaking.bind(this)); ws.on('startSpeaking', this.onStartSpeaking.bind(this));
@@ -576,7 +597,10 @@ class VoiceConnection extends EventEmitter {
this.status = VoiceStatus.CONNECTED; this.status = VoiceStatus.CONNECTED;
const ready = () => { const ready = () => {
clearTimeout(this.connectTimeout); clearTimeout(this.connectTimeout);
this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); this.emit(
'debug',
`Ready with authentication details: ${JSON.stringify(this.authentication)}`,
);
/** /**
* Emitted once the connection is ready, when a promise to join a voice channel resolves, * Emitted once the connection is ready, when a promise to join a voice channel resolves,
* the connection will already be ready. * the connection will already be ready.
@@ -588,7 +612,10 @@ class VoiceConnection extends EventEmitter {
ready(); ready();
} else { } else {
// This serves to provide support for voice receive, sending audio is required to receive it. // This serves to provide support for voice receive, sending audio is required to receive it.
const dispatcher = this.playAudio(new SingleSilence(), { type: 'opus', volume: false }); const dispatcher = this.playAudio(new SingleSilence(), {
type: 'opus',
volume: false,
});
dispatcher.once('finish', ready); dispatcher.once('finish', ready);
} }
} }
@@ -678,13 +705,22 @@ class VoiceConnection extends EventEmitter {
if (this.streamConnection) { if (this.streamConnection) {
return resolve(this.streamConnection); return resolve(this.streamConnection);
} else { } else {
const connection = (this.streamConnection = new StreamConnection(this.voiceManager, this.channel, this)); const connection = (this.streamConnection = new StreamConnection(
this.voiceManager,
this.channel,
this,
));
connection.setVideoCodec(this.videoCodec); // Sync :? connection.setVideoCodec(this.videoCodec); // Sync :?
// Setup event... // Setup event...
if (!this.eventHook) { if (!this.eventHook) {
this.eventHook = true; // Dont listen this event two times :/ this.eventHook = true; // Dont listen this event two times :/
this.channel.client.on('raw', packet => { this.channel.client.on('raw', (packet) => {
if (typeof packet !== 'object' || !packet.t || !packet.d || !packet.d?.stream_key) { if (
typeof packet !== 'object' ||
!packet.t ||
!packet.d ||
!packet.d?.stream_key
) {
return; return;
} }
const { t: event, d: data } = packet; const { t: event, d: data } = packet;
@@ -697,12 +733,17 @@ class VoiceConnection extends EventEmitter {
// Current user stream // Current user stream
switch (event) { switch (event) {
case 'STREAM_CREATE': { case 'STREAM_CREATE': {
this.streamConnection.setSessionId(this.authentication.sessionId); this.streamConnection.setSessionId(
this.authentication.sessionId,
);
this.streamConnection.serverId = data.rtc_server_id; this.streamConnection.serverId = data.rtc_server_id;
break; break;
} }
case 'STREAM_SERVER_UPDATE': { case 'STREAM_SERVER_UPDATE': {
this.streamConnection.setTokenAndEndpoint(data.token, data.endpoint); this.streamConnection.setTokenAndEndpoint(
data.token,
data.endpoint,
);
break; break;
} }
case 'STREAM_DELETE': { case 'STREAM_DELETE': {
@@ -715,8 +756,13 @@ class VoiceConnection extends EventEmitter {
} }
} }
} }
if (this.streamWatchConnection.has(StreamKey.userId) && this.channel.id == StreamKey.channelId) { if (
const streamConnection = this.streamWatchConnection.get(StreamKey.userId); this.streamWatchConnection.has(StreamKey.userId) &&
this.channel.id == StreamKey.channelId
) {
const streamConnection = this.streamWatchConnection.get(
StreamKey.userId,
);
// Watch user stream // Watch user stream
switch (event) { switch (event) {
case 'STREAM_CREATE': { case 'STREAM_CREATE': {
@@ -725,7 +771,10 @@ class VoiceConnection extends EventEmitter {
break; break;
} }
case 'STREAM_SERVER_UPDATE': { case 'STREAM_SERVER_UPDATE': {
streamConnection.setTokenAndEndpoint(data.token, data.endpoint); streamConnection.setTokenAndEndpoint(
data.token,
data.endpoint,
);
break; break;
} }
case 'STREAM_DELETE': { case 'STREAM_DELETE': {
@@ -745,13 +794,13 @@ class VoiceConnection extends EventEmitter {
connection.sendSignalScreenshare(); connection.sendSignalScreenshare();
connection.sendScreenshareState(true); connection.sendScreenshareState(true);
connection.on('debug', msg => connection.on('debug', (msg) =>
this.channel.client.emit( this.channel.client.emit(
'debug', 'debug',
`[VOICE STREAM (${this.channel.guild?.id || this.channel.id}:${connection.status})]: ${msg}`, `[VOICE STREAM (${this.channel.guild?.id || this.channel.id}:${connection.status})]: ${msg}`,
), ),
); );
connection.once('failed', reason => { connection.once('failed', (reason) => {
this.streamConnection = null; this.streamConnection = null;
reject(reason); reject(reason);
}); });
@@ -782,7 +831,9 @@ class VoiceConnection extends EventEmitter {
if (!userId) { if (!userId) {
throw new Error('VOICE_USER_MISSING'); throw new Error('VOICE_USER_MISSING');
} }
const voiceState = this.channel.guild?.voiceStates.cache.get(userId) || this.client.voiceStates.cache.get(userId); const voiceState =
this.channel.guild?.voiceStates.cache.get(userId) ||
this.client.voiceStates.cache.get(userId);
if (!voiceState || !voiceState.streaming) { if (!voiceState || !voiceState.streaming) {
throw new Error('VOICE_USER_NOT_STREAMING'); throw new Error('VOICE_USER_NOT_STREAMING');
} }
@@ -791,14 +842,24 @@ class VoiceConnection extends EventEmitter {
if (this.streamWatchConnection.has(userId)) { if (this.streamWatchConnection.has(userId)) {
return resolve(this.streamWatchConnection.get(userId)); return resolve(this.streamWatchConnection.get(userId));
} else { } else {
const connection = new StreamConnectionReadonly(this.voiceManager, this.channel, this, userId); const connection = new StreamConnectionReadonly(
this.voiceManager,
this.channel,
this,
userId,
);
this.streamWatchConnection.set(userId, connection); this.streamWatchConnection.set(userId, connection);
connection.setVideoCodec(this.videoCodec); connection.setVideoCodec(this.videoCodec);
// Setup event... // Setup event...
if (!this.eventHook) { if (!this.eventHook) {
this.eventHook = true; // Dont listen this event two times :/ this.eventHook = true; // Dont listen this event two times :/
this.channel.client.on('raw', packet => { this.channel.client.on('raw', (packet) => {
if (typeof packet !== 'object' || !packet.t || !packet.d || !packet.d?.stream_key) { if (
typeof packet !== 'object' ||
!packet.t ||
!packet.d ||
!packet.d?.stream_key
) {
return; return;
} }
const { t: event, d: data } = packet; const { t: event, d: data } = packet;
@@ -811,12 +872,17 @@ class VoiceConnection extends EventEmitter {
// Current user stream // Current user stream
switch (event) { switch (event) {
case 'STREAM_CREATE': { case 'STREAM_CREATE': {
this.streamConnection.setSessionId(this.authentication.sessionId); this.streamConnection.setSessionId(
this.authentication.sessionId,
);
this.streamConnection.serverId = data.rtc_server_id; this.streamConnection.serverId = data.rtc_server_id;
break; break;
} }
case 'STREAM_SERVER_UPDATE': { case 'STREAM_SERVER_UPDATE': {
this.streamConnection.setTokenAndEndpoint(data.token, data.endpoint); this.streamConnection.setTokenAndEndpoint(
data.token,
data.endpoint,
);
break; break;
} }
case 'STREAM_DELETE': { case 'STREAM_DELETE': {
@@ -829,8 +895,13 @@ class VoiceConnection extends EventEmitter {
} }
} }
} }
if (this.streamWatchConnection.has(StreamKey.userId) && this.channel.id == StreamKey.channelId) { if (
const streamConnection = this.streamWatchConnection.get(StreamKey.userId); this.streamWatchConnection.has(StreamKey.userId) &&
this.channel.id == StreamKey.channelId
) {
const streamConnection = this.streamWatchConnection.get(
StreamKey.userId,
);
// Watch user stream // Watch user stream
switch (event) { switch (event) {
case 'STREAM_CREATE': { case 'STREAM_CREATE': {
@@ -839,7 +910,10 @@ class VoiceConnection extends EventEmitter {
break; break;
} }
case 'STREAM_SERVER_UPDATE': { case 'STREAM_SERVER_UPDATE': {
streamConnection.setTokenAndEndpoint(data.token, data.endpoint); streamConnection.setTokenAndEndpoint(
data.token,
data.endpoint,
);
break; break;
} }
case 'STREAM_DELETE': { case 'STREAM_DELETE': {
@@ -858,7 +932,7 @@ class VoiceConnection extends EventEmitter {
connection.sendSignalScreenshare(); connection.sendSignalScreenshare();
connection.on('debug', msg => connection.on('debug', (msg) =>
this.channel.client.emit( this.channel.client.emit(
'debug', 'debug',
`[VOICE STREAM WATCH (${userId}>${this.channel.guild?.id || this.channel.id}:${ `[VOICE STREAM WATCH (${userId}>${this.channel.guild?.id || this.channel.id}:${
@@ -866,7 +940,7 @@ class VoiceConnection extends EventEmitter {
})]: ${msg}`, })]: ${msg}`,
), ),
); );
connection.once('failed', reason => { connection.once('failed', (reason) => {
this.streamWatchConnection.delete(userId); this.streamWatchConnection.delete(userId);
reject(reason); reject(reason);
}); });
@@ -988,7 +1062,8 @@ class StreamConnection extends VoiceConnection {
this.emit('closing'); this.emit('closing');
this.emit('debug', 'Stream: disconnect() triggered'); this.emit('debug', 'Stream: disconnect() triggered');
clearTimeout(this.connectTimeout); clearTimeout(this.connectTimeout);
if (this.voiceConnection.streamConnection === this) this.voiceConnection.streamConnection = null; if (this.voiceConnection.streamConnection === this)
this.voiceConnection.streamConnection = null;
this.sendStopScreenshare(); this.sendStopScreenshare();
this._disconnect(); this._disconnect();
} }

View File

@@ -20,10 +20,20 @@ Please use the @dank074/discord-video-stream library for the best support.
const { Buffer } = require('buffer'); const { Buffer } = require('buffer');
const VideoDispatcher = require('./VideoDispatcher'); const VideoDispatcher = require('./VideoDispatcher');
const Util = require('../../../util/Util'); const Util = require('../../../util/Util');
const { H264Helpers, H265Helpers } = require('../player/processing/AnnexBNalSplitter'); const {
H264Helpers,
H265Helpers,
} = require('../player/processing/AnnexBNalSplitter');
class AnnexBDispatcher extends VideoDispatcher { class AnnexBDispatcher extends VideoDispatcher {
constructor(player, highWaterMark = 12, streams, fps, nalFunctions, payloadType) { constructor(
player,
highWaterMark = 12,
streams,
fps,
nalFunctions,
payloadType,
) {
super(player, highWaterMark, streams, fps, payloadType); super(player, highWaterMark, streams, fps, payloadType);
this._nalFunctions = nalFunctions; this._nalFunctions = nalFunctions;
} }
@@ -40,12 +50,19 @@ class AnnexBDispatcher extends VideoDispatcher {
const isLastNal = offset + naluSize >= accessUnit.length; const isLastNal = offset + naluSize >= accessUnit.length;
if (nalu.length <= this.mtu) { if (nalu.length <= this.mtu) {
// Send as Single NAL Unit Packet. // Send as Single NAL Unit Packet.
this._playChunk(Buffer.concat([this.createPayloadExtension(), nalu]), isLastNal); this._playChunk(
Buffer.concat([this.createPayloadExtension(), nalu]),
isLastNal,
);
} else { } else {
const [naluHeader, naluData] = this._nalFunctions.splitHeader(nalu); const [naluHeader, naluData] = this._nalFunctions.splitHeader(nalu);
const dataFragments = this.partitionMtu(naluData); const dataFragments = this.partitionMtu(naluData);
// Send as Fragmentation Unit A (FU-A): // Send as Fragmentation Unit A (FU-A):
for (let fragmentIndex = 0; fragmentIndex < dataFragments.length; fragmentIndex++) { for (
let fragmentIndex = 0;
fragmentIndex < dataFragments.length;
fragmentIndex++
) {
const data = dataFragments[fragmentIndex]; const data = dataFragments[fragmentIndex];
const isFirstPacket = fragmentIndex === 0; const isFirstPacket = fragmentIndex === 0;
const isFinalPacket = fragmentIndex === dataFragments.length - 1; const isFinalPacket = fragmentIndex === dataFragments.length - 1;
@@ -53,7 +70,11 @@ class AnnexBDispatcher extends VideoDispatcher {
this._playChunk( this._playChunk(
Buffer.concat([ Buffer.concat([
this.createPayloadExtension(), this.createPayloadExtension(),
this.makeFragmentationUnitHeader(isFirstPacket, isFinalPacket, naluHeader), this.makeFragmentationUnitHeader(
isFirstPacket,
isFinalPacket,
naluHeader,
),
data, data,
]), ]),
isLastNal && isFinalPacket, isLastNal && isFinalPacket,
@@ -67,7 +88,14 @@ class AnnexBDispatcher extends VideoDispatcher {
class H264Dispatcher extends AnnexBDispatcher { class H264Dispatcher extends AnnexBDispatcher {
constructor(player, highWaterMark = 12, streams, fps) { constructor(player, highWaterMark = 12, streams, fps) {
super(player, highWaterMark, streams, fps, H264Helpers, Util.getPayloadType('H264')); super(
player,
highWaterMark,
streams,
fps,
H264Helpers,
Util.getPayloadType('H264'),
);
} }
makeFragmentationUnitHeader(isFirstPacket, isLastPacket, naluHeader) { makeFragmentationUnitHeader(isFirstPacket, isLastPacket, naluHeader) {
@@ -92,7 +120,14 @@ class H264Dispatcher extends AnnexBDispatcher {
class H265Dispatcher extends AnnexBDispatcher { class H265Dispatcher extends AnnexBDispatcher {
constructor(player, highWaterMark = 12, streams, fps) { constructor(player, highWaterMark = 12, streams, fps) {
super(player, highWaterMark, streams, fps, H265Helpers, Util.getPayloadType('H265')); super(
player,
highWaterMark,
streams,
fps,
H265Helpers,
Util.getPayloadType('H265'),
);
} }
makeFragmentationUnitHeader(isFirstPacket, isLastPacket, naluHeader) { makeFragmentationUnitHeader(isFirstPacket, isLastPacket, naluHeader) {

View File

@@ -25,7 +25,11 @@ const CHANNELS = 2;
* @extends {BaseDispatcher} * @extends {BaseDispatcher}
*/ */
class AudioDispatcher extends BaseDispatcher { class AudioDispatcher extends BaseDispatcher {
constructor(player, { seek = 0, volume = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, streams) { constructor(
player,
{ seek = 0, volume = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {},
streams,
) {
const streamOptions = { seek, volume, fec, plp, bitrate, highWaterMark }; const streamOptions = { seek, volume, fec, plp, bitrate, highWaterMark };
super(player, highWaterMark, Util.getPayloadType('opus'), false, streams); super(player, highWaterMark, Util.getPayloadType('opus'), false, streams);
@@ -63,7 +67,8 @@ class AudioDispatcher extends BaseDispatcher {
*/ */
setBitrate(value) { setBitrate(value) {
if (!value || !this.bitrateEditable) return false; if (!value || !this.bitrateEditable) return false;
const bitrate = value === 'auto' ? this.player.voiceConnection.channel.bitrate : value; const bitrate =
value === 'auto' ? this.player.voiceConnection.channel.bitrate : value;
this.streams.opus.setBitrate(bitrate * 1000); this.streams.opus.setBitrate(bitrate * 1000);
return true; return true;
} }

View File

@@ -20,7 +20,13 @@ const extensions = [{ id: 5, length: 2, value: 0 }];
* @extends {Writable} * @extends {Writable}
*/ */
class BaseDispatcher extends Writable { class BaseDispatcher extends Writable {
constructor(player, highWaterMark = 12, payloadType, extensionEnabled, streams = {}) { constructor(
player,
highWaterMark = 12,
payloadType,
extensionEnabled,
streams = {},
) {
super({ super({
highWaterMark, highWaterMark,
}); });
@@ -63,10 +69,14 @@ class BaseDispatcher extends Writable {
}; };
this.on('error', () => streamError()); this.on('error', () => streamError());
if (this.streams.input) this.streams.input.on('error', err => streamError('input', err)); if (this.streams.input)
if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', err => streamError('ffmpeg', err)); this.streams.input.on('error', (err) => streamError('input', err));
if (this.streams.opus) this.streams.opus.on('error', err => streamError('opus', err)); if (this.streams.ffmpeg)
if (this.streams.volume) this.streams.volume.on('error', err => streamError('volume', err)); this.streams.ffmpeg.on('error', (err) => streamError('ffmpeg', err));
if (this.streams.opus)
this.streams.opus.on('error', (err) => streamError('opus', err));
if (this.streams.volume)
this.streams.volume.on('error', (err) => streamError('volume', err));
this.on('finish', () => { this.on('finish', () => {
this._cleanup(); this._cleanup();
@@ -82,7 +92,8 @@ class BaseDispatcher extends Writable {
resetNonceBuffer() { resetNonceBuffer() {
this._nonceBuffer = this._nonceBuffer =
this.player.voiceConnection.authentication.mode === 'aead_aes256_gcm_rtpsize' this.player.voiceConnection.authentication.mode ===
'aead_aes256_gcm_rtpsize'
? Buffer.alloc(12) ? Buffer.alloc(12)
: Buffer.alloc(24); : Buffer.alloc(24);
} }
@@ -179,7 +190,11 @@ class BaseDispatcher extends Writable {
* @readonly * @readonly
*/ */
get pausedTime() { get pausedTime() {
return this._silentPausedTime + this._pausedTime + (this.paused ? performance.now() - this.pausedSince : 0); return (
this._silentPausedTime +
this._pausedTime +
(this.paused ? performance.now() - this.pausedSince : 0)
);
} }
/** /**
@@ -217,9 +232,12 @@ class BaseDispatcher extends Writable {
this._writeCallback = null; this._writeCallback = null;
done(); done();
}; };
const next = (this.count + 1) * this.FRAME_LENGTH - (performance.now() - this.startTime - this._pausedTime); const next =
(this.count + 1) * this.FRAME_LENGTH -
(performance.now() - this.startTime - this._pausedTime);
setTimeout(() => { setTimeout(() => {
if ((!this.pausedSince || this._silence) && this._writeCallback) this._writeCallback(); if ((!this.pausedSince || this._silence) && this._writeCallback)
this._writeCallback();
}, next).unref(); }, next).unref();
this.timestamp += this.TIMESTAMP_INC; this.timestamp += this.TIMESTAMP_INC;
if (this.timestamp > MAX_UINT_32) this.timestamp = 0; if (this.timestamp > MAX_UINT_32) this.timestamp = 0;
@@ -234,7 +252,8 @@ class BaseDispatcher extends Writable {
_playChunk(chunk, isLastPacket = false) { _playChunk(chunk, isLastPacket = false) {
if ( if (
(this.player.dispatcher !== this && this.player.videoDispatcher !== this) || (this.player.dispatcher !== this &&
this.player.videoDispatcher !== this) ||
!this.player.voiceConnection.authentication.secret_key !this.player.voiceConnection.authentication.secret_key
) { ) {
return; return;
@@ -321,15 +340,24 @@ class BaseDispatcher extends Writable {
switch (mode) { switch (mode) {
case 'aead_aes256_gcm_rtpsize': { case 'aead_aes256_gcm_rtpsize': {
const cipher = crypto.createCipheriv('aes-256-gcm', secret_key, this._nonceBuffer); const cipher = crypto.createCipheriv(
'aes-256-gcm',
secret_key,
this._nonceBuffer,
);
cipher.setAAD(additionalData); cipher.setAAD(additionalData);
encrypted = Buffer.concat([cipher.update(buffer), cipher.final(), cipher.getAuthTag()]); encrypted = Buffer.concat([
cipher.update(buffer),
cipher.final(),
cipher.getAuthTag(),
]);
return [encrypted, noncePadding]; return [encrypted, noncePadding];
} }
case 'aead_xchacha20_poly1305_rtpsize': { case 'aead_xchacha20_poly1305_rtpsize': {
encrypted = secretbox.methods.crypto_aead_xchacha20poly1305_ietf_encrypt( encrypted =
secretbox.methods.crypto_aead_xchacha20poly1305_ietf_encrypt(
buffer, buffer,
additionalData, additionalData,
this._nonceBuffer, this._nonceBuffer,
@@ -385,7 +413,8 @@ class BaseDispatcher extends Writable {
rtpHeader.writeUIntBE(this.getNewSequence(), 2, 2); rtpHeader.writeUIntBE(this.getNewSequence(), 2, 2);
rtpHeader.writeUIntBE(this.timestamp, 4, 4); rtpHeader.writeUIntBE(this.timestamp, 4, 4);
rtpHeader.writeUIntBE( rtpHeader.writeUIntBE(
this.player.voiceConnection.authentication.ssrc + Number(this.getTypeDispatcher() === 'video'), this.player.voiceConnection.authentication.ssrc +
Number(this.getTypeDispatcher() === 'video'),
8, 8,
4, 4,
); );
@@ -408,7 +437,7 @@ class BaseDispatcher extends Writable {
this.emit('debug', 'Failed to send a packet - no UDP socket'); this.emit('debug', 'Failed to send a packet - no UDP socket');
return; return;
} }
this.player.voiceConnection.sockets.udp.send(packet).catch(e => { this.player.voiceConnection.sockets.udp.send(packet).catch((e) => {
if (this.getTypeDispatcher() === 'audio') { if (this.getTypeDispatcher() === 'audio') {
this._setSpeaking(this._setSpeaking(0)); this._setSpeaking(this._setSpeaking(0));
} else if (this.getTypeDispatcher() === 'video') { } else if (this.getTypeDispatcher() === 'video') {
@@ -444,7 +473,9 @@ class BaseDispatcher extends Writable {
} }
_setStreamStatus(value) { _setStreamStatus(value) {
if (typeof this.player.voiceConnection?.sendScreenshareState !== 'undefined') { if (
typeof this.player.voiceConnection?.sendScreenshareState !== 'undefined'
) {
this.player.voiceConnection.sendScreenshareState(value); this.player.voiceConnection.sendScreenshareState(value);
} }
/** /**

View File

@@ -38,11 +38,18 @@ class VP8Dispatcher extends VideoDispatcher {
const pictureIdBuf = Buffer.alloc(2); const pictureIdBuf = Buffer.alloc(2);
pictureIdBuf.writeUintBE(this.count, 0, 2); pictureIdBuf.writeUintBE(this.count, 0, 2);
pictureIdBuf[0] |= 0x80; pictureIdBuf[0] |= 0x80;
return Buffer.concat([this.createPayloadExtension(), payloadDescriptorBuf, pictureIdBuf, buffer]); return Buffer.concat([
this.createPayloadExtension(),
payloadDescriptorBuf,
pictureIdBuf,
buffer,
]);
} }
_codecCallback(chunk) { _codecCallback(chunk) {
const chunkSplit = this.partitionMtu(chunk).map((c, i) => this.makeChunk(c, i === 0)); const chunkSplit = this.partitionMtu(chunk).map((c, i) =>
this.makeChunk(c, i === 0),
);
for (let i = 0; i < chunkSplit.length; i++) { for (let i = 0; i < chunkSplit.length; i++) {
this._playChunk(chunkSplit[i], i + 1 === chunkSplit.length); this._playChunk(chunkSplit[i], i + 1 === chunkSplit.length);
} }

View File

@@ -79,22 +79,30 @@ class VoiceConnectionUDPClient extends EventEmitter {
send(packet) { send(packet) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.socket) throw new Error('UDP_SEND_FAIL'); if (!this.socket) throw new Error('UDP_SEND_FAIL');
if (!this.discordAddress || !this.discordPort) throw new Error('UDP_ADDRESS_MALFORMED'); if (!this.discordAddress || !this.discordPort)
this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { throw new Error('UDP_ADDRESS_MALFORMED');
this.socket.send(
packet,
0,
packet.length,
this.discordPort,
this.discordAddress,
(error) => {
if (error) { if (error) {
this.emit('debug', `[UDP] >> ERROR: ${error}`); this.emit('debug', `[UDP] >> ERROR: ${error}`);
reject(error); reject(error);
} else { } else {
resolve(packet); resolve(packet);
} }
}); },
);
}); });
} }
async createUDPSocket(address) { async createUDPSocket(address) {
this.discordAddress = address; this.discordAddress = address;
const socket = (this.socket = udp.createSocket('udp4')); const socket = (this.socket = udp.createSocket('udp4'));
socket.on('error', e => { socket.on('error', (e) => {
this.emit('debug', `[UDP] Error: ${e}`); this.emit('debug', `[UDP] Error: ${e}`);
this.emit('error', e); this.emit('error', e);
}); });
@@ -102,7 +110,7 @@ class VoiceConnectionUDPClient extends EventEmitter {
this.emit('debug', '[UDP] socket closed'); this.emit('debug', '[UDP] socket closed');
}); });
this.emit('debug', `[UDP] created socket`); this.emit('debug', `[UDP] created socket`);
socket.once('message', message => { socket.once('message', (message) => {
this.emit('debug', `[UDP] message: [${[...message]}] (${message})`); this.emit('debug', `[UDP] message: [${[...message]}] (${message})`);
if (message.readUInt16BE(0) !== 2) { if (message.readUInt16BE(0) !== 2) {
throw new Error('UDP_WRONG_HANDSHAKE'); throw new Error('UDP_WRONG_HANDSHAKE');
@@ -117,7 +125,10 @@ class VoiceConnectionUDPClient extends EventEmitter {
return; return;
} }
this.emit('debug', `[UDP] Parse local packet: ${packet.address}:${packet.port}`); this.emit(
'debug',
`[UDP] Parse local packet: ${packet.address}:${packet.port}`,
);
this.localAddress = packet.address; this.localAddress = packet.address;
this.localPort = packet.port; this.localPort = packet.port;
@@ -143,7 +154,9 @@ class VoiceConnectionUDPClient extends EventEmitter {
this.emit('debug', `[UDP] << ${JSON.stringify(packet)}`); this.emit('debug', `[UDP] << ${JSON.stringify(packet)}`);
socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer)); socket.on('message', (buffer) =>
this.voiceConnection.receiver.packets.push(buffer),
);
}); });
const blankMessage = Buffer.alloc(74); const blankMessage = Buffer.alloc(74);

View File

@@ -67,7 +67,10 @@ class VoiceWebSocket extends EventEmitter {
if (this.dead) return; if (this.dead) return;
if (this.ws) this.reset(); if (this.ws) this.reset();
if (this.attempts >= 5) { if (this.attempts >= 5) {
this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts)); this.emit(
'debug',
new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts),
);
return; return;
} }
@@ -77,8 +80,14 @@ class VoiceWebSocket extends EventEmitter {
* The actual WebSocket used to connect to the Voice WebSocket Server. * The actual WebSocket used to connect to the Voice WebSocket Server.
* @type {WebSocket} * @type {WebSocket}
*/ */
this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 8 }); this.ws = WebSocket.create(
this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`); `wss://${this.connection.authentication.endpoint}/`,
{ v: 8 },
);
this.emit(
'debug',
`[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`,
);
this.ws.onopen = this.onOpen.bind(this); this.ws.onopen = this.onOpen.bind(this);
this.ws.onmessage = this.onMessage.bind(this); this.ws.onmessage = this.onMessage.bind(this);
this.ws.onclose = this.onClose.bind(this); this.ws.onclose = this.onClose.bind(this);
@@ -93,8 +102,9 @@ class VoiceWebSocket extends EventEmitter {
send(data) { send(data) {
this.emit('debug', `[WS] >> ${data}`); this.emit('debug', `[WS] >> ${data}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data); if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
this.ws.send(data, null, error => { throw new Error('WS_NOT_OPEN', data);
this.ws.send(data, null, (error) => {
if (error) reject(error); if (error) reject(error);
else resolve(data); else resolve(data);
}); });
@@ -115,11 +125,17 @@ class VoiceWebSocket extends EventEmitter {
* Called whenever the WebSocket opens. * Called whenever the WebSocket opens.
*/ */
onOpen() { onOpen() {
this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`); this.emit(
'debug',
`[WS] opened at gateway ${this.connection.authentication.endpoint}`,
);
this.sendPacket({ this.sendPacket({
op: Opcodes.DISPATCH, op: Opcodes.DISPATCH,
d: { d: {
server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id, server_id:
this.connection.serverId ||
this.connection.channel.guild?.id ||
this.connection.channel.id,
user_id: this.client.user.id, user_id: this.client.user.id,
token: this.connection.authentication.token, token: this.connection.authentication.token,
session_id: this.connection.authentication.sessionId, session_id: this.connection.authentication.sessionId,
@@ -149,8 +165,12 @@ class VoiceWebSocket extends EventEmitter {
* @param {CloseEvent} event The WebSocket close event * @param {CloseEvent} event The WebSocket close event
*/ */
onClose(event) { onClose(event) {
this.emit('debug', `[WS] closed with code ${event.code} and reason: ${event.reason}`); this.emit(
if (!this.dead) setTimeout(this.connect.bind(this), this.attempts * 1000).unref(); 'debug',
`[WS] closed with code ${event.code} and reason: ${event.reason}`,
);
if (!this.dead)
setTimeout(this.connect.bind(this), this.attempts * 1000).unref();
} }
/** /**
@@ -200,7 +220,9 @@ class VoiceWebSocket extends EventEmitter {
}); });
break; break;
case VoiceOpcodes.CLIENT_DISCONNECT: case VoiceOpcodes.CLIENT_DISCONNECT:
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id); const streamInfo =
this.connection.receiver &&
this.connection.receiver.packets.streams.get(packet.d.user_id);
if (streamInfo) { if (streamInfo) {
this.connection.receiver.packets.streams.delete(packet.d.user_id); this.connection.receiver.packets.streams.delete(packet.d.user_id);
streamInfo.stream.push(null); streamInfo.stream.push(null);
@@ -251,7 +273,10 @@ class VoiceWebSocket extends EventEmitter {
this.emit('warn', 'A voice heartbeat interval is being overwritten'); this.emit('warn', 'A voice heartbeat interval is being overwritten');
clearInterval(this.heartbeatInterval); clearInterval(this.heartbeatInterval);
} }
this.heartbeatInterval = setInterval(this.sendHeartbeat.bind(this), interval).unref(); this.heartbeatInterval = setInterval(
this.sendHeartbeat.bind(this),
interval,
).unref();
} }
/** /**
@@ -259,7 +284,10 @@ class VoiceWebSocket extends EventEmitter {
*/ */
clearHeartbeat() { clearHeartbeat() {
if (!this.heartbeatInterval) { if (!this.heartbeatInterval) {
this.emit('warn', 'Tried to clear a heartbeat interval that does not exist'); this.emit(
'warn',
'Tried to clear a heartbeat interval that does not exist',
);
return; return;
} }
clearInterval(this.heartbeatInterval); clearInterval(this.heartbeatInterval);

View File

@@ -20,13 +20,22 @@ Please use the @dank074/discord-video-stream library for the best support.
const EventEmitter = require('events'); const EventEmitter = require('events');
const { Readable: ReadableStream } = require('stream'); const { Readable: ReadableStream } = require('stream');
const prism = require('prism-media'); const prism = require('prism-media');
const { H264NalSplitter, H265NalSplitter } = require('./processing/AnnexBNalSplitter'); const {
H264NalSplitter,
H265NalSplitter,
} = require('./processing/AnnexBNalSplitter');
const { IvfTransformer } = require('./processing/IvfSplitter'); const { IvfTransformer } = require('./processing/IvfSplitter');
const { H264Dispatcher } = require('../dispatcher/AnnexBDispatcher'); const { H264Dispatcher } = require('../dispatcher/AnnexBDispatcher');
const AudioDispatcher = require('../dispatcher/AudioDispatcher'); const AudioDispatcher = require('../dispatcher/AudioDispatcher');
const { VP8Dispatcher } = require('../dispatcher/VPxDispatcher'); const { VP8Dispatcher } = require('../dispatcher/VPxDispatcher');
const FFMPEG_OUTPUT_PREFIX = ['-use_wallclock_as_timestamps', '1', '-copyts', '-analyzeduration', '0']; const FFMPEG_OUTPUT_PREFIX = [
'-use_wallclock_as_timestamps',
'1',
'-copyts',
'-analyzeduration',
'0',
];
const FFMPEG_INPUT_PREFIX = [ const FFMPEG_INPUT_PREFIX = [
'-reconnect', '-reconnect',
'1', '1',
@@ -38,8 +47,15 @@ const FFMPEG_INPUT_PREFIX = [
'4294', '4294',
]; ];
const FFMPEG_PCM_ARGUMENTS = ['-f', 's16le', '-ar', '48000', '-ac', '2']; const FFMPEG_PCM_ARGUMENTS = ['-f', 's16le', '-ar', '48000', '-ac', '2'];
const FFMPEG_VP8_ARGUMENTS = ['-f', 'ivf', '-deadline', 'realtime', '-c:v', 'libvpx']; const FFMPEG_VP8_ARGUMENTS = [
const FFMPEG_H264_ARGUMENTS = options => [ '-f',
'ivf',
'-deadline',
'realtime',
'-c:v',
'libvpx',
];
const FFMPEG_H264_ARGUMENTS = (options) => [
'-c:v', '-c:v',
'libx264', 'libx264',
'-f', '-f',
@@ -62,7 +78,7 @@ const FFMPEG_H264_ARGUMENTS = options => [
'h264_metadata=aud=insert', 'h264_metadata=aud=insert',
]; ];
const FFMPEG_H265_ARGUMENTS = options => [ const FFMPEG_H265_ARGUMENTS = (options) => [
'-c:v', '-c:v',
'libx265', 'libx265',
'-f', '-f',
@@ -127,9 +143,12 @@ class MediaPlayer extends EventEmitter {
} }
const ffmpeg = new prism.FFmpeg({ args }); const ffmpeg = new prism.FFmpeg({ args });
this.emit('debug', `[ffmpeg-audio_process] Spawn process with args:\n${args.join(' ')}`); this.emit(
'debug',
`[ffmpeg-audio_process] Spawn process with args:\n${args.join(' ')}`,
);
ffmpeg.process.stderr.on('data', data => { ffmpeg.process.stderr.on('data', (data) => {
this.emit('debug', `[ffmpeg-audio_process]: ${data.toString()}`); this.emit('debug', `[ffmpeg-audio_process]: ${data.toString()}`);
}); });
@@ -143,12 +162,19 @@ class MediaPlayer extends EventEmitter {
playPCMStream(stream, options, streams = {}) { playPCMStream(stream, options, streams = {}) {
this.destroyDispatcher(); this.destroyDispatcher();
const opus = (streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 })); const opus = (streams.opus = new prism.opus.Encoder({
channels: 2,
rate: 48000,
frameSize: 960,
}));
if (options && options.volume === false) { if (options && options.volume === false) {
stream.pipe(opus); stream.pipe(opus);
return this.playOpusStream(opus, options, streams); return this.playOpusStream(opus, options, streams);
} }
streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); streams.volume = new prism.VolumeTransformer({
type: 's16le',
volume: options ? options.volume : 1,
});
stream.pipe(streams.volume).pipe(opus); stream.pipe(streams.volume).pipe(opus);
return this.playOpusStream(opus, options, streams); return this.playOpusStream(opus, options, streams);
} }
@@ -158,12 +184,21 @@ class MediaPlayer extends EventEmitter {
streams.opus = stream; streams.opus = stream;
if (options.volume !== false && !streams.input) { if (options.volume !== false && !streams.input) {
streams.input = stream; streams.input = stream;
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); const decoder = new prism.opus.Decoder({
streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); channels: 2,
rate: 48000,
frameSize: 960,
});
streams.volume = new prism.VolumeTransformer({
type: 's16le',
volume: options ? options.volume : 1,
});
streams.opus = stream streams.opus = stream
.pipe(decoder) .pipe(decoder)
.pipe(streams.volume) .pipe(streams.volume)
.pipe(new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 })); .pipe(
new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }),
);
} }
const dispatcher = this.createDispatcher(options, streams); const dispatcher = this.createDispatcher(options, streams);
streams.opus.pipe(dispatcher); streams.opus.pipe(dispatcher);
@@ -232,9 +267,12 @@ class MediaPlayer extends EventEmitter {
input.pipe(ffmpeg); input.pipe(ffmpeg);
} }
this.emit('debug', `[ffmpeg-video_process] Spawn process with args:\n${args.join(' ')}`); this.emit(
'debug',
`[ffmpeg-video_process] Spawn process with args:\n${args.join(' ')}`,
);
ffmpeg.process.stderr.on('data', data => { ffmpeg.process.stderr.on('data', (data) => {
this.emit('debug', `[ffmpeg-video_process]: ${data.toString()}`); this.emit('debug', `[ffmpeg-video_process]: ${data.toString()}`);
}); });
@@ -279,7 +317,11 @@ class MediaPlayer extends EventEmitter {
createDispatcher(options, streams) { createDispatcher(options, streams) {
this.destroyDispatcher(); this.destroyDispatcher();
const dispatcher = (this.dispatcher = new AudioDispatcher(this, options, streams)); const dispatcher = (this.dispatcher = new AudioDispatcher(
this,
options,
streams,
));
return dispatcher; return dispatcher;
} }

View File

@@ -166,7 +166,9 @@ class AnnexBNalSplitter extends Transform {
findNalStart(buf) { findNalStart(buf) {
const pos = buf.indexOf(nalSuffix); const pos = buf.indexOf(nalSuffix);
if (pos === -1) return null; if (pos === -1) return null;
return pos > 0 && buf[pos - 1] === 0 ? { index: pos - 1, length: 4 } : { index: pos, length: 3 }; return pos > 0 && buf[pos - 1] === 0
? { index: pos - 1, length: 4 }
: { index: pos, length: 3 };
} }
processFrame(frame) { processFrame(frame) {
@@ -174,11 +176,14 @@ class AnnexBNalSplitter extends Transform {
const unitType = this._nalFunctions.getUnitType(frame); const unitType = this._nalFunctions.getUnitType(frame);
if (this._nalFunctions.isAUD(unitType) && this._accessUnit.length > 0) { if (this._nalFunctions.isAUD(unitType) && this._accessUnit.length > 0) {
const sizeOfAccessUnit = this._accessUnit.reduce((acc, nalu) => acc + nalu.length + 4, 0); const sizeOfAccessUnit = this._accessUnit.reduce(
(acc, nalu) => acc + nalu.length + 4,
0,
);
const accessUnitBuf = Buffer.allocUnsafe(sizeOfAccessUnit); const accessUnitBuf = Buffer.allocUnsafe(sizeOfAccessUnit);
let offset = 0; let offset = 0;
this._accessUnit.forEach(nalu => { this._accessUnit.forEach((nalu) => {
accessUnitBuf.writeUint32BE(nalu.length, offset); accessUnitBuf.writeUint32BE(nalu.length, offset);
offset += 4; offset += 4;
nalu.copy(accessUnitBuf, offset); nalu.copy(accessUnitBuf, offset);
@@ -220,7 +225,10 @@ class H264NalSplitter extends AnnexBNalSplitter {
} }
removeEpbs(frame, unitType) { removeEpbs(frame, unitType) {
return unitType === H264NalUnitTypes.SPS || unitType === H264NalUnitTypes.SEI ? this.rbsp(frame) : frame; return unitType === H264NalUnitTypes.SPS ||
unitType === H264NalUnitTypes.SEI
? this.rbsp(frame)
: frame;
} }
} }

View File

@@ -28,7 +28,8 @@ class IvfTransformer extends Transform {
this.header = null; this.header = null;
this.buf = null; this.buf = null;
this.retFullFrame = options && options.fullframe ? options.fullframe : false; this.retFullFrame =
options && options.fullframe ? options.fullframe : false;
} }
_parseHeader(header) { _parseHeader(header) {
@@ -69,7 +70,8 @@ class IvfTransformer extends Transform {
} }
_updateBufLen(size) { _updateBufLen(size) {
if (this.buf.length > size) this.buf = this.buf.subarray(size, this.buf.length); if (this.buf.length > size)
this.buf = this.buf.subarray(size, this.buf.length);
else this.buf = null; else this.buf = null;
} }

View File

@@ -33,7 +33,8 @@ class PacketHandler extends EventEmitter {
} }
getNonceBuffer() { getNonceBuffer() {
return this.receiver.connection.authentication.mode === 'aead_aes256_gcm_rtpsize' return this.receiver.connection.authentication.mode ===
'aead_aes256_gcm_rtpsize'
? Buffer.alloc(12) ? Buffer.alloc(12)
: Buffer.alloc(24); : Buffer.alloc(24);
} }
@@ -88,7 +89,10 @@ class PacketHandler extends EventEmitter {
const header = buffer.slice(0, headerSize); const header = buffer.slice(0, headerSize);
// Encrypted contains the extension, if any, the opus packet, and the auth tag // Encrypted contains the extension, if any, the opus packet, and the auth tag
const encrypted = buffer.slice(headerSize, buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH); const encrypted = buffer.slice(
headerSize,
buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH,
);
const authTag = buffer.slice( const authTag = buffer.slice(
buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH, buffer.length - AUTH_TAG_LENGTH - UNPADDED_NONCE_LENGTH,
buffer.length - UNPADDED_NONCE_LENGTH, buffer.length - UNPADDED_NONCE_LENGTH,
@@ -97,11 +101,18 @@ class PacketHandler extends EventEmitter {
let packet; let packet;
switch (mode) { switch (mode) {
case 'aead_aes256_gcm_rtpsize': { case 'aead_aes256_gcm_rtpsize': {
const decipheriv = crypto.createDecipheriv('aes-256-gcm', secret_key, nonce); const decipheriv = crypto.createDecipheriv(
'aes-256-gcm',
secret_key,
nonce,
);
decipheriv.setAAD(header); decipheriv.setAAD(header);
decipheriv.setAuthTag(authTag); decipheriv.setAuthTag(authTag);
packet = Buffer.concat([decipheriv.update(encrypted), decipheriv.final()]); packet = Buffer.concat([
decipheriv.update(encrypted),
decipheriv.final(),
]);
break; break;
} }
case 'aead_xchacha20_poly1305_rtpsize': { case 'aead_xchacha20_poly1305_rtpsize': {
@@ -163,10 +174,18 @@ class PacketHandler extends EventEmitter {
if (userStat.speaking === 0) { if (userStat.speaking === 0) {
userStat.speaking = Speaking.FLAGS.SPEAKING; userStat.speaking = Speaking.FLAGS.SPEAKING;
} }
this.connection.onSpeaking({ user_id: userStat.userId, ssrc: ssrc, speaking: userStat.speaking }); this.connection.onSpeaking({
user_id: userStat.userId,
ssrc: ssrc,
speaking: userStat.speaking,
});
speakingTimeout = setTimeout(() => { speakingTimeout = setTimeout(() => {
try { try {
this.connection.onSpeaking({ user_id: userStat.userId, ssrc: ssrc, speaking: 0 }); this.connection.onSpeaking({
user_id: userStat.userId,
ssrc: ssrc,
speaking: 0,
});
clearTimeout(speakingTimeout); clearTimeout(speakingTimeout);
this.speakingTimeouts.delete(ssrc); this.speakingTimeouts.delete(ssrc);
} catch { } catch {
@@ -241,7 +260,8 @@ class PacketHandler extends EventEmitter {
packet = this.parseBuffer(buffer); packet = this.parseBuffer(buffer);
this.videoReceiver(ssrc, userStat, packet); this.videoReceiver(ssrc, userStat, packet);
} }
if (userStat && !(packet instanceof Error)) this.receiver.emit('receiverData', userStat, packet); if (userStat && !(packet instanceof Error))
this.receiver.emit('receiverData', userStat, packet);
} }
// When udp connection is closed (STREAM_DELETE), destroy all streams (Memory leak) // When udp connection is closed (STREAM_DELETE), destroy all streams (Memory leak)

View File

@@ -23,7 +23,7 @@ class VoiceReceiver extends EventEmitter {
* @event VoiceReceiver#debug * @event VoiceReceiver#debug
* @param {Error|string} error The error or message to debug * @param {Error|string} error The error or message to debug
*/ */
this.packets.on('error', err => this.emit('debug', err)); this.packets.on('error', (err) => this.emit('debug', err));
} }
/** /**
@@ -45,24 +45,39 @@ class VoiceReceiver extends EventEmitter {
* @param {ReceiveStreamOptions} options Options. * @param {ReceiveStreamOptions} options Options.
* @returns {ReadableStream} * @returns {ReadableStream}
*/ */
createStream(user, { mode = 'opus', end = 'silence', paddingSilence = false } = {}) { createStream(
user,
{ mode = 'opus', end = 'silence', paddingSilence = false } = {},
) {
user = this.connection.client.users.resolve(user); user = this.connection.client.users.resolve(user);
if (end === 'silence') paddingSilence = false; if (end === 'silence') paddingSilence = false;
if (!user) throw new Error('VOICE_USER_MISSING'); if (!user) throw new Error('VOICE_USER_MISSING');
const stream = this.packets.makeStream(user.id, end); // Opus stream const stream = this.packets.makeStream(user.id, end); // Opus stream
if (paddingSilence) { if (paddingSilence) {
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); const decoder = new prism.opus.Decoder({
channels: 2,
rate: 48000,
frameSize: 960,
});
const pcmTransformer = new PCMInsertSilence(); const pcmTransformer = new PCMInsertSilence();
stream.pipe(decoder).pipe(pcmTransformer); stream.pipe(decoder).pipe(pcmTransformer);
if (mode === 'opus') { if (mode === 'opus') {
const encoder = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); const encoder = new prism.opus.Encoder({
channels: 2,
rate: 48000,
frameSize: 960,
});
pcmTransformer.pipe(encoder); pcmTransformer.pipe(encoder);
return encoder; return encoder;
} }
return pcmTransformer; return pcmTransformer;
} else { } else {
if (mode === 'pcm') { if (mode === 'pcm') {
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); const decoder = new prism.opus.Decoder({
channels: 2,
rate: 48000,
frameSize: 960,
});
stream.pipe(decoder); stream.pipe(decoder);
return decoder; return decoder;
} }

View File

@@ -35,8 +35,8 @@ class Recorder extends EventEmitter {
this.promise = null; this.promise = null;
if (!portUdpH264 || !portUdpOpus) { if (!portUdpH264 || !portUdpOpus) {
this.promise = randomPorts(6, 'udp4').then(ports => { this.promise = randomPorts(6, 'udp4').then((ports) => {
ports = ports.filter(port => port % 2 === 0); ports = ports.filter((port) => port % 2 === 0);
this.portUdpH264 ??= ports[0]; this.portUdpH264 ??= ports[0];
this.portUdpOpus ??= ports[1]; this.portUdpOpus ??= ports[1];
}); });
@@ -60,7 +60,11 @@ class Recorder extends EventEmitter {
} }
async init(output) { async init(output) {
await this.promise; await this.promise;
const sdpData = Util.getSDPCodecName(this.portUdpH264, this.portUdpH265, this.portUdpOpus); const sdpData = Util.getSDPCodecName(
this.portUdpH264,
this.portUdpH265,
this.portUdpOpus,
);
const isStream = output instanceof Writable; const isStream = output instanceof Writable;
if (isStream) { if (isStream) {
this.outputStream = StreamOutput(output); this.outputStream = StreamOutput(output);
@@ -109,7 +113,7 @@ class Recorder extends EventEmitter {
this.stream = stream; this.stream = stream;
this.stream.stdin.write(sdpData); this.stream.stdin.write(sdpData);
this.stream.stdin.end(); this.stream.stdin.end();
this.stream.stderr.once('data', data => { this.stream.stderr.once('data', (data) => {
this.emit('debug', `stderr: ${data}`); this.emit('debug', `stderr: ${data}`);
this.ready = true; this.ready = true;
this.emit('ready'); this.emit('ready');
@@ -122,14 +126,16 @@ class Recorder extends EventEmitter {
*/ */
feed( feed(
payload, payload,
callback = e => { callback = (e) => {
if (e) { if (e) {
console.error('Error sending packet:', e); console.error('Error sending packet:', e);
} }
}, },
) { ) {
if (!(payload instanceof RtpPacket)) { if (!(payload instanceof RtpPacket)) {
payload = RtpPacket.deSerialize(Buffer.isBuffer(payload) ? payload : Buffer.from(payload)); payload = RtpPacket.deSerialize(
Buffer.isBuffer(payload) ? payload : Buffer.from(payload),
);
} }
const message = payload.serialize(); const message = payload.serialize();
// Get port from payloadType // Get port from payloadType
@@ -149,8 +155,11 @@ class Recorder extends EventEmitter {
destroy() { destroy() {
const ffmpegPid = this.stream.pid; // But it is ppid ;-; const ffmpegPid = this.stream.pid; // But it is ppid ;-;
const args = this.stream.spawnargs.slice(1).join(' '); // Skip ffmpeg const args = this.stream.spawnargs.slice(1).join(' '); // Skip ffmpeg
find('name', 'ffmpeg', true).then(list => { find('name', 'ffmpeg', true).then((list) => {
let process = list.find(o => o.pid === ffmpegPid || o.ppid === ffmpegPid || o.cmd.includes(args)); let process = list.find(
(o) =>
o.pid === ffmpegPid || o.ppid === ffmpegPid || o.cmd.includes(args),
);
if (process) { if (process) {
kill(process.pid); kill(process.pid);
this.receiver?.videoStreams?.delete(this.userId); this.receiver?.videoStreams?.delete(this.userId);

View File

@@ -41,7 +41,7 @@ async function randomPort(protocol = 'udp4', interfaceAddresses) {
}); });
const port = socket.address()?.port; const port = socket.address()?.port;
await new Promise(resolve => socket.close(resolve)); await new Promise((resolve) => socket.close(resolve));
return port; return port;
} }
@@ -53,7 +53,11 @@ async function randomPort(protocol = 'udp4', interfaceAddresses) {
* @returns {Promise<number[]>} An array of assigned random ports. * @returns {Promise<number[]>} An array of assigned random ports.
*/ */
async function randomPorts(num, protocol = 'udp4', interfaceAddresses) { async function randomPorts(num, protocol = 'udp4', interfaceAddresses) {
return Promise.all(Array.from({ length: num }).map(() => randomPort(protocol, interfaceAddresses))); return Promise.all(
Array.from({ length: num }).map(() =>
randomPort(protocol, interfaceAddresses),
),
);
} }
/** /**
@@ -78,12 +82,12 @@ async function findPort(min, max, protocol = 'udp4', interfaceAddresses) {
}), }),
); );
const error = await new Promise(resolve => { const error = await new Promise((resolve) => {
socket.once('error', resolve); socket.once('error', resolve);
socket.once('listening', () => resolve(null)); socket.once('listening', () => resolve(null));
}); });
await new Promise(resolve => socket.close(resolve)); await new Promise((resolve) => socket.close(resolve));
if (error) continue; if (error) continue;

View File

@@ -64,11 +64,19 @@ class PlayInterface {
} else if (type === 'opus') { } else if (type === 'opus') {
return this.player.playOpusStream(resource, options); return this.player.playOpusStream(resource, options);
} else if (type === 'ogg/opus') { } else if (type === 'ogg/opus') {
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); if (!(resource instanceof Readable))
return this.player.playOpusStream(resource.pipe(new prism.opus.OggDemuxer()), options); throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
return this.player.playOpusStream(
resource.pipe(new prism.opus.OggDemuxer()),
options,
);
} else if (type === 'webm/opus') { } else if (type === 'webm/opus') {
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); if (!(resource instanceof Readable))
return this.player.playOpusStream(resource.pipe(new prism.opus.WebmDemuxer()), options); throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
return this.player.playOpusStream(
resource.pipe(new prism.opus.WebmDemuxer()),
options,
);
} }
} }
throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE');
@@ -114,7 +122,11 @@ class PlayInterface {
static applyToClass(structure) { static applyToClass(structure) {
for (const prop of ['playAudio', 'playVideo']) { for (const prop of ['playAudio', 'playVideo']) {
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop)); Object.defineProperty(
structure.prototype,
prop,
Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop),
);
} }
} }
} }

View File

@@ -1,24 +1,78 @@
'use strict'; 'use strict';
const libs = { const libs = {
sodium: sodium => ({ sodium: (sodium) => ({
crypto_aead_xchacha20poly1305_ietf_encrypt: (plaintext, additionalData, nonce, key) => crypto_aead_xchacha20poly1305_ietf_encrypt: (
sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce, key), plaintext,
crypto_aead_xchacha20poly1305_ietf_decrypt: (plaintext, additionalData, nonce, key) => additionalData,
sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, null, nonce, key), nonce,
key,
) =>
sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(
plaintext,
additionalData,
null,
nonce,
key,
),
crypto_aead_xchacha20poly1305_ietf_decrypt: (
plaintext,
additionalData,
nonce,
key,
) =>
sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(
plaintext,
additionalData,
null,
nonce,
key,
),
}), }),
'libsodium-wrappers': sodium => ({ 'libsodium-wrappers': (sodium) => ({
crypto_aead_xchacha20poly1305_ietf_encrypt: (plaintext, additionalData, nonce, key) => crypto_aead_xchacha20poly1305_ietf_encrypt: (
sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce, key), plaintext,
crypto_aead_xchacha20poly1305_ietf_decrypt: (plaintext, additionalData, nonce, key) => additionalData,
sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, plaintext, additionalData, nonce, key), nonce,
key,
) =>
sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
plaintext,
additionalData,
null,
nonce,
key,
),
crypto_aead_xchacha20poly1305_ietf_decrypt: (
plaintext,
additionalData,
nonce,
key,
) =>
sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
null,
plaintext,
additionalData,
nonce,
key,
),
}), }),
'@stablelib/xchacha20poly1305': stablelib => ({ '@stablelib/xchacha20poly1305': (stablelib) => ({
crypto_aead_xchacha20poly1305_ietf_encrypt(cipherText, additionalData, nonce, key) { crypto_aead_xchacha20poly1305_ietf_encrypt(
cipherText,
additionalData,
nonce,
key,
) {
const crypto = new stablelib.XChaCha20Poly1305(key); const crypto = new stablelib.XChaCha20Poly1305(key);
return crypto.seal(nonce, cipherText, additionalData); return crypto.seal(nonce, cipherText, additionalData);
}, },
crypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, nonce, key) { crypto_aead_xchacha20poly1305_ietf_decrypt(
plaintext,
additionalData,
nonce,
key,
) {
const crypto = new stablelib.XChaCha20Poly1305(key); const crypto = new stablelib.XChaCha20Poly1305(key);
return crypto.open(nonce, plaintext, additionalData); return crypto.open(nonce, plaintext, additionalData);
}, },

View File

@@ -52,11 +52,11 @@ class UnixStream {
} }
function StreamInput(stream) { function StreamInput(stream) {
return new UnixStream(stream, socket => stream.pipe(socket)); return new UnixStream(stream, (socket) => stream.pipe(socket));
} }
function StreamOutput(stream) { function StreamOutput(stream) {
return new UnixStream(stream, socket => socket.pipe(stream)); return new UnixStream(stream, (socket) => socket.pipe(stream));
} }
module.exports = { StreamOutput, StreamInput, UnixStream }; module.exports = { StreamOutput, StreamInput, UnixStream };

View File

@@ -56,7 +56,10 @@ class VolumeInterface extends EventEmitter {
const out = Buffer.alloc(buffer.length); const out = Buffer.alloc(buffer.length);
for (let i = 0; i < buffer.length; i += 2) { for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) break; if (i >= buffer.length - 1) break;
const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i)))); const uint = Math.min(
32767,
Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i))),
);
out.writeInt16LE(uint, i); out.writeInt16LE(uint, i);
} }
@@ -95,10 +98,19 @@ class VolumeInterface extends EventEmitter {
} }
} }
const props = ['volumeDecibels', 'volumeLogarithmic', 'setVolumeDecibels', 'setVolumeLogarithmic']; const props = [
'volumeDecibels',
'volumeLogarithmic',
'setVolumeDecibels',
'setVolumeLogarithmic',
];
exports.applyToClass = function applyToClass(structure) { exports.applyToClass = function applyToClass(structure) {
for (const prop of props) { for (const prop of props) {
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop)); Object.defineProperty(
structure.prototype,
prop,
Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop),
);
} }
}; };

View File

@@ -8,7 +8,13 @@ const { RPCErrorCodes } = require('discord-api-types/v10');
const WebSocketShard = require('./WebSocketShard'); const WebSocketShard = require('./WebSocketShard');
const PacketHandlers = require('./handlers'); const PacketHandlers = require('./handlers');
const { Error } = require('../../errors'); const { Error } = require('../../errors');
const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants'); const {
Events,
ShardEvents,
Status,
WSCodes,
WSEvents,
} = require('../../util/Constants');
const BeforeReadyWhitelist = [ const BeforeReadyWhitelist = [
WSEvents.READY, WSEvents.READY,
@@ -70,7 +76,10 @@ class WebSocketManager extends EventEmitter {
* @private * @private
* @name WebSocketManager#shardQueue * @name WebSocketManager#shardQueue
*/ */
Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true }); Object.defineProperty(this, 'shardQueue', {
value: new Set(),
writable: true,
});
/** /**
* An array of queued events before this WebSocketManager became ready * An array of queued events before this WebSocketManager became ready
@@ -118,7 +127,10 @@ class WebSocketManager extends EventEmitter {
* @private * @private
*/ */
debug(message, shard) { debug(message, shard) {
this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`); this.client.emit(
Events.DEBUG,
`[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`,
);
} }
/** /**
@@ -129,7 +141,7 @@ class WebSocketManager extends EventEmitter {
let gatewayURL = 'wss://gateway.discord.gg'; let gatewayURL = 'wss://gateway.discord.gg';
await this.client.api.gateway await this.client.api.gateway
.get({ auth: false }) .get({ auth: false })
.then(r => (gatewayURL = r.url)) .then((r) => (gatewayURL = r.url))
.catch(() => {}); .catch(() => {});
const total = Infinity; const total = Infinity;
@@ -149,14 +161,19 @@ class WebSocketManager extends EventEmitter {
let { shards } = this.client.options; let { shards } = this.client.options;
if (shards === 'auto') { if (shards === 'auto') {
this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); this.debug(
`Using the recommended shard count provided by Discord: ${recommendedShards}`,
);
this.totalShards = this.client.options.shardCount = recommendedShards; this.totalShards = this.client.options.shardCount = recommendedShards;
shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); shards = this.client.options.shards = Array.from(
{ length: recommendedShards },
(_, i) => i,
);
} }
this.totalShards = shards.length; this.totalShards = shards.length;
this.debug(`Spawning shards: ${shards.join(', ')}`); this.debug(`Spawning shards: ${shards.join(', ')}`);
this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); this.shardQueue = new Set(shards.map((id) => new WebSocketShard(this, id)));
return this.createShards(); return this.createShards();
} }
@@ -175,7 +192,7 @@ class WebSocketManager extends EventEmitter {
this.shardQueue.delete(shard); this.shardQueue.delete(shard);
if (!shard.eventsAttached) { if (!shard.eventsAttached) {
shard.on(ShardEvents.ALL_READY, unavailableGuilds => { shard.on(ShardEvents.ALL_READY, (unavailableGuilds) => {
/** /**
* Emitted when a shard turns ready. * Emitted when a shard turns ready.
* @event Client#shardReady * @event Client#shardReady
@@ -188,8 +205,12 @@ class WebSocketManager extends EventEmitter {
this.checkShardsReady(); this.checkShardsReady();
}); });
shard.on(ShardEvents.CLOSE, event => { shard.on(ShardEvents.CLOSE, (event) => {
if (event.code === 1_000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { 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. * Emitted when a shard's WebSocket disconnects and will no longer reconnect.
* @event Client#shardDisconnect * @event Client#shardDisconnect
@@ -215,7 +236,11 @@ class WebSocketManager extends EventEmitter {
this.shardQueue.add(shard); this.shardQueue.add(shard);
if (shard.sessionId) this.debug(`Session id is present, attempting an immediate reconnect...`, shard); if (shard.sessionId)
this.debug(
`Session id is present, attempting an immediate reconnect...`,
shard,
);
this.reconnect(); this.reconnect();
}); });
@@ -224,7 +249,10 @@ class WebSocketManager extends EventEmitter {
}); });
shard.on(ShardEvents.DESTROYED, () => { shard.on(ShardEvents.DESTROYED, () => {
this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); this.debug(
'Shard was destroyed but no WebSocket connection was present! Reconnecting...',
shard,
);
this.client.emit(Events.SHARD_RECONNECTING, shard.id); this.client.emit(Events.SHARD_RECONNECTING, shard.id);
@@ -252,7 +280,9 @@ class WebSocketManager extends EventEmitter {
} }
// If we have more shards, add a 5s delay // If we have more shards, add a 5s delay
if (this.shardQueue.size) { if (this.shardQueue.size) {
this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`); this.debug(
`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`,
);
await sleep(5_000); await sleep(5_000);
return this.createShards(); return this.createShards();
} }
@@ -271,7 +301,9 @@ class WebSocketManager extends EventEmitter {
try { try {
await this.createShards(); await this.createShards();
} catch (error) { } catch (error) {
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); this.debug(
`Couldn't reconnect or fetch information about the gateway. ${error}`,
);
if (error.httpStatus !== 401) { if (error.httpStatus !== 401) {
this.debug(`Possible network error occurred. Retrying in 5s...`); this.debug(`Possible network error occurred. Retrying in 5s...`);
await sleep(5_000); await sleep(5_000);
@@ -313,10 +345,13 @@ class WebSocketManager extends EventEmitter {
*/ */
destroy() { destroy() {
if (this.destroyed) return; if (this.destroyed) return;
this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); this.debug(
`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`,
);
this.destroyed = true; this.destroyed = true;
this.shardQueue.clear(); this.shardQueue.clear();
for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false }); for (const shard of this.shards.values())
shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false });
} }
/** /**
@@ -362,7 +397,10 @@ class WebSocketManager extends EventEmitter {
*/ */
checkShardsReady() { checkShardsReady() {
if (this.status === Status.READY) return; if (this.status === Status.READY) return;
if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) { if (
this.shards.size !== this.totalShards ||
this.shards.some((s) => s.status !== Status.READY)
) {
return; return;
} }

View File

@@ -3,7 +3,14 @@
const EventEmitter = require('node:events'); const EventEmitter = require('node:events');
const { setTimeout, setInterval, clearTimeout } = require('node:timers'); const { setTimeout, setInterval, clearTimeout } = require('node:timers');
const WebSocket = require('../../WebSocket'); const WebSocket = require('../../WebSocket');
const { Status, Events, ShardEvents, Opcodes, WSEvents, WSCodes } = require('../../util/Constants'); const {
Status,
Events,
ShardEvents,
Opcodes,
WSEvents,
WSCodes,
} = require('../../util/Constants');
const Intents = require('../../util/Intents'); const Intents = require('../../util/Intents');
const Util = require('../../util/Util'); const Util = require('../../util/Util');
@@ -140,7 +147,10 @@ class WebSocketShard extends EventEmitter {
* @type {?NodeJS.Timeout} * @type {?NodeJS.Timeout}
* @private * @private
*/ */
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true }); Object.defineProperty(this, 'helloTimeout', {
value: null,
writable: true,
});
/** /**
* The WebSocket timeout. * The WebSocket timeout.
@@ -148,7 +158,10 @@ class WebSocketShard extends EventEmitter {
* @type {?NodeJS.Timeout} * @type {?NodeJS.Timeout}
* @private * @private
*/ */
Object.defineProperty(this, 'wsCloseTimeout', { value: null, writable: true }); Object.defineProperty(this, 'wsCloseTimeout', {
value: null,
writable: true,
});
/** /**
* If the manager attached its event handlers on the shard * If the manager attached its event handlers on the shard
@@ -156,7 +169,10 @@ class WebSocketShard extends EventEmitter {
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
Object.defineProperty(this, 'eventsAttached', { value: false, writable: true }); Object.defineProperty(this, 'eventsAttached', {
value: false,
writable: true,
});
/** /**
* A set of guild ids this shard expects to receive * A set of guild ids this shard expects to receive
@@ -164,7 +180,10 @@ class WebSocketShard extends EventEmitter {
* @type {?Set<string>} * @type {?Set<string>}
* @private * @private
*/ */
Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true }); Object.defineProperty(this, 'expectedGuilds', {
value: null,
writable: true,
});
/** /**
* The ready timeout * The ready timeout
@@ -172,7 +191,10 @@ class WebSocketShard extends EventEmitter {
* @type {?NodeJS.Timeout} * @type {?NodeJS.Timeout}
* @private * @private
*/ */
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); Object.defineProperty(this, 'readyTimeout', {
value: null,
writable: true,
});
/** /**
* Time when the WebSocket connection was opened * Time when the WebSocket connection was opened
@@ -201,7 +223,10 @@ class WebSocketShard extends EventEmitter {
connect() { connect() {
const { client } = this.manager; const { client } = this.manager;
if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.READY) { if (
this.connection?.readyState === WebSocket.OPEN &&
this.status === Status.READY
) {
return Promise.resolve(); return Promise.resolve();
} }
@@ -226,7 +251,7 @@ class WebSocketShard extends EventEmitter {
resolve(); resolve();
}; };
const onClose = event => { const onClose = (event) => {
cleanup(); cleanup();
reject(event); reject(event);
}; };
@@ -244,7 +269,9 @@ class WebSocketShard extends EventEmitter {
this.once(ShardEvents.DESTROYED, onInvalidOrDestroyed); this.once(ShardEvents.DESTROYED, onInvalidOrDestroyed);
if (this.connection?.readyState === WebSocket.OPEN) { if (this.connection?.readyState === WebSocket.OPEN) {
this.debug('An open connection was found, attempting an immediate identify.'); this.debug(
'An open connection was found, attempting an immediate identify.',
);
this.identify(); this.identify();
return; return;
} }
@@ -275,7 +302,10 @@ class WebSocketShard extends EventEmitter {
Agent : ${Util.verifyProxyAgent(client.options.ws.agent)}`, Agent : ${Util.verifyProxyAgent(client.options.ws.agent)}`,
); );
this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; this.status =
this.status === Status.DISCONNECTED
? Status.RECONNECTING
: Status.CONNECTING;
this.setHelloTimeout(); this.setHelloTimeout();
this.setWsCloseTimeout(-1); this.setWsCloseTimeout(-1);
this.connectedAt = Date.now(); this.connectedAt = Date.now();
@@ -283,7 +313,9 @@ class WebSocketShard extends EventEmitter {
// Adding a handshake timeout to just make sure no zombie connection appears. // Adding a handshake timeout to just make sure no zombie connection appears.
const ws = (this.connection = WebSocket.create(gateway, wsQuery, { const ws = (this.connection = WebSocket.create(gateway, wsQuery, {
handshakeTimeout: 30_000, handshakeTimeout: 30_000,
agent: Util.verifyProxyAgent(client.options.ws.agent) ? client.options.ws.agent : undefined, agent: Util.verifyProxyAgent(client.options.ws.agent)
? client.options.ws.agent
: undefined,
})); }));
ws.onopen = this.onOpen.bind(this); ws.onopen = this.onOpen.bind(this);
ws.onmessage = this.onMessage.bind(this); ws.onmessage = this.onMessage.bind(this);
@@ -312,7 +344,11 @@ class WebSocketShard extends EventEmitter {
if (zlib) { if (zlib) {
const l = data.length; const l = data.length;
const flush = const flush =
l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff; 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); this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
if (!flush) return; if (!flush) return;
@@ -328,7 +364,8 @@ class WebSocketShard extends EventEmitter {
return; return;
} }
this.manager.client.emit(Events.RAW, packet, this.id); this.manager.client.emit(Events.RAW, packet, this.id);
if (packet.op === Opcodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id); if (packet.op === Opcodes.DISPATCH)
this.manager.emit(packet.t, packet.d, this.id);
this.onPacket(packet); this.onPacket(packet);
} }
@@ -434,9 +471,15 @@ class WebSocketShard extends EventEmitter {
this.resumeURL = packet.d.resume_gateway_url; this.resumeURL = packet.d.resume_gateway_url;
this.sessionId = packet.d.session_id; this.sessionId = packet.d.session_id;
this.expectedGuilds = new Set(packet.d.guilds.filter(d => d?.unavailable == true).map(d => d.id)); this.expectedGuilds = new Set(
packet.d.guilds
.filter((d) => d?.unavailable == true)
.map((d) => d.id),
);
this.status = Status.WAITING_FOR_GUILDS; this.status = Status.WAITING_FOR_GUILDS;
this.debug(`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`); this.debug(
`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`,
);
this.lastHeartbeatAcked = true; this.lastHeartbeatAcked = true;
this.sendHeartbeat('ReadyHeartbeat'); this.sendHeartbeat('ReadyHeartbeat');
break; break;
@@ -449,7 +492,9 @@ class WebSocketShard extends EventEmitter {
this.status = Status.READY; this.status = Status.READY;
const replayed = packet.s - this.closeSequence; const replayed = packet.s - this.closeSequence;
this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`); this.debug(
`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`,
);
this.lastHeartbeatAcked = true; this.lastHeartbeatAcked = true;
this.sendHeartbeat('ResumeHeartbeat'); this.sendHeartbeat('ResumeHeartbeat');
break; break;
@@ -496,7 +541,10 @@ class WebSocketShard extends EventEmitter {
break; break;
default: default:
this.manager.handlePacket(packet, this); this.manager.handlePacket(packet, this);
if (this.status === Status.WAITING_FOR_GUILDS && packet.t === WSEvents.GUILD_CREATE) { if (
this.status === Status.WAITING_FOR_GUILDS &&
packet.t === WSEvents.GUILD_CREATE
) {
this.expectedGuilds.delete(packet.d.id); this.expectedGuilds.delete(packet.d.id);
this.checkReady(); this.checkReady();
} }
@@ -529,7 +577,9 @@ class WebSocketShard extends EventEmitter {
this.emit(ShardEvents.ALL_READY); this.emit(ShardEvents.ALL_READY);
return; return;
} }
const hasGuildsIntent = new Intents(this.manager.client.options.intents).has(Intents.FLAGS.GUILDS); 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 // 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 // * The timeout is 15 seconds by default
// * This can be optionally changed in the client options via the `waitGuildTimeout` option // * This can be optionally changed in the client options via the `waitGuildTimeout` option
@@ -572,7 +622,9 @@ class WebSocketShard extends EventEmitter {
} }
this.debug('Setting a HELLO timeout for 20s.'); this.debug('Setting a HELLO timeout for 20s.');
this.helloTimeout = setTimeout(() => { this.helloTimeout = setTimeout(() => {
this.debug('Did not receive HELLO in time. Destroying and connecting again.'); this.debug(
'Did not receive HELLO in time. Destroying and connecting again.',
);
this.destroy({ reset: true, closeCode: 4009 }); this.destroy({ reset: true, closeCode: 4009 });
}, 20_000).unref(); }, 20_000).unref();
} }
@@ -597,7 +649,9 @@ class WebSocketShard extends EventEmitter {
// Check if close event was emitted. // Check if close event was emitted.
if (this.closeEmitted) { if (this.closeEmitted) {
this.debug(`[WebSocket] close was already emitted, assuming the connection was closed properly.`); this.debug(
`[WebSocket] close was already emitted, assuming the connection was closed properly.`,
);
// Setting the variable false to check for zombie connections. // Setting the variable false to check for zombie connections.
this.closeEmitted = false; this.closeEmitted = false;
return; return;
@@ -635,7 +689,10 @@ class WebSocketShard extends EventEmitter {
this.debug(`Setting a heartbeat interval for ${time}ms.`); this.debug(`Setting a heartbeat interval for ${time}ms.`);
// Sanity checks // Sanity checks
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval); if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), time).unref(); this.heartbeatInterval = setInterval(
() => this.sendHeartbeat(),
time,
).unref();
} }
/** /**
@@ -647,10 +704,16 @@ class WebSocketShard extends EventEmitter {
*/ */
sendHeartbeat( sendHeartbeat(
tag = 'HeartbeatTimer', tag = 'HeartbeatTimer',
ignoreHeartbeatAck = [Status.WAITING_FOR_GUILDS, Status.IDENTIFYING, Status.RESUMING].includes(this.status), ignoreHeartbeatAck = [
Status.WAITING_FOR_GUILDS,
Status.IDENTIFYING,
Status.RESUMING,
].includes(this.status),
) { ) {
if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) { if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) {
this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`); this.debug(
`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`,
);
} else if (!this.lastHeartbeatAcked) { } else if (!this.lastHeartbeatAcked) {
this.debug( this.debug(
`[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting.
@@ -703,9 +766,10 @@ class WebSocketShard extends EventEmitter {
// Patch something // Patch something
Object.keys(client.options.ws.properties) Object.keys(client.options.ws.properties)
.filter(k => k.startsWith('$')) .filter((k) => k.startsWith('$'))
.forEach(k => { .forEach((k) => {
client.options.ws.properties[k.slice(1)] = client.options.ws.properties[k]; client.options.ws.properties[k.slice(1)] =
client.options.ws.properties[k];
delete client.options.ws.properties[k]; delete client.options.ws.properties[k];
}); });
@@ -728,14 +792,18 @@ class WebSocketShard extends EventEmitter {
*/ */
identifyResume() { identifyResume() {
if (!this.sessionId) { if (!this.sessionId) {
this.debug('[RESUME] No session id was present; identifying as a new session.'); this.debug(
'[RESUME] No session id was present; identifying as a new session.',
);
this.identifyNew(); this.identifyNew();
return; return;
} }
this.status = Status.RESUMING; this.status = Status.RESUMING;
this.debug(`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`); this.debug(
`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`,
);
const d = { const d = {
token: this.manager.client.token, token: this.manager.client.token,
@@ -767,13 +835,15 @@ class WebSocketShard extends EventEmitter {
*/ */
_send(data) { _send(data) {
if (this.connection?.readyState !== WebSocket.OPEN) { if (this.connection?.readyState !== WebSocket.OPEN) {
this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`); this.debug(
`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`,
);
this.destroy({ closeCode: 4_000 }); this.destroy({ closeCode: 4_000 });
return; return;
} }
this.debug(`[WebSocketShard] send packet '${JSON.stringify(data)}'`); this.debug(`[WebSocketShard] send packet '${JSON.stringify(data)}'`);
this.connection.send(WebSocket.pack(data), err => { this.connection.send(WebSocket.pack(data), (err) => {
if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id); if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id);
}); });
} }
@@ -827,7 +897,9 @@ class WebSocketShard extends EventEmitter {
// If the connection is currently opened, we will (hopefully) receive close // If the connection is currently opened, we will (hopefully) receive close
if (this.connection.readyState === WebSocket.OPEN) { if (this.connection.readyState === WebSocket.OPEN) {
this.connection.close(closeCode); this.connection.close(closeCode);
this.debug(`[WebSocket] Close: Tried closing. | WS State: ${CONNECTION_STATE[this.connection.readyState]}`); this.debug(
`[WebSocket] Close: Tried closing. | WS State: ${CONNECTION_STATE[this.connection.readyState]}`,
);
} else { } else {
// Connection is not OPEN // Connection is not OPEN
this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`); this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
@@ -886,7 +958,10 @@ class WebSocketShard extends EventEmitter {
* @private * @private
*/ */
_cleanupConnection() { _cleanupConnection() {
this.connection.onopen = this.connection.onclose = this.connection.onmessage = null; this.connection.onopen =
this.connection.onclose =
this.connection.onmessage =
null;
this.connection.onerror = () => null; this.connection.onerror = () => null;
} }

View File

@@ -3,10 +3,15 @@
const { Events } = require('../../../util/Constants'); const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => { module.exports = (client, { d: data }) => {
const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands; const commandManager = data.guild_id
? client.guilds.cache.get(data.guild_id)?.commands
: client.application.commands;
if (!commandManager) return; if (!commandManager) return;
const command = commandManager._add(data, data.application_id === client.application.id); const command = commandManager._add(
data,
data.application_id === client.application.id,
);
/** /**
* Emitted when a guild application command is created. * Emitted when a guild application command is created.

View File

@@ -3,7 +3,9 @@
const { Events } = require('../../../util/Constants'); const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => { module.exports = (client, { d: data }) => {
const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands; const commandManager = data.guild_id
? client.guilds.cache.get(data.guild_id)?.commands
: client.application.commands;
if (!commandManager) return; if (!commandManager) return;
const isOwn = data.application_id === client.application.id; const isOwn = data.application_id === client.application.id;

View File

@@ -3,11 +3,16 @@
const { Events } = require('../../../util/Constants'); const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => { module.exports = (client, { d: data }) => {
const commandManager = data.guild_id ? client.guilds.cache.get(data.guild_id)?.commands : client.application.commands; const commandManager = data.guild_id
? client.guilds.cache.get(data.guild_id)?.commands
: client.application.commands;
if (!commandManager) return; if (!commandManager) return;
const oldCommand = commandManager.cache.get(data.id)?._clone() ?? null; const oldCommand = commandManager.cache.get(data.id)?._clone() ?? null;
const newCommand = commandManager._add(data, data.application_id === client.application.id); const newCommand = commandManager._add(
data,
data.application_id === client.application.id,
);
/** /**
* Emitted when a guild application command is updated. * Emitted when a guild application command is updated.

View File

@@ -4,7 +4,9 @@ const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => { module.exports = (client, { d: data }) => {
const channel = client.channels.cache.get(data.channel_id); const channel = client.channels.cache.get(data.channel_id);
const time = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; const time = data.last_pin_timestamp
? new Date(data.last_pin_timestamp).getTime()
: null;
if (channel) { if (channel) {
// Discord sends null for last_pin_timestamp if the last pinned message was removed // Discord sends null for last_pin_timestamp if the last pinned message was removed

View File

@@ -4,13 +4,19 @@ module.exports = (client, packet) => {
const channel = client.channels.cache.get(packet.d.channel_id); const channel = client.channels.cache.get(packet.d.channel_id);
if (channel) { if (channel) {
if (!channel._recipients) channel._recipients = []; if (!channel._recipients) channel._recipients = [];
channel._recipients = channel._recipients.filter(u => u.id !== packet.d.user.id); channel._recipients = channel._recipients.filter(
(u) => u.id !== packet.d.user.id,
);
/** /**
* Emitted whenever a recipient is removed from a group DM. * Emitted whenever a recipient is removed from a group DM.
* @event Client#channelRecipientRemove * @event Client#channelRecipientRemove
* @param {GroupDMChannel} channel Group DM channel * @param {GroupDMChannel} channel Group DM channel
* @param {User} user User * @param {User} user User
*/ */
client.emit(Events.CHANNEL_RECIPIENT_REMOVE, channel, client.users._add(packet.d.user)); client.emit(
Events.CHANNEL_RECIPIENT_REMOVE,
channel,
client.users._add(packet.d.user),
);
} }
}; };

View File

@@ -8,9 +8,11 @@ module.exports = (client, { d: data }) => {
if (!guild) return; if (!guild) return;
const members = new Collection(); const members = new Collection();
for (const member of data.members) members.set(member.user.id, guild.members._add(member)); for (const member of data.members)
members.set(member.user.id, guild.members._add(member));
if (data.presences) { if (data.presences) {
for (const presence of data.presences) guild.presences._add(Object.assign(presence, { guild })); for (const presence of data.presences)
guild.presences._add(Object.assign(presence, { guild }));
} }
/** /**

View File

@@ -39,7 +39,9 @@ module.exports = (client, { d: data }, shard) => {
client.settings._patch(data.user_settings); client.settings._patch(data.user_settings);
// GuildSetting // GuildSetting
for (const gSetting of Array.isArray(data.user_guild_settings) ? data.user_guild_settings : []) { for (const gSetting of Array.isArray(data.user_guild_settings)
? data.user_guild_settings
: []) {
const guild = client.guilds.cache.get(gSetting.guild_id); const guild = client.guilds.cache.get(gSetting.guild_id);
if (guild) guild.settings._patch(gSetting); if (guild) guild.settings._patch(gSetting);
} }

View File

@@ -22,8 +22,10 @@ module.exports = (client, { d: data }) => {
const oldNickname = client.relationships.friendNicknames.get(data.id); const oldNickname = client.relationships.friendNicknames.get(data.id);
// Update // Update
if (data.type) client.relationships.cache.set(data.id, data.type); if (data.type) client.relationships.cache.set(data.id, data.type);
if (data.nickname) client.relationships.friendNicknames.set(data.id, data.nickname); if (data.nickname)
if (data.since) client.relationships.sinceCache.set(data.id, new Date(data.since || 0)); client.relationships.friendNicknames.set(data.id, data.nickname);
if (data.since)
client.relationships.sinceCache.set(data.id, new Date(data.since || 0));
client.emit( client.emit(
Events.RELATIONSHIP_UPDATE, Events.RELATIONSHIP_UPDATE,
data.id, data.id,

View File

@@ -25,7 +25,7 @@ module.exports = (client, { d: data }) => {
'[USER_REQUIRED_ACTION] Successfully accepted the new Terms of Service and Privacy Policy.', '[USER_REQUIRED_ACTION] Successfully accepted the new Terms of Service and Privacy Policy.',
); );
}) })
.catch(e => { .catch((e) => {
client.emit( client.emit(
'debug', 'debug',
`[USER_REQUIRED_ACTION] Failed to accept the new Terms of Service and Privacy Policy: ${e}`, `[USER_REQUIRED_ACTION] Failed to accept the new Terms of Service and Privacy Policy: ${e}`,

View File

@@ -12,5 +12,8 @@ module.exports = (client, { d: data }) => {
* @event Client#voiceChannelEffectSend * @event Client#voiceChannelEffectSend
* @param {VoiceChannelEffect} voiceChannelEffect The sent voice channel effect * @param {VoiceChannelEffect} voiceChannelEffect The sent voice channel effect
*/ */
client.emit(Events.VOICE_CHANNEL_EFFECT_SEND, new VoiceChannelEffect(data, guild)); client.emit(
Events.VOICE_CHANNEL_EFFECT_SEND,
new VoiceChannelEffect(data, guild),
);
}; };

View File

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

View File

@@ -6,8 +6,14 @@ const handlers = Object.fromEntries([
['APPLICATION_COMMAND_CREATE', require('./APPLICATION_COMMAND_CREATE')], ['APPLICATION_COMMAND_CREATE', require('./APPLICATION_COMMAND_CREATE')],
['APPLICATION_COMMAND_DELETE', require('./APPLICATION_COMMAND_DELETE')], ['APPLICATION_COMMAND_DELETE', require('./APPLICATION_COMMAND_DELETE')],
['APPLICATION_COMMAND_UPDATE', require('./APPLICATION_COMMAND_UPDATE')], ['APPLICATION_COMMAND_UPDATE', require('./APPLICATION_COMMAND_UPDATE')],
['APPLICATION_COMMAND_PERMISSIONS_UPDATE', require('./APPLICATION_COMMAND_PERMISSIONS_UPDATE')], [
['AUTO_MODERATION_ACTION_EXECUTION', require('./AUTO_MODERATION_ACTION_EXECUTION')], 'APPLICATION_COMMAND_PERMISSIONS_UPDATE',
require('./APPLICATION_COMMAND_PERMISSIONS_UPDATE'),
],
[
'AUTO_MODERATION_ACTION_EXECUTION',
require('./AUTO_MODERATION_ACTION_EXECUTION'),
],
['AUTO_MODERATION_RULE_CREATE', require('./AUTO_MODERATION_RULE_CREATE')], ['AUTO_MODERATION_RULE_CREATE', require('./AUTO_MODERATION_RULE_CREATE')],
['AUTO_MODERATION_RULE_DELETE', require('./AUTO_MODERATION_RULE_DELETE')], ['AUTO_MODERATION_RULE_DELETE', require('./AUTO_MODERATION_RULE_DELETE')],
['AUTO_MODERATION_RULE_UPDATE', require('./AUTO_MODERATION_RULE_UPDATE')], ['AUTO_MODERATION_RULE_UPDATE', require('./AUTO_MODERATION_RULE_UPDATE')],
@@ -59,8 +65,14 @@ const handlers = Object.fromEntries([
['GUILD_SCHEDULED_EVENT_CREATE', require('./GUILD_SCHEDULED_EVENT_CREATE')], ['GUILD_SCHEDULED_EVENT_CREATE', require('./GUILD_SCHEDULED_EVENT_CREATE')],
['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE')], ['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE')],
['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')], ['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')],
['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')], [
['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')], 'GUILD_SCHEDULED_EVENT_USER_ADD',
require('./GUILD_SCHEDULED_EVENT_USER_ADD'),
],
[
'GUILD_SCHEDULED_EVENT_USER_REMOVE',
require('./GUILD_SCHEDULED_EVENT_USER_REMOVE'),
],
['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')], ['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')],
// Selfbot // Selfbot
['RELATIONSHIP_ADD', require('./RELATIONSHIP_ADD')], ['RELATIONSHIP_ADD', require('./RELATIONSHIP_ADD')],

View File

@@ -15,7 +15,8 @@ function makeDiscordjsError(Base) {
constructor(key, ...args) { constructor(key, ...args) {
super(message(key, args)); super(message(key, args));
this[kCode] = key; this[kCode] = key;
if (Error.captureStackTrace) Error.captureStackTrace(this, DiscordjsError); if (Error.captureStackTrace)
Error.captureStackTrace(this, DiscordjsError);
} }
get name() { get name() {
@@ -35,7 +36,8 @@ function makeDiscordjsError(Base) {
* @returns {string} Formatted string * @returns {string} Formatted string
*/ */
function message(key, args) { function message(key, args) {
if (typeof key !== 'string') throw new Error('Error message key must be a string'); if (typeof key !== 'string')
throw new Error('Error message key must be a string');
const msg = messages.get(key); const msg = messages.get(key);
if (!msg) throw new Error(`An invalid error message key was used: ${key}.`); if (!msg) throw new Error(`An invalid error message key was used: ${key}.`);
if (typeof msg === 'function') return msg(...args); if (typeof msg === 'function') return msg(...args);

View File

@@ -6,41 +6,51 @@ const Messages = {
CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`, CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`,
CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.', CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.',
CLIENT_MISSING_INTENTS: 'Valid intents must be provided for the Client.', CLIENT_MISSING_INTENTS: 'Valid intents must be provided for the Client.',
CLIENT_NOT_READY: action => `The client needs to be logged in to ${action}.`, CLIENT_NOT_READY: (action) =>
`The client needs to be logged in to ${action}.`,
TOKEN_INVALID: 'An invalid token was provided.', TOKEN_INVALID: 'An invalid token was provided.',
TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', TOKEN_MISSING:
TOTPKEY_MISSING: 'Request to use mfa, but TOTPKey was not set in client options.', 'Request to use token, but token was unavailable to the client.',
TOTPKEY_MISSING:
'Request to use mfa, but TOTPKey was not set in client options.',
WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.',
WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.',
WS_NOT_OPEN: (data = 'data') => `WebSocket not open to send ${data}`, WS_NOT_OPEN: (data = 'data') => `WebSocket not open to send ${data}`,
MANAGER_DESTROYED: 'Manager was destroyed.', MANAGER_DESTROYED: 'Manager was destroyed.',
BITFIELD_INVALID: bit => `Invalid bitfield flag or number: ${bit}.`, BITFIELD_INVALID: (bit) => `Invalid bitfield flag or number: ${bit}.`,
SHARDING_INVALID: '[Bot Token] Invalid shard settings were provided.', SHARDING_INVALID: '[Bot Token] Invalid shard settings were provided.',
SHARDING_REQUIRED: '[Bot Token] This session would have handled too many guilds - Sharding is required.', SHARDING_REQUIRED:
'[Bot Token] This session would have handled too many guilds - Sharding is required.',
INVALID_INTENTS: '[Bot Token] Invalid intent provided for WebSocket intents.', INVALID_INTENTS: '[Bot Token] Invalid intent provided for WebSocket intents.',
DISALLOWED_INTENTS: '[Bot Token] Privileged intent provided is not enabled or whitelisted.', DISALLOWED_INTENTS:
'[Bot Token] Privileged intent provided is not enabled or whitelisted.',
SHARDING_NO_SHARDS: 'No shards have been spawned.', SHARDING_NO_SHARDS: 'No shards have been spawned.',
SHARDING_IN_PROCESS: 'Shards are still being spawned.', SHARDING_IN_PROCESS: 'Shards are still being spawned.',
SHARDING_INVALID_EVAL_BROADCAST: 'Script to evaluate must be a function', SHARDING_INVALID_EVAL_BROADCAST: 'Script to evaluate must be a function',
SHARDING_SHARD_NOT_FOUND: id => `Shard ${id} could not be found.`, SHARDING_SHARD_NOT_FOUND: (id) => `Shard ${id} could not be found.`,
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`, SHARDING_ALREADY_SPAWNED: (count) => `Already spawned ${count} shards.`,
SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`, SHARDING_PROCESS_EXISTS: (id) => `Shard ${id} already has an active process.`,
SHARDING_WORKER_EXISTS: id => `Shard ${id} already has an active worker.`, SHARDING_WORKER_EXISTS: (id) => `Shard ${id} already has an active worker.`,
SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`, SHARDING_READY_TIMEOUT: (id) =>
SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`, `Shard ${id}'s Client took too long to become ready.`,
SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`, SHARDING_READY_DISCONNECTED: (id) =>
SHARDING_NO_CHILD_EXISTS: id => `Shard ${id} has no active process or worker.`, `Shard ${id}'s Client disconnected before becoming ready.`,
SHARDING_READY_DIED: (id) =>
`Shard ${id}'s process exited before its Client became ready.`,
SHARDING_NO_CHILD_EXISTS: (id) =>
`Shard ${id} has no active process or worker.`,
SHARDING_SHARD_MISCALCULATION: (shard, guild, count) => SHARDING_SHARD_MISCALCULATION: (shard, guild, count) =>
`Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`, `Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`,
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
COLOR_CONVERT: 'Unable to convert color to a number.', COLOR_CONVERT: 'Unable to convert color to a number.',
INVITE_OPTIONS_MISSING_CHANNEL: 'A valid guild channel must be provided when GuildScheduledEvent is EXTERNAL.', INVITE_OPTIONS_MISSING_CHANNEL:
'A valid guild channel must be provided when GuildScheduledEvent is EXTERNAL.',
EMBED_TITLE: 'MessageEmbed title must be a string.', EMBED_TITLE: 'MessageEmbed title must be a string.',
EMBED_FIELD_NAME: 'MessageEmbed field names must be non-empty strings.', EMBED_FIELD_NAME: 'MessageEmbed field names must be non-empty strings.',
@@ -67,66 +77,82 @@ const Messages = {
MODAL_CUSTOM_ID: 'Modal customId must be a string', MODAL_CUSTOM_ID: 'Modal customId must be a string',
MODAL_TITLE: 'Modal title must be a string', MODAL_TITLE: 'Modal title must be a string',
INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`, INTERACTION_COLLECTOR_ERROR: (reason) =>
`Collector received no interactions before ending with reason: ${reason}`,
FILE_NOT_FOUND: file => `File could not be found: ${file}`, FILE_NOT_FOUND: (file) => `File could not be found: ${file}`,
USER_BANNER_NOT_FETCHED: "You must fetch this user's banner before trying to generate its URL!", USER_BANNER_NOT_FETCHED:
"You must fetch this user's banner before trying to generate its URL!",
USER_NO_DM_CHANNEL: 'No DM Channel exists!', USER_NO_DM_CHANNEL: 'No DM Channel exists!',
VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.', VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.',
VOICE_STATE_NOT_OWN: VOICE_STATE_NOT_OWN:
'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.', 'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.',
VOICE_STATE_INVALID_TYPE: name => `${name} must be a boolean.`, VOICE_STATE_INVALID_TYPE: (name) => `${name} must be a boolean.`,
REQ_RESOURCE_TYPE: 'The resource must be a string, Buffer or a valid file stream.', REQ_RESOURCE_TYPE:
'The resource must be a string, Buffer or a valid file stream.',
IMAGE_FORMAT: format => `Invalid image format: ${format}`, IMAGE_FORMAT: (format) => `Invalid image format: ${format}`,
IMAGE_SIZE: size => `Invalid image size: ${size}`, IMAGE_SIZE: (size) => `Invalid image size: ${size}`,
MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.', MESSAGE_BULK_DELETE_TYPE:
'The messages must be an Array, Collection, or number.',
MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.', MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.',
MESSAGE_CONTENT_TYPE: 'Message content must be a non-empty string.', MESSAGE_CONTENT_TYPE: 'Message content must be a non-empty string.',
SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', SPLIT_MAX_LEN:
'Chunk exceeds the max length and contains no split characters.',
BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user id to ${ban ? 'ban' : 'unban'}.`, BAN_RESOLVE_ID: (ban = false) =>
`Couldn't resolve the user id to ${ban ? 'ban' : 'unban'}.`,
FETCH_BAN_RESOLVE_ID: "Couldn't resolve the user id to fetch the ban.", FETCH_BAN_RESOLVE_ID: "Couldn't resolve the user id to fetch the ban.",
PRUNE_DAYS_TYPE: 'Days must be a number', PRUNE_DAYS_TYPE: 'Days must be a number',
GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.',
GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_VOICE_CHANNEL_RESOLVE:
'Could not resolve channel to a guild voice channel.',
GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.',
GUILD_CHANNEL_UNOWNED: "The fetched channel does not belong to this manager's guild.", GUILD_CHANNEL_UNOWNED:
"The fetched channel does not belong to this manager's guild.",
GUILD_OWNED: 'Guild is owned by the client.', GUILD_OWNED: 'Guild is owned by the client.',
GUILD_MEMBERS_TIMEOUT: "Members didn't arrive in time.", GUILD_MEMBERS_TIMEOUT: "Members didn't arrive in time.",
GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.', GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.',
CHANNEL_NOT_CACHED: 'Could not find the channel where this message came from in the cache!', CHANNEL_NOT_CACHED:
'Could not find the channel where this message came from in the cache!',
STAGE_CHANNEL_RESOLVE: 'Could not resolve channel to a stage channel.', STAGE_CHANNEL_RESOLVE: 'Could not resolve channel to a stage channel.',
GUILD_SCHEDULED_EVENT_RESOLVE: 'Could not resolve the guild scheduled event.', GUILD_SCHEDULED_EVENT_RESOLVE: 'Could not resolve the guild scheduled event.',
INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, INVALID_TYPE: (name, expected, an = false) =>
INVALID_ELEMENT: (type, name, elem) => `Supplied ${type} ${name} includes an invalid element: ${elem}`, `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`,
INVALID_ELEMENT: (type, name, elem) =>
`Supplied ${type} ${name} includes an invalid element: ${elem}`,
MESSAGE_THREAD_PARENT: 'The message was not sent in a guild text or news channel', MESSAGE_THREAD_PARENT:
'The message was not sent in a guild text or news channel',
MESSAGE_EXISTING_THREAD: 'The message already has a thread', MESSAGE_EXISTING_THREAD: 'The message already has a thread',
THREAD_INVITABLE_TYPE: type => `Invitable cannot be edited on ${type}`, THREAD_INVITABLE_TYPE: (type) => `Invitable cannot be edited on ${type}`,
WEBHOOK_MESSAGE: 'The message was not sent by a webhook.', WEBHOOK_MESSAGE: 'The message was not sent by a webhook.',
WEBHOOK_TOKEN_UNAVAILABLE: 'This action requires a webhook token, but none is available.', WEBHOOK_TOKEN_UNAVAILABLE:
'This action requires a webhook token, but none is available.',
WEBHOOK_URL_INVALID: 'The provided webhook URL is not valid.', WEBHOOK_URL_INVALID: 'The provided webhook URL is not valid.',
WEBHOOK_APPLICATION: 'This message webhook belongs to an application and cannot be fetched.', WEBHOOK_APPLICATION:
'This message webhook belongs to an application and cannot be fetched.',
MESSAGE_REFERENCE_MISSING: 'The message does not reference another message', MESSAGE_REFERENCE_MISSING: 'The message does not reference another message',
EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji', EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji',
EMOJI_MANAGED: 'Emoji is managed and has no Author.', EMOJI_MANAGED: 'Emoji is managed and has no Author.',
MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION: guild => MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION: (guild) =>
`Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`, `Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`,
NOT_GUILD_STICKER: 'Sticker is a standard (non-guild) sticker and has no author.', NOT_GUILD_STICKER:
'Sticker is a standard (non-guild) sticker and has no author.',
REACTION_RESOLVE_USER: "Couldn't resolve the user id to remove from the reaction.", REACTION_RESOLVE_USER:
"Couldn't resolve the user id to remove from the reaction.",
VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.',
@@ -134,55 +160,73 @@ const Messages = {
INVITE_NOT_FOUND: 'Could not find the requested invite.', INVITE_NOT_FOUND: 'Could not find the requested invite.',
DELETE_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot delete them", DELETE_GROUP_DM_CHANNEL:
FETCH_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot fetch them", "Bots don't have access to Group DM Channels and cannot delete them",
FETCH_GROUP_DM_CHANNEL:
"Bots don't have access to Group DM Channels and cannot fetch them",
MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.', MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.',
GLOBAL_COMMAND_PERMISSIONS: GLOBAL_COMMAND_PERMISSIONS:
'Permissions for global commands may only be fetched or modified by providing a GuildResolvable ' + 'Permissions for global commands may only be fetched or modified by providing a GuildResolvable ' +
"or from a guild's application command manager.", "or from a guild's application command manager.",
GUILD_UNCACHED_ROLE_RESOLVE: 'Cannot resolve roles from an arbitrary guild, provide an id instead', GUILD_UNCACHED_ROLE_RESOLVE:
'Cannot resolve roles from an arbitrary guild, provide an id instead',
INTERACTION_ALREADY_REPLIED: 'The reply to this interaction has already been sent or deferred.', INTERACTION_ALREADY_REPLIED:
INTERACTION_NOT_REPLIED: 'The reply to this interaction has not been sent or deferred.', 'The reply to this interaction has already been sent or deferred.',
INTERACTION_NOT_REPLIED:
'The reply to this interaction has not been sent or deferred.',
COMMAND_INTERACTION_OPTION_NOT_FOUND: name => `Required option "${name}" not found.`, COMMAND_INTERACTION_OPTION_NOT_FOUND: (name) =>
`Required option "${name}" not found.`,
COMMAND_INTERACTION_OPTION_TYPE: (name, type, expected) => COMMAND_INTERACTION_OPTION_TYPE: (name, type, expected) =>
`Option "${name}" is of type: ${type}; expected ${expected}.`, `Option "${name}" is of type: ${type}; expected ${expected}.`,
COMMAND_INTERACTION_OPTION_EMPTY: (name, type) => COMMAND_INTERACTION_OPTION_EMPTY: (name, type) =>
`Required option "${name}" is of type: ${type}; expected a non-empty value.`, `Required option "${name}" is of type: ${type}; expected a non-empty value.`,
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND: 'No subcommand specified for interaction.', COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND:
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.', 'No subcommand specified for interaction.',
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.', COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP:
'No subcommand group specified for interaction.',
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION:
'No focused option for autocomplete interaction.',
MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`, MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: (customId) =>
`Required field with custom id "${customId}" not found.`,
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) => MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`, `Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite', INVITE_MISSING_SCOPES:
'At least one valid scope must be provided for the invite',
NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,
SWEEP_FILTER_RETURN: 'The return value of the sweepFilter function was not false or a Function', SWEEP_FILTER_RETURN:
'The return value of the sweepFilter function was not false or a Function',
GUILD_FORUM_MESSAGE_REQUIRED: 'You must provide a message to create a guild forum thread', GUILD_FORUM_MESSAGE_REQUIRED:
'You must provide a message to create a guild forum thread',
// Selfbot // Selfbot
INVALID_USER_API: 'User accounts cannot use this endpoint', INVALID_USER_API: 'User accounts cannot use this endpoint',
INVALID_APPLICATION_COMMAND: id => `Could not find a valid command for this bot: ${id}`, INVALID_APPLICATION_COMMAND: (id) =>
INVALID_COMMAND_NAME: allCMD => `Could not parse subGroupCommand and subCommand due to too long: ${allCMD.join(' ')}`, `Could not find a valid command for this bot: ${id}`,
INVALID_COMMAND_NAME: (allCMD) =>
`Could not parse subGroupCommand and subCommand due to too long: ${allCMD.join(' ')}`,
INVALID_SLASH_COMMAND_CHOICES: (parentOptions, value) => INVALID_SLASH_COMMAND_CHOICES: (parentOptions, value) =>
`${value} is not a valid choice for this option (${parentOptions})`, `${value} is not a valid choice for this option (${parentOptions})`,
SLASH_COMMAND_REQUIRED_OPTIONS_MISSING: (req, opt) => `Value required (${req}) missing (Options: ${opt})`, SLASH_COMMAND_REQUIRED_OPTIONS_MISSING: (req, opt) =>
SLASH_COMMAND_SUB_COMMAND_GROUP_INVALID: n => `${n} is not a valid sub command group`, `Value required (${req}) missing (Options: ${opt})`,
SLASH_COMMAND_SUB_COMMAND_INVALID: n => `${n} is not a valid sub command`, SLASH_COMMAND_SUB_COMMAND_GROUP_INVALID: (n) =>
`${n} is not a valid sub command group`,
SLASH_COMMAND_SUB_COMMAND_INVALID: (n) => `${n} is not a valid sub command`,
INTERACTION_FAILED: 'No responsed from Application', INTERACTION_FAILED: 'No responsed from Application',
USER_NOT_STREAMING: 'User is not streaming', USER_NOT_STREAMING: 'User is not streaming',
BULK_BAN_USERS_OPTION_EMPTY: 'Option "users" array or collection is empty', BULK_BAN_USERS_OPTION_EMPTY: 'Option "users" array or collection is empty',
// Djs v12 // Djs v12
VOICE_INVALID_HEARTBEAT: 'Tried to set voice heartbeat but no valid interval was specified.', VOICE_INVALID_HEARTBEAT:
'Tried to set voice heartbeat but no valid interval was specified.',
VOICE_USER_MISSING: "Couldn't resolve the user to create stream.", VOICE_USER_MISSING: "Couldn't resolve the user to create stream.",
VOICE_JOIN_CHANNEL: (full = false) => VOICE_JOIN_CHANNEL: (full = false) =>
`You do not have permission to join this voice channel${full ? '; it is full.' : '.'}`, `You do not have permission to join this voice channel${full ? '; it is full.' : '.'}`,
@@ -191,20 +235,26 @@ const Messages = {
VOICE_SESSION_ABSENT: 'Session ID not supplied.', VOICE_SESSION_ABSENT: 'Session ID not supplied.',
VOICE_INVALID_ENDPOINT: 'Invalid endpoint received.', VOICE_INVALID_ENDPOINT: 'Invalid endpoint received.',
VOICE_NO_BROWSER: 'Voice connections are not available in browsers.', VOICE_NO_BROWSER: 'Voice connections are not available in browsers.',
VOICE_CONNECTION_ATTEMPTS_EXCEEDED: attempts => `Too many connection attempts (${attempts}).`, VOICE_CONNECTION_ATTEMPTS_EXCEEDED: (attempts) =>
VOICE_JOIN_SOCKET_CLOSED: 'Tried to send join packet, but the WebSocket is not open.', `Too many connection attempts (${attempts}).`,
VOICE_PLAY_INTERFACE_NO_BROADCAST: 'A broadcast cannot be played in this context.', VOICE_JOIN_SOCKET_CLOSED:
'Tried to send join packet, but the WebSocket is not open.',
VOICE_PLAY_INTERFACE_NO_BROADCAST:
'A broadcast cannot be played in this context.',
VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type', VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type',
VOICE_PRISM_DEMUXERS_NEED_STREAM: 'To play a webm/ogg stream, you need to pass a ReadableStream.', VOICE_PRISM_DEMUXERS_NEED_STREAM:
'To play a webm/ogg stream, you need to pass a ReadableStream.',
VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.',
UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', UDP_SEND_FAIL:
'Tried to send a UDP packet, but there is no socket available.',
UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.', UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.',
UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.', UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.',
UDP_WRONG_HANDSHAKE: 'Wrong handshake packet for UDP', UDP_WRONG_HANDSHAKE: 'Wrong handshake packet for UDP',
INVALID_VIDEO_CODEC: codecs => `Only these codecs are supported: ${codecs.join(', ')}`, INVALID_VIDEO_CODEC: (codecs) =>
`Only these codecs are supported: ${codecs.join(', ')}`,
STREAM_CONNECTION_READONLY: 'Cannot send data to a read-only stream', STREAM_CONNECTION_READONLY: 'Cannot send data to a read-only stream',
STREAM_CANNOT_JOIN: 'Cannot join a stream to itself', STREAM_CANNOT_JOIN: 'Cannot join a stream to itself',

View File

@@ -107,7 +107,8 @@ exports.GuildEmoji = require('./structures/GuildEmoji');
exports.GuildMember = require('./structures/GuildMember').GuildMember; exports.GuildMember = require('./structures/GuildMember').GuildMember;
exports.GuildPreview = require('./structures/GuildPreview'); exports.GuildPreview = require('./structures/GuildPreview');
exports.GuildPreviewEmoji = require('./structures/GuildPreviewEmoji'); exports.GuildPreviewEmoji = require('./structures/GuildPreviewEmoji');
exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildScheduledEvent; exports.GuildScheduledEvent =
require('./structures/GuildScheduledEvent').GuildScheduledEvent;
exports.GuildTemplate = require('./structures/GuildTemplate'); exports.GuildTemplate = require('./structures/GuildTemplate');
exports.Integration = require('./structures/Integration'); exports.Integration = require('./structures/Integration');
exports.IntegrationApplication = require('./structures/IntegrationApplication'); exports.IntegrationApplication = require('./structures/IntegrationApplication');
@@ -132,7 +133,8 @@ exports.PermissionOverwrites = require('./structures/PermissionOverwrites');
exports.Presence = require('./structures/Presence').Presence; exports.Presence = require('./structures/Presence').Presence;
exports.ReactionCollector = require('./structures/ReactionCollector'); exports.ReactionCollector = require('./structures/ReactionCollector');
exports.ReactionEmoji = require('./structures/ReactionEmoji'); exports.ReactionEmoji = require('./structures/ReactionEmoji');
exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets; exports.RichPresenceAssets =
require('./structures/Presence').RichPresenceAssets;
exports.Role = require('./structures/Role').Role; exports.Role = require('./structures/Role').Role;
exports.Session = require('./structures/Session'); exports.Session = require('./structures/Session');
exports.StageChannel = require('./structures/StageChannel'); exports.StageChannel = require('./structures/StageChannel');

View File

@@ -93,7 +93,10 @@ class ApplicationCommandManager extends CachedManager {
* .then(commands => console.log(`Fetched ${commands.size} commands`)) * .then(commands => console.log(`Fetched ${commands.size} commands`))
* .catch(console.error); * .catch(console.error);
*/ */
async fetch(id, { guildId, cache = true, force = false, locale, withLocalizations } = {}) { async fetch(
id,
{ guildId, cache = true, force = false, locale, withLocalizations } = {},
) {
if (typeof id === 'object') { if (typeof id === 'object') {
({ guildId, cache = true, locale, withLocalizations } = id); ({ guildId, cache = true, locale, withLocalizations } = id);
} else if (id) { } else if (id) {
@@ -109,9 +112,16 @@ class ApplicationCommandManager extends CachedManager {
headers: { headers: {
'X-Discord-Locale': locale, 'X-Discord-Locale': locale,
}, },
query: typeof withLocalizations === 'boolean' ? { with_localizations: withLocalizations } : undefined, query:
typeof withLocalizations === 'boolean'
? { with_localizations: withLocalizations }
: undefined,
}); });
return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); return data.reduce(
(coll, command) =>
coll.set(command.id, this._add(command, cache, guildId)),
new Collection(),
);
} }
/** /**
@@ -160,9 +170,13 @@ class ApplicationCommandManager extends CachedManager {
*/ */
async set(commands, guildId) { async set(commands, guildId) {
const data = await this.commandPath({ guildId }).put({ const data = await this.commandPath({ guildId }).put({
data: commands.map(c => this.constructor.transformCommand(c)), data: commands.map((c) => this.constructor.transformCommand(c)),
}); });
return data.reduce((coll, command) => coll.set(command.id, this._add(command, true, guildId)), new Collection()); return data.reduce(
(coll, command) =>
coll.set(command.id, this._add(command, true, guildId)),
new Collection(),
);
} }
/** /**
@@ -182,7 +196,12 @@ class ApplicationCommandManager extends CachedManager {
*/ */
async edit(command, data, guildId) { async edit(command, data, guildId) {
const id = this.resolveId(command); const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); if (!id)
throw new TypeError(
'INVALID_TYPE',
'command',
'ApplicationCommandResolvable',
);
const patched = await this.commandPath({ id, guildId }).patch({ const patched = await this.commandPath({ id, guildId }).patch({
data: this.constructor.transformCommand(data), data: this.constructor.transformCommand(data),
@@ -204,7 +223,12 @@ class ApplicationCommandManager extends CachedManager {
*/ */
async delete(command, guildId) { async delete(command, guildId) {
const id = this.resolveId(command); const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); if (!id)
throw new TypeError(
'INVALID_TYPE',
'command',
'ApplicationCommandResolvable',
);
await this.commandPath({ id, guildId }).delete(); await this.commandPath({ id, guildId }).delete();
@@ -226,25 +250,37 @@ class ApplicationCommandManager extends CachedManager {
if ('default_member_permissions' in command) { if ('default_member_permissions' in command) {
default_member_permissions = command.default_member_permissions default_member_permissions = command.default_member_permissions
? new Permissions(BigInt(command.default_member_permissions)).bitfield.toString() ? new Permissions(
BigInt(command.default_member_permissions),
).bitfield.toString()
: command.default_member_permissions; : command.default_member_permissions;
} }
if ('defaultMemberPermissions' in command) { if ('defaultMemberPermissions' in command) {
default_member_permissions = default_member_permissions =
command.defaultMemberPermissions !== null command.defaultMemberPermissions !== null
? new Permissions(command.defaultMemberPermissions).bitfield.toString() ? new Permissions(
command.defaultMemberPermissions,
).bitfield.toString()
: command.defaultMemberPermissions; : command.defaultMemberPermissions;
} }
return { return {
name: command.name, name: command.name,
name_localizations: command.nameLocalizations ?? command.name_localizations, name_localizations:
command.nameLocalizations ?? command.name_localizations,
description: command.description, description: command.description,
description_localizations: command.descriptionLocalizations ?? command.description_localizations, description_localizations:
type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type], command.descriptionLocalizations ?? command.description_localizations,
options: command.options?.map(o => ApplicationCommand.transformOption(o)), type:
default_permission: command.defaultPermission ?? command.default_permission, typeof command.type === 'number'
? command.type
: ApplicationCommandTypes[command.type],
options: command.options?.map((o) =>
ApplicationCommand.transformOption(o),
),
default_permission:
command.defaultPermission ?? command.default_permission,
default_member_permissions, default_member_permissions,
dm_permission: command.dmPermission ?? command.dm_permission, dm_permission: command.dmPermission ?? command.dm_permission,
}; };

View File

@@ -3,7 +3,10 @@
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const BaseManager = require('./BaseManager'); const BaseManager = require('./BaseManager');
const { Error, TypeError } = require('../errors'); const { Error, TypeError } = require('../errors');
const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants'); const {
ApplicationCommandPermissionTypes,
APIErrors,
} = require('../util/Constants');
/** /**
* Manages API methods for permissions of Application Commands. * Manages API methods for permissions of Application Commands.
@@ -47,7 +50,10 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* @private * @private
*/ */
permissionsPath(guildId, commandId) { permissionsPath(guildId, commandId) {
return this.client.api.applications(this.client.application.id).guilds(guildId).commands(commandId).permissions; return this.client.api
.applications(this.client.application.id)
.guilds(guildId)
.commands(commandId).permissions;
} }
/** /**
@@ -96,7 +102,9 @@ class ApplicationCommandPermissionsManager extends BaseManager {
const { guildId, commandId } = this._validateOptions(guild, command); const { guildId, commandId } = this._validateOptions(guild, command);
if (commandId) { if (commandId) {
const data = await this.permissionsPath(guildId, commandId).get(); const data = await this.permissionsPath(guildId, commandId).get();
return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); return data.permissions.map((perm) =>
this.constructor.transformPermissions(perm, true),
);
} }
const data = await this.permissionsPath(guildId).get(); const data = await this.permissionsPath(guildId).get();
@@ -104,7 +112,9 @@ class ApplicationCommandPermissionsManager extends BaseManager {
(coll, perm) => (coll, perm) =>
coll.set( coll.set(
perm.id, perm.id,
perm.permissions.map(p => this.constructor.transformPermissions(p, true)), perm.permissions.map((p) =>
this.constructor.transformPermissions(p, true),
),
), ),
new Collection(), new Collection(),
); );
@@ -163,24 +173,48 @@ class ApplicationCommandPermissionsManager extends BaseManager {
if (commandId) { if (commandId) {
if (!Array.isArray(permissions)) { if (!Array.isArray(permissions)) {
throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); throw new TypeError(
'INVALID_TYPE',
'permissions',
'Array of ApplicationCommandPermissionData',
true,
);
} }
const data = await this.permissionsPath(guildId, commandId).put({ const data = await this.permissionsPath(guildId, commandId).put({
data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) }, data: {
permissions: permissions.map((perm) =>
this.constructor.transformPermissions(perm),
),
},
}); });
return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); return data.permissions.map((perm) =>
this.constructor.transformPermissions(perm, true),
);
} }
if (!Array.isArray(fullPermissions)) { if (!Array.isArray(fullPermissions)) {
throw new TypeError('INVALID_TYPE', 'fullPermissions', 'Array of GuildApplicationCommandPermissionData', true); throw new TypeError(
'INVALID_TYPE',
'fullPermissions',
'Array of GuildApplicationCommandPermissionData',
true,
);
} }
const APIPermissions = []; const APIPermissions = [];
for (const perm of fullPermissions) { for (const perm of fullPermissions) {
if (!Array.isArray(perm.permissions)) throw new TypeError('INVALID_ELEMENT', 'Array', 'fullPermissions', perm); if (!Array.isArray(perm.permissions))
throw new TypeError(
'INVALID_ELEMENT',
'Array',
'fullPermissions',
perm,
);
APIPermissions.push({ APIPermissions.push({
id: perm.id, id: perm.id,
permissions: perm.permissions.map(p => this.constructor.transformPermissions(p)), permissions: perm.permissions.map((p) =>
this.constructor.transformPermissions(p),
),
}); });
} }
const data = await this.permissionsPath(guildId).put({ const data = await this.permissionsPath(guildId).put({
@@ -190,7 +224,9 @@ class ApplicationCommandPermissionsManager extends BaseManager {
(coll, perm) => (coll, perm) =>
coll.set( coll.set(
perm.id, perm.id,
perm.permissions.map(p => this.constructor.transformPermissions(p, true)), perm.permissions.map((p) =>
this.constructor.transformPermissions(p, true),
),
), ),
new Collection(), new Collection(),
); );
@@ -221,26 +257,41 @@ class ApplicationCommandPermissionsManager extends BaseManager {
*/ */
async add({ guild, command, permissions }) { async add({ guild, command, permissions }) {
const { guildId, commandId } = this._validateOptions(guild, command); const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); if (!commandId)
throw new TypeError(
'INVALID_TYPE',
'command',
'ApplicationCommandResolvable',
);
if (!Array.isArray(permissions)) { if (!Array.isArray(permissions)) {
throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); throw new TypeError(
'INVALID_TYPE',
'permissions',
'Array of ApplicationCommandPermissionData',
true,
);
} }
let existing = []; let existing = [];
try { try {
existing = await this.fetch({ guild: guildId, command: commandId }); existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) { } catch (error) {
if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS)
throw error;
} }
const newPermissions = permissions.slice(); const newPermissions = permissions.slice();
for (const perm of existing) { for (const perm of existing) {
if (!newPermissions.some(x => x.id === perm.id)) { if (!newPermissions.some((x) => x.id === perm.id)) {
newPermissions.push(perm); newPermissions.push(perm);
} }
} }
return this.set({ guild: guildId, command: commandId, permissions: newPermissions }); return this.set({
guild: guildId,
command: commandId,
permissions: newPermissions,
});
} }
/** /**
@@ -272,15 +323,27 @@ class ApplicationCommandPermissionsManager extends BaseManager {
*/ */
async remove({ guild, command, users, roles }) { async remove({ guild, command, users, roles }) {
const { guildId, commandId } = this._validateOptions(guild, command); const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); if (!commandId)
throw new TypeError(
'INVALID_TYPE',
'command',
'ApplicationCommandResolvable',
);
if (!users && !roles) throw new TypeError('INVALID_TYPE', 'users OR roles', 'Array or Resolvable', true); if (!users && !roles)
throw new TypeError(
'INVALID_TYPE',
'users OR roles',
'Array or Resolvable',
true,
);
let resolvedIds = []; let resolvedIds = [];
if (Array.isArray(users)) { if (Array.isArray(users)) {
users.forEach(user => { users.forEach((user) => {
const userId = this.client.users.resolveId(user); const userId = this.client.users.resolveId(user);
if (!userId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', user); if (!userId)
throw new TypeError('INVALID_ELEMENT', 'Array', 'users', user);
resolvedIds.push(userId); resolvedIds.push(userId);
}); });
} else if (users) { } else if (users) {
@@ -292,14 +355,15 @@ class ApplicationCommandPermissionsManager extends BaseManager {
} }
if (Array.isArray(roles)) { if (Array.isArray(roles)) {
roles.forEach(role => { roles.forEach((role) => {
if (typeof role === 'string') { if (typeof role === 'string') {
resolvedIds.push(role); resolvedIds.push(role);
return; return;
} }
if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
const roleId = this.guild.roles.resolveId(role); const roleId = this.guild.roles.resolveId(role);
if (!roleId) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', role); if (!roleId)
throw new TypeError('INVALID_ELEMENT', 'Array', 'users', role);
resolvedIds.push(roleId); resolvedIds.push(roleId);
}); });
} else if (roles) { } else if (roles) {
@@ -309,7 +373,11 @@ class ApplicationCommandPermissionsManager extends BaseManager {
if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE');
const roleId = this.guild.roles.resolveId(roles); const roleId = this.guild.roles.resolveId(roles);
if (!roleId) { if (!roleId) {
throw new TypeError('INVALID_TYPE', 'users', 'Array or RoleResolvable'); throw new TypeError(
'INVALID_TYPE',
'users',
'Array or RoleResolvable',
);
} }
resolvedIds.push(roleId); resolvedIds.push(roleId);
} }
@@ -319,10 +387,13 @@ class ApplicationCommandPermissionsManager extends BaseManager {
try { try {
existing = await this.fetch({ guild: guildId, command: commandId }); existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) { } catch (error) {
if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS)
throw error;
} }
const permissions = existing.filter(perm => !resolvedIds.includes(perm.id)); const permissions = existing.filter(
(perm) => !resolvedIds.includes(perm.id),
);
return this.set({ guild: guildId, command: commandId, permissions }); return this.set({ guild: guildId, command: commandId, permissions });
} }
@@ -347,9 +418,19 @@ class ApplicationCommandPermissionsManager extends BaseManager {
*/ */
async has({ guild, command, permissionId }) { async has({ guild, command, permissionId }) {
const { guildId, commandId } = this._validateOptions(guild, command); const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); if (!commandId)
throw new TypeError(
'INVALID_TYPE',
'command',
'ApplicationCommandResolvable',
);
if (!permissionId) throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable'); if (!permissionId)
throw new TypeError(
'INVALID_TYPE',
'permissionId',
'UserResolvable or RoleResolvable',
);
let resolvedId = permissionId; let resolvedId = permissionId;
if (typeof permissionId !== 'string') { if (typeof permissionId !== 'string') {
resolvedId = this.client.users.resolveId(permissionId); resolvedId = this.client.users.resolveId(permissionId);
@@ -358,7 +439,11 @@ class ApplicationCommandPermissionsManager extends BaseManager {
resolvedId = this.guild.roles.resolveId(permissionId); resolvedId = this.guild.roles.resolveId(permissionId);
} }
if (!resolvedId) { if (!resolvedId) {
throw new TypeError('INVALID_TYPE', 'permissionId', 'UserResolvable or RoleResolvable'); throw new TypeError(
'INVALID_TYPE',
'permissionId',
'UserResolvable or RoleResolvable',
);
} }
} }
@@ -366,10 +451,11 @@ class ApplicationCommandPermissionsManager extends BaseManager {
try { try {
existing = await this.fetch({ guild: guildId, command: commandId }); existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) { } catch (error) {
if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS)
throw error;
} }
return existing.some(perm => perm.id === resolvedId); return existing.some((perm) => perm.id === resolvedId);
} }
_validateOptions(guild, command) { _validateOptions(guild, command) {
@@ -383,7 +469,12 @@ class ApplicationCommandPermissionsManager extends BaseManager {
} }
commandId ??= this.client.application?.commands.resolveId(command); commandId ??= this.client.application?.commands.resolveId(command);
if (!commandId) { if (!commandId) {
throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable', true); throw new TypeError(
'INVALID_TYPE',
'command',
'ApplicationCommandResolvable',
true,
);
} }
} }
return { guildId, commandId }; return { guildId, commandId };

View File

@@ -116,32 +116,52 @@ class AutoModerationRuleManager extends CachedManager {
exemptChannels, exemptChannels,
reason, reason,
}) { }) {
const data = await this.client.api.guilds(this.guild.id)['auto-moderation'].rules.post({ const data = await this.client.api
.guilds(this.guild.id)
['auto-moderation'].rules.post({
data: { data: {
name, name,
event_type: typeof eventType === 'number' ? eventType : AutoModerationRuleEventTypes[eventType], event_type:
trigger_type: typeof triggerType === 'number' ? triggerType : AutoModerationRuleTriggerTypes[triggerType], typeof eventType === 'number'
? eventType
: AutoModerationRuleEventTypes[eventType],
trigger_type:
typeof triggerType === 'number'
? triggerType
: AutoModerationRuleTriggerTypes[triggerType],
trigger_metadata: triggerMetadata && { trigger_metadata: triggerMetadata && {
keyword_filter: triggerMetadata.keywordFilter, keyword_filter: triggerMetadata.keywordFilter,
regex_patterns: triggerMetadata.regexPatterns, regex_patterns: triggerMetadata.regexPatterns,
presets: triggerMetadata.presets?.map(preset => presets: triggerMetadata.presets?.map((preset) =>
typeof preset === 'number' ? preset : AutoModerationRuleKeywordPresetTypes[preset], typeof preset === 'number'
? preset
: AutoModerationRuleKeywordPresetTypes[preset],
), ),
allow_list: triggerMetadata.allowList, allow_list: triggerMetadata.allowList,
mention_total_limit: triggerMetadata.mentionTotalLimit, mention_total_limit: triggerMetadata.mentionTotalLimit,
mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled, mention_raid_protection_enabled:
triggerMetadata.mentionRaidProtectionEnabled,
}, },
actions: actions.map(action => ({ actions: actions.map((action) => ({
type: typeof action.type === 'number' ? action.type : AutoModerationActionTypes[action.type], type:
typeof action.type === 'number'
? action.type
: AutoModerationActionTypes[action.type],
metadata: { metadata: {
duration_seconds: action.metadata?.durationSeconds, duration_seconds: action.metadata?.durationSeconds,
channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel), channel_id:
action.metadata?.channel &&
this.guild.channels.resolveId(action.metadata.channel),
custom_message: action.metadata?.customMessage, custom_message: action.metadata?.customMessage,
}, },
})), })),
enabled, enabled,
exempt_roles: exemptRoles?.map(exemptRole => this.guild.roles.resolveId(exemptRole)), exempt_roles: exemptRoles?.map((exemptRole) =>
exempt_channels: exemptChannels?.map(exemptChannel => this.guild.channels.resolveId(exemptChannel)), this.guild.roles.resolveId(exemptRole),
),
exempt_channels: exemptChannels?.map((exemptChannel) =>
this.guild.channels.resolveId(exemptChannel),
),
}, },
reason, reason,
}); });
@@ -173,7 +193,16 @@ class AutoModerationRuleManager extends CachedManager {
*/ */
async edit( async edit(
autoModerationRule, autoModerationRule,
{ name, eventType, triggerMetadata, actions, enabled, exemptRoles, exemptChannels, reason }, {
name,
eventType,
triggerMetadata,
actions,
enabled,
exemptRoles,
exemptChannels,
reason,
},
) { ) {
const autoModerationRuleId = this.resolveId(autoModerationRule); const autoModerationRuleId = this.resolveId(autoModerationRule);
@@ -183,28 +212,43 @@ class AutoModerationRuleManager extends CachedManager {
.patch({ .patch({
data: { data: {
name, name,
event_type: typeof eventType === 'number' ? eventType : AutoModerationRuleEventTypes[eventType], event_type:
typeof eventType === 'number'
? eventType
: AutoModerationRuleEventTypes[eventType],
trigger_metadata: triggerMetadata && { trigger_metadata: triggerMetadata && {
keyword_filter: triggerMetadata.keywordFilter, keyword_filter: triggerMetadata.keywordFilter,
regex_patterns: triggerMetadata.regexPatterns, regex_patterns: triggerMetadata.regexPatterns,
presets: triggerMetadata.presets?.map(preset => presets: triggerMetadata.presets?.map((preset) =>
typeof preset === 'number' ? preset : AutoModerationRuleKeywordPresetTypes[preset], typeof preset === 'number'
? preset
: AutoModerationRuleKeywordPresetTypes[preset],
), ),
allow_list: triggerMetadata.allowList, allow_list: triggerMetadata.allowList,
mention_total_limit: triggerMetadata.mentionTotalLimit, mention_total_limit: triggerMetadata.mentionTotalLimit,
mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled, mention_raid_protection_enabled:
triggerMetadata.mentionRaidProtectionEnabled,
}, },
actions: actions?.map(action => ({ actions: actions?.map((action) => ({
type: typeof action.type === 'number' ? action.type : AutoModerationActionTypes[action.type], type:
typeof action.type === 'number'
? action.type
: AutoModerationActionTypes[action.type],
metadata: { metadata: {
duration_seconds: action.metadata?.durationSeconds, duration_seconds: action.metadata?.durationSeconds,
channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel), channel_id:
action.metadata?.channel &&
this.guild.channels.resolveId(action.metadata.channel),
custom_message: action.metadata?.customMessage, custom_message: action.metadata?.customMessage,
}, },
})), })),
enabled, enabled,
exempt_roles: exemptRoles?.map(exemptRole => this.guild.roles.resolveId(exemptRole)), exempt_roles: exemptRoles?.map((exemptRole) =>
exempt_channels: exemptChannels?.map(exemptChannel => this.guild.channels.resolveId(exemptChannel)), this.guild.roles.resolveId(exemptRole),
),
exempt_channels: exemptChannels?.map((exemptChannel) =>
this.guild.channels.resolveId(exemptChannel),
),
}, },
reason, reason,
}); });
@@ -255,9 +299,15 @@ class AutoModerationRuleManager extends CachedManager {
fetch(options) { fetch(options) {
if (!options) return this._fetchMany(); if (!options) return this._fetchMany();
const { autoModerationRule, cache, force } = options; const { autoModerationRule, cache, force } = options;
const resolvedAutoModerationRule = this.resolveId(autoModerationRule ?? options); const resolvedAutoModerationRule = this.resolveId(
autoModerationRule ?? options,
);
if (resolvedAutoModerationRule) { if (resolvedAutoModerationRule) {
return this._fetchSingle({ autoModerationRule: resolvedAutoModerationRule, cache, force }); return this._fetchSingle({
autoModerationRule: resolvedAutoModerationRule,
cache,
force,
});
} }
return this._fetchMany(options); return this._fetchMany(options);
} }
@@ -268,15 +318,24 @@ class AutoModerationRuleManager extends CachedManager {
if (existing) return existing; if (existing) return existing;
} }
const data = await this.client.api.guilds(this.guild.id)('auto-moderation').rules(autoModerationRule).get(); const data = await this.client.api
.guilds(this.guild.id)('auto-moderation')
.rules(autoModerationRule)
.get();
return this._add(data, cache); return this._add(data, cache);
} }
async _fetchMany(options = {}) { async _fetchMany(options = {}) {
const data = await this.client.api.guilds(this.guild.id)('auto-moderation').rules.get(); const data = await this.client.api
.guilds(this.guild.id)('auto-moderation')
.rules.get();
return data.reduce( return data.reduce(
(col, autoModerationRule) => col.set(autoModerationRule.id, this._add(autoModerationRule, options.cache)), (col, autoModerationRule) =>
col.set(
autoModerationRule.id,
this._add(autoModerationRule, options.cache),
),
new Collection(), new Collection(),
); );
} }
@@ -289,7 +348,10 @@ class AutoModerationRuleManager extends CachedManager {
*/ */
async delete(autoModerationRule, reason) { async delete(autoModerationRule, reason) {
const autoModerationRuleId = this.resolveId(autoModerationRule); const autoModerationRuleId = this.resolveId(autoModerationRule);
await this.client.api.guilds(this.guild.id)('auto-moderation').rules(autoModerationRuleId).delete({ reason }); await this.client.api
.guilds(this.guild.id)('auto-moderation')
.rules(autoModerationRuleId)
.delete({ reason });
} }
} }

View File

@@ -34,9 +34,11 @@ class BillingManager extends BaseManager {
*/ */
async fetchPaymentSources() { async fetchPaymentSources() {
// https://discord.com/api/v9/users/@me/billing/payment-sources // https://discord.com/api/v9/users/@me/billing/payment-sources
const d = await this.client.api.users('@me').billing['payment-sources'].get(); const d = await this.client.api
.users('@me')
.billing['payment-sources'].get();
// ! TODO: Create a PaymentSource class // ! TODO: Create a PaymentSource class
this.paymentSources = new Collection(d.map(s => [s.id, s])); this.paymentSources = new Collection(d.map((s) => [s.id, s]));
return this.paymentSources; return this.paymentSources;
} }
@@ -46,8 +48,12 @@ class BillingManager extends BaseManager {
*/ */
async fetchGuildBoosts() { async fetchGuildBoosts() {
// https://discord.com/api/v9/users/@me/guilds/premium/subscription-slots // https://discord.com/api/v9/users/@me/guilds/premium/subscription-slots
const d = await this.client.api.users('@me').guilds.premium['subscription-slots'].get(); const d = await this.client.api
this.guildBoosts = new Collection(d.map(s => [s.id, new GuildBoost(this.client, s)])); .users('@me')
.guilds.premium['subscription-slots'].get();
this.guildBoosts = new Collection(
d.map((s) => [s.id, new GuildBoost(this.client, s)]),
);
return this.guildBoosts; return this.guildBoosts;
} }
@@ -58,7 +64,7 @@ class BillingManager extends BaseManager {
async fetchCurrentSubscription() { async fetchCurrentSubscription() {
// https://discord.com/api/v9/users/@me/billing/subscriptions // https://discord.com/api/v9/users/@me/billing/subscriptions
const d = await this.client.api.users('@me').billing.subscriptions.get(); const d = await this.client.api.users('@me').billing.subscriptions.get();
this.currentSubscription = new Collection(d.map(s => [s.id, s])); this.currentSubscription = new Collection(d.map((s) => [s.id, s]));
return this.currentSubscription; return this.currentSubscription;
} }
} }

View File

@@ -19,7 +19,9 @@ class CachedManager extends DataManager {
* @readonly * @readonly
* @name CachedManager#_cache * @name CachedManager#_cache
*/ */
Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) }); Object.defineProperty(this, '_cache', {
value: this.client.options.makeCache(this.constructor, this.holds),
});
let cleanup = this._cache[_cleanupSymbol]?.(); let cleanup = this._cache[_cleanupSymbol]?.();
if (cleanup) { if (cleanup) {
@@ -62,7 +64,9 @@ class CachedManager extends DataManager {
return clone; return clone;
} }
const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; const entry = this.holds
? new this.holds(this.client, data, ...extras)
: data;
if (cache) this.cache.set(id ?? entry.id, entry); if (cache) this.cache.set(id ?? entry.id, entry);
return entry; return entry;
} }

View File

@@ -3,7 +3,11 @@
const process = require('node:process'); const process = require('node:process');
const CachedManager = require('./CachedManager'); const CachedManager = require('./CachedManager');
const { Channel } = require('../structures/Channel'); const { Channel } = require('../structures/Channel');
const { Events, ThreadChannelTypes, RelationshipTypes } = require('../util/Constants'); const {
Events,
ThreadChannelTypes,
RelationshipTypes,
} = require('../util/Constants');
let cacheWarningEmitted = false; let cacheWarningEmitted = false;
@@ -16,8 +20,10 @@ class ChannelManager extends CachedManager {
super(client, Channel, iterable); super(client, Channel, iterable);
const defaultCaching = const defaultCaching =
this._cache.constructor.name === 'Collection' || this._cache.constructor.name === 'Collection' ||
((this._cache.maxSize === undefined || this._cache.maxSize === Infinity) && ((this._cache.maxSize === undefined ||
(this._cache.sweepFilter === undefined || this._cache.sweepFilter.isDefault)); this._cache.maxSize === Infinity) &&
(this._cache.sweepFilter === undefined ||
this._cache.sweepFilter.isDefault));
if (!cacheWarningEmitted && !defaultCaching) { if (!cacheWarningEmitted && !defaultCaching) {
cacheWarningEmitted = true; cacheWarningEmitted = true;
process.emitWarning( process.emitWarning(
@@ -44,10 +50,15 @@ class ChannelManager extends CachedManager {
return existing; return existing;
} }
const channel = Channel.create(this.client, data, guild, { allowUnknownGuild }); const channel = Channel.create(this.client, data, guild, {
allowUnknownGuild,
});
if (!channel) { if (!channel) {
this.client.emit(Events.DEBUG, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`); this.client.emit(
Events.DEBUG,
`Failed to find guild, or unknown type for channel ${data.id} ${data.type}`,
);
return null; return null;
} }
@@ -115,7 +126,10 @@ class ChannelManager extends CachedManager {
* .then(channel => console.log(channel.name)) * .then(channel => console.log(channel.name))
* .catch(console.error); * .catch(console.error);
*/ */
async fetch(id, { allowUnknownGuild = false, cache = true, force = false } = {}) { async fetch(
id,
{ allowUnknownGuild = false, cache = true, force = false } = {},
) {
if (!force) { if (!force) {
const existing = this.cache.get(id); const existing = this.cache.get(id);
if (existing && !existing.partial) return existing; if (existing && !existing.partial) return existing;
@@ -133,11 +147,19 @@ class ChannelManager extends CachedManager {
* client.channels.createGroupDM(); * client.channels.createGroupDM();
*/ */
async createGroupDM(recipients = []) { async createGroupDM(recipients = []) {
if (!Array.isArray(recipients)) throw new Error(`Expected an array of recipients (got ${typeof recipients})`); if (!Array.isArray(recipients))
throw new Error(
`Expected an array of recipients (got ${typeof recipients})`,
);
recipients = recipients recipients = recipients
.map(r => this.client.users.resolveId(r)) .map((r) => this.client.users.resolveId(r))
.filter(r => r && this.client.relationships.cache.get(r) == RelationshipTypes.FRIEND); .filter(
if (recipients.length == 1 || recipients.length > 9) throw new Error('Invalid Users length (max=9)'); (r) =>
r &&
this.client.relationships.cache.get(r) == RelationshipTypes.FRIEND,
);
if (recipients.length == 1 || recipients.length > 9)
throw new Error('Invalid Users length (max=9)');
const data = await this.client.api.users['@me'].channels.post({ const data = await this.client.api.users['@me'].channels.post({
data: { recipients }, data: { recipients },
}); });

View File

@@ -186,7 +186,7 @@ class ClientUserSettingManager extends BaseManager {
if ('custom_status' in data) { if ('custom_status' in data) {
this.customStatus = data.custom_status; this.customStatus = data.custom_status;
const activities = this.client.presence.activities.filter( const activities = this.client.presence.activities.filter(
a => ![ActivityTypes.CUSTOM, 'CUSTOM'].includes(a.type), (a) => ![ActivityTypes.CUSTOM, 'CUSTOM'].includes(a.type),
); );
if (data.custom_status) { if (data.custom_status) {
const custom = new CustomStatus(this.client); const custom = new CustomStatus(this.client);
@@ -200,7 +200,10 @@ class ClientUserSettingManager extends BaseManager {
if (emoji) custom.setEmoji(emoji); if (emoji) custom.setEmoji(emoji);
activities.push(custom); activities.push(custom);
} }
this.client.emit('debug', '[SETTING > ClientUser] Sync activities & status'); this.client.emit(
'debug',
'[SETTING > ClientUser] Sync activities & status',
);
this.client.user.setPresence({ activities }); this.client.user.setPresence({ activities });
} }
if ('friend_source_flags' in data) { if ('friend_source_flags' in data) {
@@ -212,7 +215,10 @@ class ClientUserSettingManager extends BaseManager {
* @type {Collection<Snowflake, Guild>} * @type {Collection<Snowflake, Guild>}
*/ */
this.disableDMfromGuilds = new Collection( this.disableDMfromGuilds = new Collection(
data.restricted_guilds.map(guildId => [guildId, this.client.guilds.cache.get(guildId)]), data.restricted_guilds.map((guildId) => [
guildId,
this.client.guilds.cache.get(guildId),
]),
); );
} }
} }
@@ -293,7 +299,10 @@ class ClientUserSettingManager extends BaseManager {
data.emoji_name = options.emoji?.name; data.emoji_name = options.emoji?.name;
data.emoji_id = options.emoji?.id; data.emoji_id = options.emoji?.id;
} else { } else {
data.emoji_name = typeof options.emoji?.name === 'string' ? options.emoji?.name : null; data.emoji_name =
typeof options.emoji?.name === 'string'
? options.emoji?.name
: null;
} }
} }
return this.edit({ custom_status: data }); return this.edit({ custom_status: data });
@@ -305,7 +314,9 @@ class ClientUserSettingManager extends BaseManager {
}; };
if (typeof options.text === 'string') { if (typeof options.text === 'string') {
if (options.text.length > 128) { if (options.text.length > 128) {
throw new RangeError('[INVALID_VALUE] Custom status text must be less than 128 characters'); throw new RangeError(
'[INVALID_VALUE] Custom status text must be less than 128 characters',
);
} }
data.text = options.text; data.text = options.text;
} }
@@ -315,16 +326,20 @@ class ClientUserSettingManager extends BaseManager {
data.emoji_name = emoji.name; data.emoji_name = emoji.name;
data.emoji_id = emoji.id; data.emoji_id = emoji.id;
} else { } else {
data.emoji_name = typeof options.emoji === 'string' ? options.emoji : null; data.emoji_name =
typeof options.emoji === 'string' ? options.emoji : null;
} }
} }
if (typeof options.expires === 'number') { if (typeof options.expires === 'number') {
if (options.expires < Date.now()) { if (options.expires < Date.now()) {
throw new RangeError(`[INVALID_VALUE] Custom status expiration must be greater than ${Date.now()}`); throw new RangeError(
`[INVALID_VALUE] Custom status expiration must be greater than ${Date.now()}`,
);
} }
data.expires_at = new Date(options.expires).toISOString(); data.expires_at = new Date(options.expires).toISOString();
} }
if (['online', 'idle', 'dnd', 'invisible'].includes(options.status)) this.edit({ status: options.status }); if (['online', 'idle', 'dnd', 'invisible'].includes(options.status))
this.edit({ status: options.status });
return this.edit({ custom_status: data }); return this.edit({ custom_status: data });
} }
} }
@@ -340,7 +355,9 @@ class ClientUserSettingManager extends BaseManager {
} }
return this.edit({ return this.edit({
default_guilds_restricted: status, default_guilds_restricted: status,
restricted_guilds: status ? this.client.guilds.cache.map(v => v.id) : [], restricted_guilds: status
? this.client.guilds.cache.map((v) => v.id)
: [],
}); });
} }
/** /**
@@ -364,8 +381,11 @@ class ClientUserSettingManager extends BaseManager {
* @returns {Promise} * @returns {Promise}
*/ */
removeRestrictedGuild(guildId) { removeRestrictedGuild(guildId) {
if (!this.disableDMfromServer.delete(guildId)) throw new Error('Guild is already restricted'); if (!this.disableDMfromServer.delete(guildId))
return this.edit({ restricted_guilds: this.disableDMfromServer.map((v, k) => k) }); throw new Error('Guild is already restricted');
return this.edit({
restricted_guilds: this.disableDMfromServer.map((v, k) => k),
});
} }
} }

View File

@@ -38,7 +38,8 @@ class DataManager extends BaseManager {
*/ */
resolve(idOrInstance) { resolve(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance; if (idOrInstance instanceof this.holds) return idOrInstance;
if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) ?? null; if (typeof idOrInstance === 'string')
return this.cache.get(idOrInstance) ?? null;
return null; return null;
} }

View File

@@ -47,7 +47,9 @@ class GuildBanManager extends CachedManager {
* @returns {?GuildBan} * @returns {?GuildBan}
*/ */
resolve(ban) { resolve(ban) {
return super.resolve(ban) ?? super.resolve(this.client.users.resolveId(ban)); return (
super.resolve(ban) ?? super.resolve(this.client.users.resolveId(ban))
);
} }
/** /**
@@ -99,7 +101,8 @@ class GuildBanManager extends CachedManager {
if (!options) return this._fetchMany(); if (!options) return this._fetchMany();
const { user, cache, force, limit, before, after } = options; const { user, cache, force, limit, before, after } = options;
const resolvedUser = this.client.users.resolveId(user ?? options); const resolvedUser = this.client.users.resolveId(user ?? options);
if (resolvedUser) return this._fetchSingle({ user: resolvedUser, cache, force }); if (resolvedUser)
return this._fetchSingle({ user: resolvedUser, cache, force });
if (!before && !after && !limit && typeof cache === 'undefined') { if (!before && !after && !limit && typeof cache === 'undefined') {
throw new Error('FETCH_BAN_RESOLVE_ID'); throw new Error('FETCH_BAN_RESOLVE_ID');
@@ -123,7 +126,10 @@ class GuildBanManager extends CachedManager {
query: options, query: options,
}); });
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, options.cache)), new Collection()); return data.reduce(
(col, ban) => col.set(ban.user.id, this._add(ban, options.cache)),
new Collection(),
);
} }
/** /**
* Options used to ban a user from a guild. * Options used to ban a user from a guild.
@@ -149,7 +155,8 @@ class GuildBanManager extends CachedManager {
* .catch(console.error); * .catch(console.error);
*/ */
async create(user, options = {}) { async create(user, options = {}) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); if (typeof options !== 'object')
throw new TypeError('INVALID_TYPE', 'options', 'object', true);
const id = this.client.users.resolveId(user); const id = this.client.users.resolveId(user);
if (!id) throw new Error('BAN_RESOLVE_ID', true); if (!id) throw new Error('BAN_RESOLVE_ID', true);
@@ -232,18 +239,32 @@ class GuildBanManager extends CachedManager {
*/ */
async bulkCreate(users, options = {}) { async bulkCreate(users, options = {}) {
if (!users || !(Array.isArray(users) || users instanceof Collection)) { if (!users || !(Array.isArray(users) || users instanceof Collection)) {
throw new TypeError('INVALID_TYPE', 'users', 'Array or Collection of UserResolvable', true); throw new TypeError(
'INVALID_TYPE',
'users',
'Array or Collection of UserResolvable',
true,
);
} }
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); if (typeof options !== 'object')
throw new TypeError('INVALID_TYPE', 'options', 'object', true);
const userIds = users.map(user => this.client.users.resolveId(user)); const userIds = users.map((user) => this.client.users.resolveId(user));
if (userIds.length === 0) throw new Error('BULK_BAN_USERS_OPTION_EMPTY'); if (userIds.length === 0) throw new Error('BULK_BAN_USERS_OPTION_EMPTY');
const result = await this.client.api.guilds(this.guild.id)['bulk-ban'].post({ const result = await this.client.api
data: { delete_message_days: options.deleteMessageSeconds, user_ids: userIds }, .guilds(this.guild.id)
['bulk-ban'].post({
data: {
delete_message_days: options.deleteMessageSeconds,
user_ids: userIds,
},
reason: options.reason, reason: options.reason,
}); });
return { bannedUsers: result.banned_users, failedUsers: result.failed_users }; return {
bannedUsers: result.banned_users,
failedUsers: result.failed_users,
};
} }
} }

View File

@@ -18,7 +18,11 @@ const {
} = require('../util/Constants'); } = require('../util/Constants');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
const Util = require('../util/Util'); const Util = require('../util/Util');
const { resolveAutoArchiveMaxLimit, transformGuildForumTag, transformGuildDefaultReaction } = require('../util/Util'); const {
resolveAutoArchiveMaxLimit,
transformGuildForumTag,
transformGuildDefaultReaction,
} = require('../util/Util');
let cacheWarningEmitted = false; let cacheWarningEmitted = false;
let storeChannelDeprecationEmitted = false; let storeChannelDeprecationEmitted = false;
@@ -32,8 +36,10 @@ class GuildChannelManager extends CachedManager {
super(guild.client, GuildChannel, iterable); super(guild.client, GuildChannel, iterable);
const defaultCaching = const defaultCaching =
this._cache.constructor.name === 'Collection' || this._cache.constructor.name === 'Collection' ||
((this._cache.maxSize === undefined || this._cache.maxSize === Infinity) && ((this._cache.maxSize === undefined ||
(this._cache.sweepFilter === undefined || this._cache.sweepFilter.isDefault)); this._cache.maxSize === Infinity) &&
(this._cache.sweepFilter === undefined ||
this._cache.sweepFilter.isDefault));
if (!cacheWarningEmitted && !defaultCaching) { if (!cacheWarningEmitted && !defaultCaching) {
cacheWarningEmitted = true; cacheWarningEmitted = true;
process.emitWarning( process.emitWarning(
@@ -89,7 +95,8 @@ class GuildChannelManager extends CachedManager {
* @returns {?(GuildChannel|ThreadChannel)} * @returns {?(GuildChannel|ThreadChannel)}
*/ */
resolve(channel) { resolve(channel) {
if (channel instanceof ThreadChannel) return this.cache.get(channel.id) ?? null; if (channel instanceof ThreadChannel)
return this.cache.get(channel.id) ?? null;
return super.resolve(channel); return super.resolve(channel);
} }
@@ -154,17 +161,33 @@ class GuildChannelManager extends CachedManager {
} = {}, } = {},
) { ) {
parent &&= this.client.channels.resolveId(parent); parent &&= this.client.channels.resolveId(parent);
permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); permissionOverwrites &&= permissionOverwrites.map((o) =>
const intType = typeof type === 'number' ? type : ChannelTypes[type] ?? ChannelTypes.GUILD_TEXT; PermissionOverwrites.resolve(o, this.guild),
);
const intType =
typeof type === 'number'
? type
: (ChannelTypes[type] ?? ChannelTypes.GUILD_TEXT);
const videoMode = typeof videoQualityMode === 'number' ? videoQualityMode : VideoQualityModes[videoQualityMode]; const videoMode =
typeof videoQualityMode === 'number'
? videoQualityMode
: VideoQualityModes[videoQualityMode];
const sortMode = typeof defaultSortOrder === 'number' ? defaultSortOrder : SortOrderTypes[defaultSortOrder]; const sortMode =
typeof defaultSortOrder === 'number'
? defaultSortOrder
: SortOrderTypes[defaultSortOrder];
const layoutMode = const layoutMode =
typeof defaultForumLayout === 'number' ? defaultForumLayout : ForumLayoutTypes[defaultForumLayout]; typeof defaultForumLayout === 'number'
? defaultForumLayout
: ForumLayoutTypes[defaultForumLayout];
if (intType === ChannelTypes.GUILD_STORE && !storeChannelDeprecationEmitted) { if (
intType === ChannelTypes.GUILD_STORE &&
!storeChannelDeprecationEmitted
) {
storeChannelDeprecationEmitted = true; storeChannelDeprecationEmitted = true;
process.emitWarning( process.emitWarning(
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@@ -187,8 +210,12 @@ class GuildChannelManager extends CachedManager {
rate_limit_per_user: rateLimitPerUser, rate_limit_per_user: rateLimitPerUser,
rtc_region: rtcRegion, rtc_region: rtcRegion,
video_quality_mode: videoMode, video_quality_mode: videoMode,
available_tags: availableTags?.map(availableTag => transformGuildForumTag(availableTag)), available_tags: availableTags?.map((availableTag) =>
default_reaction_emoji: defaultReactionEmoji && transformGuildDefaultReaction(defaultReactionEmoji), transformGuildForumTag(availableTag),
),
default_reaction_emoji:
defaultReactionEmoji &&
transformGuildDefaultReaction(defaultReactionEmoji),
default_sort_order: sortMode, default_sort_order: sortMode,
default_forum_layout: layoutMode, default_forum_layout: layoutMode,
default_thread_rate_limit_per_user: defaultThreadRateLimitPerUser, default_thread_rate_limit_per_user: defaultThreadRateLimitPerUser,
@@ -215,7 +242,8 @@ class GuildChannelManager extends CachedManager {
*/ */
async createWebhook(channel, name, { avatar, reason } = {}) { async createWebhook(channel, name, { avatar, reason } = {}) {
const id = this.resolveId(channel); const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); if (!id)
throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const resolvedImage = await DataResolver.resolveImage(avatar); const resolvedImage = await DataResolver.resolveImage(avatar);
@@ -239,8 +267,11 @@ class GuildChannelManager extends CachedManager {
async addFollower(channel, targetChannel, reason) { async addFollower(channel, targetChannel, reason) {
const channelId = this.resolveId(channel); const channelId = this.resolveId(channel);
const targetChannelId = this.resolveId(targetChannel); const targetChannelId = this.resolveId(targetChannel);
if (!channelId || !targetChannelId) throw new Error('GUILD_CHANNEL_RESOLVE'); if (!channelId || !targetChannelId)
const { webhook_id } = await this.client.api.channels[channelId].followers.post({ throw new Error('GUILD_CHANNEL_RESOLVE');
const { webhook_id } = await this.client.api.channels[
channelId
].followers.post({
data: { webhook_channel_id: targetChannelId }, data: { webhook_channel_id: targetChannelId },
reason, reason,
}); });
@@ -288,31 +319,36 @@ class GuildChannelManager extends CachedManager {
*/ */
async edit(channel, data, reason) { async edit(channel, data, reason) {
channel = this.resolve(channel); channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); if (!channel)
throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const parentId = data.parent && this.client.channels.resolveId(data.parent); const parentId = data.parent && this.client.channels.resolveId(data.parent);
if (typeof data.position !== 'undefined') await this.setPosition(channel, data.position, { reason }); if (typeof data.position !== 'undefined')
await this.setPosition(channel, data.position, { reason });
let permission_overwrites = data.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild)); let permission_overwrites = data.permissionOverwrites?.map((o) =>
PermissionOverwrites.resolve(o, this.guild),
);
if (data.lockPermissions) { if (data.lockPermissions) {
if (parentId) { if (parentId) {
const newParent = this.cache.get(parentId); const newParent = this.cache.get(parentId);
if (newParent?.type === 'GUILD_CATEGORY') { if (newParent?.type === 'GUILD_CATEGORY') {
permission_overwrites = newParent.permissionOverwrites.cache.map(o => permission_overwrites = newParent.permissionOverwrites.cache.map(
PermissionOverwrites.resolve(o, this.guild), (o) => PermissionOverwrites.resolve(o, this.guild),
); );
} }
} else if (channel.parent) { } else if (channel.parent) {
permission_overwrites = channel.parent.permissionOverwrites.cache.map(o => permission_overwrites = channel.parent.permissionOverwrites.cache.map(
PermissionOverwrites.resolve(o, this.guild), (o) => PermissionOverwrites.resolve(o, this.guild),
); );
} }
} }
let defaultAutoArchiveDuration = data.defaultAutoArchiveDuration; let defaultAutoArchiveDuration = data.defaultAutoArchiveDuration;
if (defaultAutoArchiveDuration === 'MAX') defaultAutoArchiveDuration = resolveAutoArchiveMaxLimit(this.guild); if (defaultAutoArchiveDuration === 'MAX')
defaultAutoArchiveDuration = resolveAutoArchiveMaxLimit(this.guild);
const newData = await this.client.api.channels(channel.id).patch({ const newData = await this.client.api.channels(channel.id).patch({
data: { data: {
@@ -324,18 +360,26 @@ class GuildChannelManager extends CachedManager {
user_limit: data.userLimit ?? channel.userLimit, user_limit: data.userLimit ?? channel.userLimit,
rtc_region: 'rtcRegion' in data ? data.rtcRegion : channel.rtcRegion, rtc_region: 'rtcRegion' in data ? data.rtcRegion : channel.rtcRegion,
video_quality_mode: video_quality_mode:
typeof data.videoQualityMode === 'string' ? VideoQualityModes[data.videoQualityMode] : data.videoQualityMode, typeof data.videoQualityMode === 'string'
? VideoQualityModes[data.videoQualityMode]
: data.videoQualityMode,
parent_id: parentId, parent_id: parentId,
lock_permissions: data.lockPermissions, lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser, rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: defaultAutoArchiveDuration, default_auto_archive_duration: defaultAutoArchiveDuration,
permission_overwrites, permission_overwrites,
available_tags: data.availableTags?.map(availableTag => transformGuildForumTag(availableTag)), available_tags: data.availableTags?.map((availableTag) =>
default_reaction_emoji: data.defaultReactionEmoji && transformGuildDefaultReaction(data.defaultReactionEmoji), transformGuildForumTag(availableTag),
),
default_reaction_emoji:
data.defaultReactionEmoji &&
transformGuildDefaultReaction(data.defaultReactionEmoji),
default_thread_rate_limit_per_user: data.defaultThreadRateLimitPerUser, default_thread_rate_limit_per_user: data.defaultThreadRateLimitPerUser,
flags: 'flags' in data ? ChannelFlags.resolve(data.flags) : undefined, flags: 'flags' in data ? ChannelFlags.resolve(data.flags) : undefined,
default_sort_order: default_sort_order:
typeof data.defaultSortOrder === 'string' ? SortOrderTypes[data.defaultSortOrder] : data.defaultSortOrder, typeof data.defaultSortOrder === 'string'
? SortOrderTypes[data.defaultSortOrder]
: data.defaultSortOrder,
}, },
reason, reason,
}); });
@@ -357,7 +401,8 @@ class GuildChannelManager extends CachedManager {
*/ */
async setPosition(channel, position, { relative, reason } = {}) { async setPosition(channel, position, { relative, reason } = {}) {
channel = this.resolve(channel); channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); if (!channel)
throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const updatedChannels = await Util.setPosition( const updatedChannels = await Util.setPosition(
channel, channel,
position, position,
@@ -399,13 +444,18 @@ class GuildChannelManager extends CachedManager {
if (id) { if (id) {
const data = await this.client.api.channels(id).get(); const data = await this.client.api.channels(id).get();
// Since this is the guild manager, throw if on a different guild // Since this is the guild manager, throw if on a different guild
if (this.guild.id !== data.guild_id) throw new Error('GUILD_CHANNEL_UNOWNED'); if (this.guild.id !== data.guild_id)
throw new Error('GUILD_CHANNEL_UNOWNED');
return this.client.channels._add(data, this.guild, { cache }); return this.client.channels._add(data, this.guild, { cache });
} }
const data = await this.client.api.guilds(this.guild.id).channels.get(); const data = await this.client.api.guilds(this.guild.id).channels.get();
const channels = new Collection(); const channels = new Collection();
for (const channel of data) channels.set(channel.id, this.client.channels._add(channel, this.guild, { cache })); for (const channel of data)
channels.set(
channel.id,
this.client.channels._add(channel, this.guild, { cache }),
);
return channels; return channels;
} }
@@ -421,9 +471,13 @@ class GuildChannelManager extends CachedManager {
*/ */
async fetchWebhooks(channel) { async fetchWebhooks(channel) {
const id = this.resolveId(channel); const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); if (!id)
throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const data = await this.client.api.channels[id].webhooks.get(); const data = await this.client.api.channels[id].webhooks.get();
return data.reduce((hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)), new Collection()); return data.reduce(
(hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)),
new Collection(),
);
} }
/** /**
@@ -453,14 +507,17 @@ class GuildChannelManager extends CachedManager {
* .catch(console.error); * .catch(console.error);
*/ */
async setPositions(channelPositions) { async setPositions(channelPositions) {
channelPositions = channelPositions.map(r => ({ channelPositions = channelPositions.map((r) => ({
id: this.client.channels.resolveId(r.channel), id: this.client.channels.resolveId(r.channel),
position: r.position, position: r.position,
lock_permissions: r.lockPermissions, lock_permissions: r.lockPermissions,
parent_id: typeof r.parent !== 'undefined' ? this.resolveId(r.parent) : undefined, parent_id:
typeof r.parent !== 'undefined' ? this.resolveId(r.parent) : undefined,
})); }));
await this.client.api.guilds(this.guild.id).channels.patch({ data: channelPositions }); await this.client.api
.guilds(this.guild.id)
.channels.patch({ data: channelPositions });
return this.client.actions.GuildChannelsPositionUpdate.handle({ return this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id, guild_id: this.guild.id,
channels: channelPositions, channels: channelPositions,
@@ -480,7 +537,8 @@ class GuildChannelManager extends CachedManager {
*/ */
async delete(channel, reason) { async delete(channel, reason) {
const id = this.resolveId(channel); const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); if (!id)
throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
await this.client.api.channels(id).delete({ reason }); await this.client.api.channels(id).delete({ reason });
} }
} }

View File

@@ -56,17 +56,30 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
const data = { image: attachment, name }; const data = { image: attachment, name };
if (roles) { if (roles) {
if (!Array.isArray(roles) && !(roles instanceof Collection)) { if (!Array.isArray(roles) && !(roles instanceof Collection)) {
throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true); throw new TypeError(
'INVALID_TYPE',
'options.roles',
'Array or Collection of Roles or Snowflakes',
true,
);
} }
data.roles = []; data.roles = [];
for (const role of roles.values()) { for (const role of roles.values()) {
const resolvedRole = this.guild.roles.resolveId(role); const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role); if (!resolvedRole)
throw new TypeError(
'INVALID_ELEMENT',
'Array or Collection',
'options.roles',
role,
);
data.roles.push(resolvedRole); data.roles.push(resolvedRole);
} }
} }
const emoji = await this.client.api.guilds(this.guild.id).emojis.post({ data, reason }); const emoji = await this.client.api
.guilds(this.guild.id)
.emojis.post({ data, reason });
return this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji; return this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji;
} }
@@ -92,7 +105,10 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
const existing = this.cache.get(id); const existing = this.cache.get(id);
if (existing) return existing; if (existing) return existing;
} }
const emoji = await this.client.api.guilds(this.guild.id).emojis(id).get(); const emoji = await this.client.api
.guilds(this.guild.id)
.emojis(id)
.get();
return this._add(emoji, cache); return this._add(emoji, cache);
} }
@@ -110,7 +126,8 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
*/ */
async delete(emoji, reason) { async delete(emoji, reason) {
const id = this.resolveId(emoji); const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true); if (!id)
throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason }); await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason });
} }
@@ -123,8 +140,9 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
*/ */
async edit(emoji, data, reason) { async edit(emoji, data, reason) {
const id = this.resolveId(emoji); const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true); if (!id)
const roles = data.roles?.map(r => this.guild.roles.resolveId(r)); throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
const roles = data.roles?.map((r) => this.guild.roles.resolveId(r));
const newData = await this.client.api const newData = await this.client.api
.guilds(this.guild.id) .guilds(this.guild.id)
.emojis(id) .emojis(id)
@@ -151,7 +169,8 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
*/ */
async fetchAuthor(emoji) { async fetchAuthor(emoji) {
emoji = this.resolve(emoji); emoji = this.resolve(emoji);
if (!emoji) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true); if (!emoji)
throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
if (emoji.managed) { if (emoji.managed) {
throw new Error('EMOJI_MANAGED'); throw new Error('EMOJI_MANAGED');
} }
@@ -159,10 +178,16 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
const { me } = this.guild.members; const { me } = this.guild.members;
if (!me) throw new Error('GUILD_UNCACHED_ME'); if (!me) throw new Error('GUILD_UNCACHED_ME');
if (!me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) { if (!me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild); throw new Error(
'MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION',
this.guild,
);
} }
const data = await this.client.api.guilds(this.guild.id).emojis(emoji.id).get(); const data = await this.client.api
.guilds(this.guild.id)
.emojis(emoji.id)
.get();
emoji._patch(data); emoji._patch(data);
return emoji.author; return emoji.author;
} }

View File

@@ -31,7 +31,9 @@ class GuildEmojiRoleManager extends DataManager {
* @readonly * @readonly
*/ */
get cache() { get cache() {
return this.guild.roles.cache.filter(role => this.emoji._roles.includes(role.id)); return this.guild.roles.cache.filter((role) =>
this.emoji._roles.includes(role.id),
);
} }
/** /**
@@ -40,13 +42,19 @@ class GuildEmojiRoleManager extends DataManager {
* @returns {Promise<GuildEmoji>} * @returns {Promise<GuildEmoji>}
*/ */
async add(roleOrRoles) { async add(roleOrRoles) {
if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles]; if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection))
roleOrRoles = [roleOrRoles];
const resolvedRoles = []; const resolvedRoles = [];
for (const role of roleOrRoles.values()) { for (const role of roleOrRoles.values()) {
const resolvedRole = this.guild.roles.resolveId(role); const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) { if (!resolvedRole) {
throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role); throw new TypeError(
'INVALID_ELEMENT',
'Array or Collection',
'roles',
role,
);
} }
resolvedRoles.push(resolvedRole); resolvedRoles.push(resolvedRole);
} }
@@ -61,18 +69,26 @@ class GuildEmojiRoleManager extends DataManager {
* @returns {Promise<GuildEmoji>} * @returns {Promise<GuildEmoji>}
*/ */
async remove(roleOrRoles) { async remove(roleOrRoles) {
if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles]; if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection))
roleOrRoles = [roleOrRoles];
const resolvedRoleIds = []; const resolvedRoleIds = [];
for (const role of roleOrRoles.values()) { for (const role of roleOrRoles.values()) {
const roleId = this.guild.roles.resolveId(role); const roleId = this.guild.roles.resolveId(role);
if (!roleId) { if (!roleId) {
throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role); throw new TypeError(
'INVALID_ELEMENT',
'Array or Collection',
'roles',
role,
);
} }
resolvedRoleIds.push(roleId); resolvedRoleIds.push(roleId);
} }
const newRoles = [...this.cache.keys()].filter(id => !resolvedRoleIds.includes(id)); const newRoles = [...this.cache.keys()].filter(
(id) => !resolvedRoleIds.includes(id),
);
return this.set(newRoles); return this.set(newRoles);
} }

View File

@@ -3,7 +3,11 @@
const ThreadManager = require('./ThreadManager'); const ThreadManager = require('./ThreadManager');
const { TypeError } = require('../errors'); const { TypeError } = require('../errors');
const MessagePayload = require('../structures/MessagePayload'); const MessagePayload = require('../structures/MessagePayload');
const { resolveAutoArchiveMaxLimit, getUploadURL, uploadFile } = require('../util/Util'); const {
resolveAutoArchiveMaxLimit,
getUploadURL,
uploadFile,
} = require('../util/Util');
/** /**
* Manages API methods for threads in forum channels and stores their cache. * Manages API methods for threads in forum channels and stores their cache.
@@ -72,7 +76,7 @@ class GuildForumThreadManager extends ThreadManager {
// New API // New API
const attachments = await getUploadURL(this.client, this.channel.id, files); const attachments = await getUploadURL(this.client, this.channel.id, files);
const requestPromises = attachments.map(async attachment => { const requestPromises = attachments.map(async (attachment) => {
await uploadFile(files[attachment.id].file, attachment.upload_url); await uploadFile(files[attachment.id].file, attachment.upload_url);
return { return {
id: attachment.id, id: attachment.id,
@@ -86,9 +90,12 @@ class GuildForumThreadManager extends ThreadManager {
const attachmentsData = await Promise.all(requestPromises); const attachmentsData = await Promise.all(requestPromises);
attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id)); attachmentsData.sort((a, b) => parseInt(a.id) - parseInt(b.id));
if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild); if (autoArchiveDuration === 'MAX')
autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
const post_data = await this.client.api.channels(this.channel.id).threads.post({ const post_data = await this.client.api
.channels(this.channel.id)
.threads.post({
data: { data: {
name, name,
auto_archive_duration: autoArchiveDuration, auto_archive_duration: autoArchiveDuration,

View File

@@ -156,12 +156,18 @@ class GuildInviteManager extends CachedManager {
async _fetchMany(cache) { async _fetchMany(cache) {
const data = await this.client.api.guilds(this.guild.id).invites.get(); const data = await this.client.api.guilds(this.guild.id).invites.get();
return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection()); return data.reduce(
(col, invite) => col.set(invite.code, this._add(invite, cache)),
new Collection(),
);
} }
async _fetchChannelMany(channelId, cache) { async _fetchChannelMany(channelId, cache) {
const data = await this.client.api.channels(channelId).invites.get(); const data = await this.client.api.channels(channelId).invites.get();
return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection()); return data.reduce(
(col, invite) => col.set(invite.code, this._add(invite, cache)),
new Collection(),
);
} }
/** /**
@@ -177,7 +183,16 @@ class GuildInviteManager extends CachedManager {
*/ */
async create( async create(
channel, channel,
{ temporary = false, maxAge = 86400, maxUses = 0, unique, targetUser, targetApplication, targetType, reason } = {}, {
temporary = false,
maxAge = 86400,
maxUses = 0,
unique,
targetUser,
targetApplication,
targetType,
reason,
} = {},
) { ) {
const id = this.guild.channels.resolveId(channel); const id = this.guild.channels.resolveId(channel);
if (!id) throw new Error('GUILD_CHANNEL_RESOLVE'); if (!id) throw new Error('GUILD_CHANNEL_RESOLVE');
@@ -189,7 +204,10 @@ class GuildInviteManager extends CachedManager {
max_uses: maxUses, max_uses: maxUses,
unique, unique,
target_user_id: this.client.users.resolveId(targetUser), target_user_id: this.client.users.resolveId(targetUser),
target_application_id: targetApplication?.id ?? targetApplication?.applicationId ?? targetApplication, target_application_id:
targetApplication?.id ??
targetApplication?.applicationId ??
targetApplication,
target_type: targetType, target_type: targetType,
}, },
reason, reason,

View File

@@ -187,13 +187,18 @@ class GuildManager extends CachedManager {
verificationLevel = VerificationLevels[verificationLevel]; verificationLevel = VerificationLevels[verificationLevel];
} }
if (typeof defaultMessageNotifications === 'string') { if (typeof defaultMessageNotifications === 'string') {
defaultMessageNotifications = DefaultMessageNotificationLevels[defaultMessageNotifications]; defaultMessageNotifications =
DefaultMessageNotificationLevels[defaultMessageNotifications];
} }
if (typeof explicitContentFilter === 'string') { if (typeof explicitContentFilter === 'string') {
explicitContentFilter = ExplicitContentFilterLevels[explicitContentFilter]; explicitContentFilter =
ExplicitContentFilterLevels[explicitContentFilter];
} }
for (const channel of channels) { for (const channel of channels) {
channel.type &&= typeof channel.type === 'number' ? channel.type : ChannelTypes[channel.type]; channel.type &&=
typeof channel.type === 'number'
? channel.type
: ChannelTypes[channel.type];
channel.parent_id = channel.parentId; channel.parent_id = channel.parentId;
delete channel.parentId; delete channel.parentId;
channel.user_limit = channel.userLimit; channel.user_limit = channel.userLimit;
@@ -242,10 +247,11 @@ class GuildManager extends CachedManager {
}, },
}); });
if (this.client.guilds.cache.has(data.id)) return this.client.guilds.cache.get(data.id); if (this.client.guilds.cache.has(data.id))
return this.client.guilds.cache.get(data.id);
return new Promise(resolve => { return new Promise((resolve) => {
const handleGuild = guild => { const handleGuild = (guild) => {
if (guild.id === data.id) { if (guild.id === data.id) {
clearTimeout(timeout); clearTimeout(timeout);
this.client.removeListener(Events.GUILD_CREATE, handleGuild); this.client.removeListener(Events.GUILD_CREATE, handleGuild);
@@ -293,12 +299,19 @@ class GuildManager extends CachedManager {
if (existing) return existing; if (existing) return existing;
} }
const data = await this.client.api.guilds(id).get({ query: { with_counts: options.withCounts ?? true } }); const data = await this.client.api
.guilds(id)
.get({ query: { with_counts: options.withCounts ?? true } });
return this._add(data, options.cache); return this._add(data, options.cache);
} }
const data = await this.client.api.users('@me').guilds.get({ query: options }); const data = await this.client.api
return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection()); .users('@me')
.guilds.get({ query: options });
return data.reduce(
(coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)),
new Collection(),
);
} }
/** /**
@@ -319,8 +332,10 @@ class GuildManager extends CachedManager {
const data = await this.client.api.guilds(guildId)['incident-actions'].put({ const data = await this.client.api.guilds(guildId)['incident-actions'].put({
data: { data: {
invites_disabled_until: invitesDisabledUntil && new Date(invitesDisabledUntil).toISOString(), invites_disabled_until:
dms_disabled_until: dmsDisabledUntil && new Date(dmsDisabledUntil).toISOString(), invitesDisabledUntil && new Date(invitesDisabledUntil).toISOString(),
dms_disabled_until:
dmsDisabledUntil && new Date(dmsDisabledUntil).toISOString(),
}, },
}); });

View File

@@ -106,20 +106,41 @@ class GuildMemberManager extends CachedManager {
deaf: options.deaf, deaf: options.deaf,
}; };
if (options.roles) { if (options.roles) {
if (!Array.isArray(options.roles) && !(options.roles instanceof Collection)) { if (
throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true); !Array.isArray(options.roles) &&
!(options.roles instanceof Collection)
) {
throw new TypeError(
'INVALID_TYPE',
'options.roles',
'Array or Collection of Roles or Snowflakes',
true,
);
} }
const resolvedRoles = []; const resolvedRoles = [];
for (const role of options.roles.values()) { for (const role of options.roles.values()) {
const resolvedRole = this.guild.roles.resolveId(role); const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role); if (!resolvedRole)
throw new TypeError(
'INVALID_ELEMENT',
'Array or Collection',
'options.roles',
role,
);
resolvedRoles.push(resolvedRole); resolvedRoles.push(resolvedRole);
} }
resolvedOptions.roles = resolvedRoles; resolvedOptions.roles = resolvedRoles;
} }
const data = await this.client.api.guilds(this.guild.id).members(userId).put({ data: resolvedOptions }); const data = await this.client.api
.guilds(this.guild.id)
.members(userId)
.put({ data: resolvedOptions });
// Data is an empty buffer if the member is already part of the guild. // Data is an empty buffer if the member is already part of the guild.
return data instanceof Buffer ? (options.fetchWhenExisting === false ? null : this.fetch(userId)) : this._add(data); return data instanceof Buffer
? options.fetchWhenExisting === false
? null
: this.fetch(userId)
: this._add(data);
} }
/** /**
@@ -207,12 +228,13 @@ class GuildMemberManager extends CachedManager {
if (user) return this._fetchSingle({ user, cache: true }); if (user) return this._fetchSingle({ user, cache: true });
if (options.user) { if (options.user) {
if (Array.isArray(options.user)) { if (Array.isArray(options.user)) {
options.user = options.user.map(u => this.client.users.resolveId(u)); options.user = options.user.map((u) => this.client.users.resolveId(u));
return this._fetchMany(options); return this._fetchMany(options);
} else { } else {
options.user = this.client.users.resolveId(options.user); options.user = this.client.users.resolveId(options.user);
} }
if (!options.limit && !options.withPresences) return this._fetchSingle(options); if (!options.limit && !options.withPresences)
return this._fetchSingle(options);
} }
return this._fetchMany(options); return this._fetchMany(options);
} }
@@ -240,8 +262,13 @@ class GuildMemberManager extends CachedManager {
* @returns {Promise<Collection<Snowflake, GuildMember>>} * @returns {Promise<Collection<Snowflake, GuildMember>>}
*/ */
async search({ query, limit = 1, cache = true } = {}) { async search({ query, limit = 1, cache = true } = {}) {
const data = await this.client.api.guilds(this.guild.id).members.search.get({ query: { query, limit } }); const data = await this.client.api
return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection()); .guilds(this.guild.id)
.members.search.get({ query: { query, limit } });
return data.reduce(
(col, member) => col.set(member.user.id, this._add(member, cache)),
new Collection(),
);
} }
/** /**
@@ -286,10 +313,13 @@ class GuildMemberManager extends CachedManager {
_data.channel_id = null; _data.channel_id = null;
_data.channel = undefined; _data.channel = undefined;
} }
_data.roles &&= _data.roles.map(role => (role instanceof Role ? role.id : role)); _data.roles &&= _data.roles.map((role) =>
role instanceof Role ? role.id : role,
);
_data.communication_disabled_until = _data.communication_disabled_until =
_data.communicationDisabledUntil && new Date(_data.communicationDisabledUntil).toISOString(); _data.communicationDisabledUntil &&
new Date(_data.communicationDisabledUntil).toISOString();
_data.flags = _data.flags && GuildMemberFlags.resolve(_data.flags); _data.flags = _data.flags && GuildMemberFlags.resolve(_data.flags);
@@ -304,7 +334,10 @@ class GuildMemberManager extends CachedManager {
let endpoint = this.client.api.guilds(this.guild.id); let endpoint = this.client.api.guilds(this.guild.id);
if (id === this.client.user.id) { if (id === this.client.user.id) {
const keys = Object.keys(data); const keys = Object.keys(data);
if (keys.length === 1 && ['nick', 'avatar', 'banner', 'bio'].includes(keys[0])) { if (
keys.length === 1 &&
['nick', 'avatar', 'banner', 'bio'].includes(keys[0])
) {
endpoint = endpoint.members('@me'); endpoint = endpoint.members('@me');
} else { } else {
endpoint = endpoint.members(id); endpoint = endpoint.members(id);
@@ -351,7 +384,13 @@ class GuildMemberManager extends CachedManager {
* .then(pruned => console.log(`I just pruned ${pruned} people!`)) * .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error); * .catch(console.error);
*/ */
async prune({ days = 7, dry = false, count: compute_prune_count = true, roles = [], reason } = {}) { async prune({
days = 7,
dry = false,
count: compute_prune_count = true,
roles = [],
reason,
} = {}) {
if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
const query = { days }; const query = { days };
@@ -440,7 +479,10 @@ class GuildMemberManager extends CachedManager {
if (existing && !existing.partial) return existing; if (existing && !existing.partial) return existing;
} }
const data = await this.client.api.guilds(this.guild.id).members(user).get(); const data = await this.client.api
.guilds(this.guild.id)
.members(user)
.get();
return this._add(data, cache); return this._add(data, cache);
} }
@@ -455,7 +497,11 @@ class GuildMemberManager extends CachedManager {
const userId = this.resolveId(user); const userId = this.resolveId(user);
const roleId = this.guild.roles.resolveId(role); const roleId = this.guild.roles.resolveId(role);
await this.client.api.guilds(this.guild.id).members(userId).roles(roleId).put({ reason }); await this.client.api
.guilds(this.guild.id)
.members(userId)
.roles(roleId)
.put({ reason });
return this.resolve(user) ?? this.client.users.resolve(user) ?? userId; return this.resolve(user) ?? this.client.users.resolve(user) ?? userId;
} }
@@ -471,7 +517,11 @@ class GuildMemberManager extends CachedManager {
const userId = this.resolveId(user); const userId = this.resolveId(user);
const roleId = this.guild.roles.resolveId(role); const roleId = this.guild.roles.resolveId(role);
await this.client.api.guilds(this.guild.id).members(userId).roles(roleId).delete({ reason }); await this.client.api
.guilds(this.guild.id)
.members(userId)
.roles(roleId)
.delete({ reason });
return this.resolve(user) ?? this.client.users.resolve(user) ?? userId; return this.resolve(user) ?? this.client.users.resolve(user) ?? userId;
} }
@@ -483,7 +533,7 @@ class GuildMemberManager extends CachedManager {
* @returns {Promise<Collection<Snowflake, GuildMember>>} * @returns {Promise<Collection<Snowflake, GuildMember>>}
*/ */
fetchByMemberSafety(timeout = 15_000) { fetchByMemberSafety(timeout = 15_000) {
return new Promise(resolve => { return new Promise((resolve) => {
const nonce = SnowflakeUtil.generate(); const nonce = SnowflakeUtil.generate();
const fetchedMembers = new Collection(); const fetchedMembers = new Collection();
let timeout_ = setTimeout(() => { let timeout_ = setTimeout(() => {
@@ -556,12 +606,17 @@ class GuildMemberManager extends CachedManager {
for (const member of members.values()) { for (const member of members.values()) {
fetchedMembers.set(member.id, member); fetchedMembers.set(member.id, member);
} }
if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) { if (
members.size < 1_000 ||
(limit && fetchedMembers.size >= limit) ||
i === chunk.count
) {
clearTimeout(timeout); clearTimeout(timeout);
this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.decrementMaxListeners(); this.client.decrementMaxListeners();
let fetched = fetchedMembers; let fetched = fetchedMembers;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first(); if (user_ids && !Array.isArray(user_ids) && fetched.size)
fetched = fetched.first();
resolve(fetched); resolve(fetched);
} }
}; };

View File

@@ -33,7 +33,9 @@ class GuildMemberRoleManager extends DataManager {
*/ */
get cache() { get cache() {
const everyone = this.guild.roles.everyone; const everyone = this.guild.roles.everyone;
return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); return this.guild.roles.cache
.filter((role) => this.member._roles.includes(role.id))
.set(everyone.id, everyone);
} }
/** /**
@@ -42,9 +44,11 @@ class GuildMemberRoleManager extends DataManager {
* @readonly * @readonly
*/ */
get hoist() { get hoist() {
const hoistedRoles = this.cache.filter(role => role.hoist); const hoistedRoles = this.cache.filter((role) => role.hoist);
if (!hoistedRoles.size) return null; if (!hoistedRoles.size) return null;
return hoistedRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); return hoistedRoles.reduce((prev, role) =>
role.comparePositionTo(prev) > 0 ? role : prev,
);
} }
/** /**
@@ -53,9 +57,13 @@ class GuildMemberRoleManager extends DataManager {
* @readonly * @readonly
*/ */
get icon() { get icon() {
const iconRoles = this.cache.filter(role => role.icon || role.unicodeEmoji); const iconRoles = this.cache.filter(
(role) => role.icon || role.unicodeEmoji,
);
if (!iconRoles.size) return null; if (!iconRoles.size) return null;
return iconRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); return iconRoles.reduce((prev, role) =>
role.comparePositionTo(prev) > 0 ? role : prev,
);
} }
/** /**
@@ -64,9 +72,11 @@ class GuildMemberRoleManager extends DataManager {
* @readonly * @readonly
*/ */
get color() { get color() {
const coloredRoles = this.cache.filter(role => role.colors.primaryColor); const coloredRoles = this.cache.filter((role) => role.colors.primaryColor);
if (!coloredRoles.size) return null; if (!coloredRoles.size) return null;
return coloredRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); return coloredRoles.reduce((prev, role) =>
role.comparePositionTo(prev) > 0 ? role : prev,
);
} }
/** /**
@@ -75,7 +85,10 @@ class GuildMemberRoleManager extends DataManager {
* @readonly * @readonly
*/ */
get highest() { get highest() {
return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first()); return this.cache.reduce(
(prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev),
this.cache.first(),
);
} }
/** /**
@@ -84,7 +97,7 @@ class GuildMemberRoleManager extends DataManager {
* @readonly * @readonly
*/ */
get premiumSubscriberRole() { get premiumSubscriberRole() {
return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null; return this.cache.find((role) => role.tags?.premiumSubscriberRole) ?? null;
} }
/** /**
@@ -95,7 +108,10 @@ class GuildMemberRoleManager extends DataManager {
*/ */
get botRole() { get botRole() {
if (!this.member.user.bot) return null; if (!this.member.user.bot) return null;
return this.cache.find(role => role.tags?.botId === this.member.user.id) ?? null; return (
this.cache.find((role) => role.tags?.botId === this.member.user.id) ??
null
);
} }
/** /**
@@ -111,7 +127,13 @@ class GuildMemberRoleManager extends DataManager {
const resolvedRoles = []; const resolvedRoles = [];
for (const role of roleOrRoles.values()) { for (const role of roleOrRoles.values()) {
const resolvedRole = this.guild.roles.resolveId(role); const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role); if (!resolvedRole)
throw new TypeError(
'INVALID_ELEMENT',
'Array or Collection',
'roles',
role,
);
resolvedRoles.push(resolvedRole); resolvedRoles.push(resolvedRole);
} }
@@ -120,10 +142,16 @@ class GuildMemberRoleManager extends DataManager {
} else { } else {
roleOrRoles = this.guild.roles.resolveId(roleOrRoles); roleOrRoles = this.guild.roles.resolveId(roleOrRoles);
if (roleOrRoles === null) { if (roleOrRoles === null) {
throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes'); throw new TypeError(
'INVALID_TYPE',
'roles',
'Role, Snowflake or Array or Collection of Roles or Snowflakes',
);
} }
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].put({ reason }); await this.client.api.guilds[this.guild.id].members[this.member.id].roles[
roleOrRoles
].put({ reason });
const clone = this.member._clone(); const clone = this.member._clone();
clone._roles = [...this.cache.keys(), roleOrRoles]; clone._roles = [...this.cache.keys(), roleOrRoles];
@@ -144,22 +172,36 @@ class GuildMemberRoleManager extends DataManager {
const resolvedRoles = []; const resolvedRoles = [];
for (const role of roleOrRoles.values()) { for (const role of roleOrRoles.values()) {
const resolvedRole = this.guild.roles.resolveId(role); const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'roles', role); if (!resolvedRole)
throw new TypeError(
'INVALID_ELEMENT',
'Array or Collection',
'roles',
role,
);
resolvedRoles.push(resolvedRole); resolvedRoles.push(resolvedRole);
} }
const newRoles = this.cache.filter(role => !resolvedRoles.includes(role.id)); const newRoles = this.cache.filter(
(role) => !resolvedRoles.includes(role.id),
);
return this.set(newRoles, reason); return this.set(newRoles, reason);
} else { } else {
roleOrRoles = this.guild.roles.resolveId(roleOrRoles); roleOrRoles = this.guild.roles.resolveId(roleOrRoles);
if (roleOrRoles === null) { if (roleOrRoles === null) {
throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes'); throw new TypeError(
'INVALID_TYPE',
'roles',
'Role, Snowflake or Array or Collection of Roles or Snowflakes',
);
} }
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].delete({ reason }); await this.client.api.guilds[this.guild.id].members[this.member.id].roles[
roleOrRoles
].delete({ reason });
const clone = this.member._clone(); const clone = this.member._clone();
const newRoles = this.cache.filter(role => role.id !== roleOrRoles); const newRoles = this.cache.filter((role) => role.id !== roleOrRoles);
clone._roles = [...newRoles.keys()]; clone._roles = [...newRoles.keys()];
return clone; return clone;
} }

View File

@@ -4,7 +4,11 @@ const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager'); const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors'); const { TypeError, Error } = require('../errors');
const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent'); const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
const { PrivacyLevels, GuildScheduledEventEntityTypes, GuildScheduledEventStatuses } = require('../util/Constants'); const {
PrivacyLevels,
GuildScheduledEventEntityTypes,
GuildScheduledEventStatuses,
} = require('../util/Constants');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
const Util = require('../util/Util'); const Util = require('../util/Util');
@@ -82,7 +86,8 @@ class GuildScheduledEventManager extends CachedManager {
* @returns {Promise<GuildScheduledEvent>} * @returns {Promise<GuildScheduledEvent>}
*/ */
async create(options) { async create(options) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); if (typeof options !== 'object')
throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let { let {
privacyLevel, privacyLevel,
entityType, entityType,
@@ -97,8 +102,10 @@ class GuildScheduledEventManager extends CachedManager {
recurrenceRule, recurrenceRule,
} = options; } = options;
if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel]; if (typeof privacyLevel === 'string')
if (typeof entityType === 'string') entityType = GuildScheduledEventEntityTypes[entityType]; privacyLevel = PrivacyLevels[privacyLevel];
if (typeof entityType === 'string')
entityType = GuildScheduledEventEntityTypes[entityType];
let entity_metadata, channel_id; let entity_metadata, channel_id;
if (entityType === GuildScheduledEventEntityTypes.EXTERNAL) { if (entityType === GuildScheduledEventEntityTypes.EXTERNAL) {
@@ -107,21 +114,28 @@ class GuildScheduledEventManager extends CachedManager {
} else { } else {
channel_id = this.guild.channels.resolveId(channel); channel_id = this.guild.channels.resolveId(channel);
if (!channel_id) throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); if (!channel_id) throw new Error('GUILD_VOICE_CHANNEL_RESOLVE');
entity_metadata = typeof entityMetadata === 'undefined' ? entityMetadata : null; entity_metadata =
typeof entityMetadata === 'undefined' ? entityMetadata : null;
} }
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').post({ const data = await this.client.api
.guilds(this.guild.id, 'scheduled-events')
.post({
data: { data: {
channel_id, channel_id,
name, name,
privacy_level: privacyLevel, privacy_level: privacyLevel,
scheduled_start_time: new Date(scheduledStartTime).toISOString(), scheduled_start_time: new Date(scheduledStartTime).toISOString(),
scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime, scheduled_end_time: scheduledEndTime
? new Date(scheduledEndTime).toISOString()
: scheduledEndTime,
description, description,
image: image && (await DataResolver.resolveImage(image)), image: image && (await DataResolver.resolveImage(image)),
entity_type: entityType, entity_type: entityType,
entity_metadata, entity_metadata,
recurrence_rule: recurrenceRule && Util.transformGuildScheduledEventRecurrenceRule(recurrenceRule), recurrence_rule:
recurrenceRule &&
Util.transformGuildScheduledEventRecurrenceRule(recurrenceRule),
}, },
reason, reason,
}); });
@@ -171,7 +185,10 @@ class GuildScheduledEventManager extends CachedManager {
return data.reduce( return data.reduce(
(coll, rawGuildScheduledEventData) => (coll, rawGuildScheduledEventData) =>
coll.set(rawGuildScheduledEventData.id, this._add(rawGuildScheduledEventData, options.cache)), coll.set(
rawGuildScheduledEventData.id,
this._add(rawGuildScheduledEventData, options.cache),
),
new Collection(), new Collection(),
); );
} }
@@ -204,9 +221,11 @@ class GuildScheduledEventManager extends CachedManager {
*/ */
async edit(guildScheduledEvent, options) { async edit(guildScheduledEvent, options) {
const guildScheduledEventId = this.resolveId(guildScheduledEvent); const guildScheduledEventId = this.resolveId(guildScheduledEvent);
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE'); if (!guildScheduledEventId)
throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); if (typeof options !== 'object')
throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let { let {
privacyLevel, privacyLevel,
entityType, entityType,
@@ -222,9 +241,12 @@ class GuildScheduledEventManager extends CachedManager {
recurrenceRule, recurrenceRule,
} = options; } = options;
if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel]; if (typeof privacyLevel === 'string')
if (typeof entityType === 'string') entityType = GuildScheduledEventEntityTypes[entityType]; privacyLevel = PrivacyLevels[privacyLevel];
if (typeof status === 'string') status = GuildScheduledEventStatuses[status]; if (typeof entityType === 'string')
entityType = GuildScheduledEventEntityTypes[entityType];
if (typeof status === 'string')
status = GuildScheduledEventStatuses[status];
let entity_metadata; let entity_metadata;
if (entityMetadata) { if (entityMetadata) {
@@ -233,19 +255,30 @@ class GuildScheduledEventManager extends CachedManager {
}; };
} }
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).patch({ const data = await this.client.api
.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId)
.patch({
data: { data: {
channel_id: typeof channel === 'undefined' ? channel : this.guild.channels.resolveId(channel), channel_id:
typeof channel === 'undefined'
? channel
: this.guild.channels.resolveId(channel),
name, name,
privacy_level: privacyLevel, privacy_level: privacyLevel,
scheduled_start_time: scheduledStartTime ? new Date(scheduledStartTime).toISOString() : undefined, scheduled_start_time: scheduledStartTime
scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime, ? new Date(scheduledStartTime).toISOString()
: undefined,
scheduled_end_time: scheduledEndTime
? new Date(scheduledEndTime).toISOString()
: scheduledEndTime,
description, description,
entity_type: entityType, entity_type: entityType,
status, status,
image: image && (await DataResolver.resolveImage(image)), image: image && (await DataResolver.resolveImage(image)),
entity_metadata, entity_metadata,
recurrence_rule: recurrenceRule && Util.transformGuildScheduledEventRecurrenceRule(recurrenceRule), recurrence_rule:
recurrenceRule &&
Util.transformGuildScheduledEventRecurrenceRule(recurrenceRule),
}, },
reason, reason,
}); });
@@ -260,9 +293,12 @@ class GuildScheduledEventManager extends CachedManager {
*/ */
async delete(guildScheduledEvent) { async delete(guildScheduledEvent) {
const guildScheduledEventId = this.resolveId(guildScheduledEvent); const guildScheduledEventId = this.resolveId(guildScheduledEvent);
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE'); if (!guildScheduledEventId)
throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).delete(); await this.client.api
.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId)
.delete();
} }
/** /**
@@ -291,11 +327,14 @@ class GuildScheduledEventManager extends CachedManager {
*/ */
async fetchSubscribers(guildScheduledEvent, options = {}) { async fetchSubscribers(guildScheduledEvent, options = {}) {
const guildScheduledEventId = this.resolveId(guildScheduledEvent); const guildScheduledEventId = this.resolveId(guildScheduledEvent);
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE'); if (!guildScheduledEventId)
throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
let { limit, withMember, before, after } = options; let { limit, withMember, before, after } = options;
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).users.get({ const data = await this.client.api
.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId)
.users.get({
query: { limit, with_member: withMember, before, after }, query: { limit, with_member: withMember, before, after },
}); });
@@ -304,7 +343,9 @@ class GuildScheduledEventManager extends CachedManager {
coll.set(rawData.user.id, { coll.set(rawData.user.id, {
guildScheduledEventId: rawData.guild_scheduled_event_id, guildScheduledEventId: rawData.guild_scheduled_event_id,
user: this.client.users._add(rawData.user), user: this.client.users._add(rawData.user),
member: rawData.member ? this.guild.members._add({ ...rawData.member, user: rawData.user }) : null, member: rawData.member
? this.guild.members._add({ ...rawData.member, user: rawData.user })
: null,
}), }),
new Collection(), new Collection(),
); );

View File

@@ -66,7 +66,8 @@ class GuildStickerManager extends CachedManager {
const sticker = await this.client.api const sticker = await this.client.api
.guilds(this.guild.id) .guilds(this.guild.id)
.stickers.post({ data, files: [file], reason, dontUsePayloadJSON: true }); .stickers.post({ data, files: [file], reason, dontUsePayloadJSON: true });
return this.client.actions.GuildStickerCreate.handle(this.guild, sticker).sticker; return this.client.actions.GuildStickerCreate.handle(this.guild, sticker)
.sticker;
} }
/** /**
@@ -103,9 +104,13 @@ class GuildStickerManager extends CachedManager {
*/ */
async edit(sticker, data, reason) { async edit(sticker, data, reason) {
const stickerId = this.resolveId(sticker); const stickerId = this.resolveId(sticker);
if (!stickerId) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable'); if (!stickerId)
throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const d = await this.client.api.guilds(this.guild.id).stickers(stickerId).patch({ const d = await this.client.api
.guilds(this.guild.id)
.stickers(stickerId)
.patch({
data, data,
reason, reason,
}); });
@@ -127,9 +132,13 @@ class GuildStickerManager extends CachedManager {
*/ */
async delete(sticker, reason) { async delete(sticker, reason) {
sticker = this.resolveId(sticker); sticker = this.resolveId(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable'); if (!sticker)
throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
await this.client.api.guilds(this.guild.id).stickers(sticker).delete({ reason }); await this.client.api
.guilds(this.guild.id)
.stickers(sticker)
.delete({ reason });
} }
/** /**
@@ -154,12 +163,17 @@ class GuildStickerManager extends CachedManager {
const existing = this.cache.get(id); const existing = this.cache.get(id);
if (existing) return existing; if (existing) return existing;
} }
const sticker = await this.client.api.guilds(this.guild.id).stickers(id).get(); const sticker = await this.client.api
.guilds(this.guild.id)
.stickers(id)
.get();
return this._add(sticker, cache); return this._add(sticker, cache);
} }
const data = await this.client.api.guilds(this.guild.id).stickers.get(); const data = await this.client.api.guilds(this.guild.id).stickers.get();
return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)])); return new Collection(
data.map((sticker) => [sticker.id, this._add(sticker, cache)]),
);
} }
/** /**
@@ -169,8 +183,12 @@ class GuildStickerManager extends CachedManager {
*/ */
async fetchUser(sticker) { async fetchUser(sticker) {
sticker = this.resolve(sticker); sticker = this.resolve(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable'); if (!sticker)
const data = await this.client.api.guilds(this.guild.id).stickers(sticker.id).get(); throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const data = await this.client.api
.guilds(this.guild.id)
.stickers(sticker.id)
.get();
sticker._patch(data); sticker._patch(data);
return sticker.user; return sticker.user;
} }

View File

@@ -66,26 +66,42 @@ class GuildTextThreadManager extends ThreadManager {
} = {}) { } = {}) {
let path = this.client.api.channels(this.channel.id); let path = this.client.api.channels(this.channel.id);
if (type && typeof type !== 'string' && typeof type !== 'number') { if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number'); throw new TypeError(
'INVALID_TYPE',
'type',
'ThreadChannelType or Number',
);
} }
let resolvedType = let resolvedType =
this.channel.type === 'GUILD_NEWS' ? ChannelTypes.GUILD_NEWS_THREAD : ChannelTypes.GUILD_PUBLIC_THREAD; this.channel.type === 'GUILD_NEWS'
? ChannelTypes.GUILD_NEWS_THREAD
: ChannelTypes.GUILD_PUBLIC_THREAD;
if (startMessage) { if (startMessage) {
const startMessageId = this.channel.messages.resolveId(startMessage); const startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable'); if (!startMessageId)
throw new TypeError(
'INVALID_TYPE',
'startMessage',
'MessageResolvable',
);
path = path.messages(startMessageId); path = path.messages(startMessageId);
} else if (this.channel.type !== 'GUILD_NEWS') { } else if (this.channel.type !== 'GUILD_NEWS') {
resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType; resolvedType =
typeof type === 'string' ? ChannelTypes[type] : (type ?? resolvedType);
} }
if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild); if (autoArchiveDuration === 'MAX')
autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
const data = await path.threads.post({ const data = await path.threads.post({
data: { data: {
name, name,
auto_archive_duration: autoArchiveDuration, auto_archive_duration: autoArchiveDuration,
type: resolvedType, type: resolvedType,
invitable: resolvedType === ChannelTypes.GUILD_PRIVATE_THREAD ? invitable : undefined, invitable:
resolvedType === ChannelTypes.GUILD_PRIVATE_THREAD
? invitable
: undefined,
rate_limit_per_user: rateLimitPerUser, rate_limit_per_user: rateLimitPerUser,
}, },
reason, reason,

View File

@@ -66,7 +66,9 @@ class MessageManager extends CachedManager {
* .catch(console.error); * .catch(console.error);
*/ */
fetch(message, { cache = true, force = false } = {}) { fetch(message, { cache = true, force = false } = {}) {
return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache); return typeof message === 'string'
? this._fetchId(message, cache, force)
: this._fetchMany(message, cache);
} }
/** /**
@@ -82,9 +84,14 @@ class MessageManager extends CachedManager {
* .catch(console.error); * .catch(console.error);
*/ */
async fetchPinned(cache = true) { async fetchPinned(cache = true) {
const data = await this.client.api.channels[this.channel.id].pins.get(); const data = await this.client.api.channels[
this.channel.id
].messages.pins.get({
query: { limit: 50 },
});
const messages = new Collection(); const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache)); for (const message of data?.items || [])
messages.set(message.id, this._add(message, cache));
return messages; return messages;
} }
@@ -121,18 +128,26 @@ class MessageManager extends CachedManager {
*/ */
async edit(message, options) { async edit(message, options) {
const messageId = this.resolveId(message); const messageId = this.resolveId(message);
if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!messageId)
throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
const { data, files } = await (options instanceof MessagePayload const { data, files } = await (options instanceof MessagePayload
? options ? options
: MessagePayload.create(message instanceof Message ? message : this, options) : MessagePayload.create(
message instanceof Message ? message : this,
options,
)
) )
.resolveData() .resolveData()
.resolveFiles(); .resolveFiles();
// New API // New API
const attachments = await Util.getUploadURL(this.client, this.channel.id, files); const attachments = await Util.getUploadURL(
const requestPromises = attachments.map(async attachment => { this.client,
this.channel.id,
files,
);
const requestPromises = attachments.map(async (attachment) => {
await Util.uploadFile(files[attachment.id].file, attachment.upload_url); await Util.uploadFile(files[attachment.id].file, attachment.upload_url);
return { return {
id: attachment.id, id: attachment.id,
@@ -148,7 +163,9 @@ class MessageManager extends CachedManager {
data.attachments = attachmentsData; data.attachments = attachmentsData;
// Empty Files // Empty Files
const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data }); const d = await this.client.api.channels[this.channel.id].messages[
messageId
].patch({ data });
const existing = this.cache.get(messageId); const existing = this.cache.get(messageId);
if (existing) { if (existing) {
@@ -166,9 +183,13 @@ class MessageManager extends CachedManager {
*/ */
async crosspost(message) { async crosspost(message) {
message = this.resolveId(message); message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!message)
throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post(); const data = await this.client.api
.channels(this.channel.id)
.messages(message)
.crosspost.post();
return this.cache.get(data.id) ?? this._add(data); return this.cache.get(data.id) ?? this._add(data);
} }
@@ -180,9 +201,13 @@ class MessageManager extends CachedManager {
*/ */
async pin(message, reason) { async pin(message, reason) {
message = this.resolveId(message); message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!message)
throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).put({ reason }); await this.client.api
.channels(this.channel.id)
.messages.pins(message)
.put({ reason });
} }
/** /**
@@ -193,9 +218,13 @@ class MessageManager extends CachedManager {
*/ */
async unpin(message, reason) { async unpin(message, reason) {
message = this.resolveId(message); message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!message)
throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).delete({ reason }); await this.client.api
.channels(this.channel.id)
.messages.pins(message)
.delete({ reason });
} }
/** /**
@@ -207,10 +236,12 @@ class MessageManager extends CachedManager {
*/ */
async react(message, emoji, burst = false) { async react(message, emoji, burst = false) {
message = this.resolveId(message); message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!message)
throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
emoji = Util.resolvePartialEmoji(emoji); emoji = Util.resolvePartialEmoji(emoji);
if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable'); if (!emoji)
throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable');
const emojiId = emoji.id const emojiId = emoji.id
? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}` ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}`
@@ -235,7 +266,8 @@ class MessageManager extends CachedManager {
*/ */
async delete(message) { async delete(message) {
message = this.resolveId(message); message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); if (!message)
throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).messages(message).delete(); await this.client.api.channels(this.channel.id).messages(message).delete();
} }
@@ -255,8 +287,10 @@ class MessageManager extends CachedManager {
}, },
cache, cache,
) )
.then(data_ => .then((data_) =>
data_.has(messageId) ? resolve(data_.get(messageId)) : reject(new Error('MESSAGE_ID_NOT_FOUND')), data_.has(messageId)
? resolve(data_.get(messageId))
: reject(new Error('MESSAGE_ID_NOT_FOUND')),
) )
.catch(reject); .catch(reject);
}); });
@@ -294,8 +328,21 @@ class MessageManager extends CachedManager {
*/ */
async search(options = {}) { async search(options = {}) {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let { authors, content, mentions, has, maxId, minId, channels, pinned, nsfw, offset, limit, sortBy, sortOrder } = let {
Object.assign( authors,
content,
mentions,
has,
maxId,
minId,
channels,
pinned,
nsfw,
offset,
limit,
sortBy,
sortOrder,
} = Object.assign(
{ {
authors: [], authors: [],
content: '', content: '',
@@ -314,17 +361,20 @@ class MessageManager extends CachedManager {
options, options,
); );
// Validate // Validate
if (authors.length > 0) authors = authors.map(u => this.client.users.resolveId(u)); if (authors.length > 0)
if (mentions.length > 0) mentions = mentions.map(u => this.client.users.resolveId(u)); authors = authors.map((u) => this.client.users.resolveId(u));
if (mentions.length > 0)
mentions = mentions.map((u) => this.client.users.resolveId(u));
if (channels.length > 0) { if (channels.length > 0) {
channels = channels channels = channels
.map(c => this.client.channels.resolveId(c)) .map((c) => this.client.channels.resolveId(c))
.filter(id => { .filter((id) => {
if (this.channel.guildId) { if (this.channel.guildId) {
const c = this.channel.guild.channels.cache.get(id); const c = this.channel.guild.channels.cache.get(id);
if (!c || !c.messages) return false; if (!c || !c.messages) return false;
const perm = c.permissionsFor(this.client.user); const perm = c.permissionsFor(this.client.user);
if (!perm.has('READ_MESSAGE_HISTORY') || !perm.has('VIEW_CHANNEL')) return false; if (!perm.has('READ_MESSAGE_HISTORY') || !perm.has('VIEW_CHANNEL'))
return false;
return true; return true;
} else { } else {
return true; return true;
@@ -335,11 +385,18 @@ class MessageManager extends CachedManager {
let stringQuery = []; let stringQuery = [];
const result = new Collection(); const result = new Collection();
let data; let data;
if (authors.length > 0) stringQuery.push(authors.map(id => `author_id=${id}`).join('&')); if (authors.length > 0)
if (content && content.length) stringQuery.push(`content=${encodeURIComponent(content)}`); stringQuery.push(authors.map((id) => `author_id=${id}`).join('&'));
if (mentions.length > 0) stringQuery.push(mentions.map(id => `mentions=${id}`).join('&')); if (content && content.length)
has = has.filter(v => ['link', 'embed', 'file', 'video', 'image', 'sound', 'sticker'].includes(v)); stringQuery.push(`content=${encodeURIComponent(content)}`);
if (has.length > 0) stringQuery.push(has.map(v => `has=${v}`).join('&')); if (mentions.length > 0)
stringQuery.push(mentions.map((id) => `mentions=${id}`).join('&'));
has = has.filter((v) =>
['link', 'embed', 'file', 'video', 'image', 'sound', 'sticker'].includes(
v,
),
);
if (has.length > 0) stringQuery.push(has.map((v) => `has=${v}`).join('&'));
if (maxId) stringQuery.push(`max_id=${maxId}`); if (maxId) stringQuery.push(`max_id=${maxId}`);
if (minId) stringQuery.push(`min_id=${minId}`); if (minId) stringQuery.push(`min_id=${minId}`);
if (nsfw) stringQuery.push('include_nsfw=true'); if (nsfw) stringQuery.push('include_nsfw=true');
@@ -356,7 +413,7 @@ class MessageManager extends CachedManager {
stringQuery.push('sort_order=desc'); stringQuery.push('sort_order=desc');
} }
if (this.channel.guildId && channels.length > 0) { if (this.channel.guildId && channels.length > 0) {
stringQuery.push(channels.map(id => `channel_id=${id}`).join('&')); stringQuery.push(channels.map((id) => `channel_id=${id}`).join('&'));
} }
if (typeof pinned == 'boolean') stringQuery.push(`pinned=${pinned}`); if (typeof pinned == 'boolean') stringQuery.push(`pinned=${pinned}`);
// Main // Main
@@ -367,13 +424,22 @@ class MessageManager extends CachedManager {
}; };
} }
if (this.channel.guildId) { if (this.channel.guildId) {
data = await this.client.api.guilds[this.channel.guildId].messages[`search?${stringQuery.join('&')}`].get(); data =
await this.client.api.guilds[this.channel.guildId].messages[
`search?${stringQuery.join('&')}`
].get();
} else { } else {
stringQuery = stringQuery.filter(v => !v.startsWith('channel_id') && !v.startsWith('include_nsfw')); stringQuery = stringQuery.filter(
data = await this.client.api.channels[this.channel.id].messages[`search?${stringQuery.join('&')}`].get(); (v) => !v.startsWith('channel_id') && !v.startsWith('include_nsfw'),
);
data =
await this.client.api.channels[this.channel.id].messages[
`search?${stringQuery.join('&')}`
].get();
} }
for await (const message of data.messages) result.set(message[0].id, new Message(this.client, message[0])); for await (const message of data.messages)
result.set(message[0].id, new Message(this.client, message[0]));
return { return {
messages: result, messages: result,
total: data.total_results, total: data.total_results,
@@ -381,9 +447,12 @@ class MessageManager extends CachedManager {
} }
async _fetchMany(options = {}, cache) { async _fetchMany(options = {}, cache) {
const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); const data = await this.client.api.channels[this.channel.id].messages.get({
query: options,
});
const messages = new Collection(); const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache)); for (const message of data)
messages.set(message.id, this._add(message, cache));
return messages; return messages;
} }
@@ -393,7 +462,10 @@ class MessageManager extends CachedManager {
* @returns {Promise<Message>} * @returns {Promise<Message>}
*/ */
async endPoll(messageId) { async endPoll(messageId) {
const message = await this.client.api.channels(this.channel.id).polls(messageId).expire.post(); const message = await this.client.api
.channels(this.channel.id)
.polls(messageId)
.expire.post();
return this._add(message, false); return this._add(message, false);
} }
@@ -410,11 +482,18 @@ class MessageManager extends CachedManager {
* @returns {Promise<Collection<Snowflake, User>>} * @returns {Promise<Collection<Snowflake, User>>}
*/ */
async fetchPollAnswerVoters({ messageId, answerId, after, limit }) { async fetchPollAnswerVoters({ messageId, answerId, after, limit }) {
const voters = await this.client.channels(this.channel.id).polls(messageId).answers(answerId).get({ const voters = await this.client
.channels(this.channel.id)
.polls(messageId)
.answers(answerId)
.get({
query: { limit, after }, query: { limit, after },
}); });
return voters.users.reduce((acc, user) => acc.set(user.id, this.client.users._add(user, false)), new Collection()); return voters.users.reduce(
(acc, user) => acc.set(user.id, this.client.users._add(user, false)),
new Collection(),
);
} }
} }

View File

@@ -64,7 +64,12 @@ class PermissionOverwriteManager extends CachedManager {
*/ */
async set(overwrites, reason) { async set(overwrites, reason) {
if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) { if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) {
throw new TypeError('INVALID_TYPE', 'overwrites', 'Array or Collection of Permission Overwrites', true); throw new TypeError(
'INVALID_TYPE',
'overwrites',
'Array or Collection of Permission Overwrites',
true,
);
} }
return this.channel.edit({ permissionOverwrites: overwrites, reason }); return this.channel.edit({ permissionOverwrites: overwrites, reason });
} }
@@ -87,15 +92,26 @@ class PermissionOverwriteManager extends CachedManager {
* @private * @private
*/ */
async upsert(userOrRole, options, overwriteOptions = {}, existing) { async upsert(userOrRole, options, overwriteOptions = {}, existing) {
let userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole); let userOrRoleId =
this.channel.guild.roles.resolveId(userOrRole) ??
this.client.users.resolveId(userOrRole);
let { type, reason } = overwriteOptions; let { type, reason } = overwriteOptions;
if (typeof type !== 'number') { if (typeof type !== 'number') {
userOrRole = this.channel.guild.roles.resolve(userOrRole) ?? this.client.users.resolve(userOrRole); userOrRole =
if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'); this.channel.guild.roles.resolve(userOrRole) ??
type = userOrRole instanceof Role ? OverwriteTypes.role : OverwriteTypes.member; this.client.users.resolve(userOrRole);
if (!userOrRole)
throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
type =
userOrRole instanceof Role
? OverwriteTypes.role
: OverwriteTypes.member;
} }
const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options, existing); const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(
options,
existing,
);
await this.client.api await this.client.api
.channels(this.channel.id) .channels(this.channel.id)
@@ -141,7 +157,8 @@ class PermissionOverwriteManager extends CachedManager {
*/ */
edit(userOrRole, options, overwriteOptions) { edit(userOrRole, options, overwriteOptions) {
const existing = this.cache.get( const existing = this.cache.get(
this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole), this.channel.guild.roles.resolveId(userOrRole) ??
this.client.users.resolveId(userOrRole),
); );
return this.upsert(userOrRole, options, overwriteOptions, existing); return this.upsert(userOrRole, options, overwriteOptions, existing);
} }
@@ -153,10 +170,16 @@ class PermissionOverwriteManager extends CachedManager {
* @returns {Promise<GuildChannel>} * @returns {Promise<GuildChannel>}
*/ */
async delete(userOrRole, reason) { async delete(userOrRole, reason) {
const userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole); const userOrRoleId =
if (!userOrRoleId) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'); this.channel.guild.roles.resolveId(userOrRole) ??
this.client.users.resolveId(userOrRole);
if (!userOrRoleId)
throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
await this.client.api.channels(this.channel.id).permissions(userOrRoleId).delete({ reason }); await this.client.api
.channels(this.channel.id)
.permissions(userOrRoleId)
.delete({ reason });
return this.channel; return this.channel;
} }
} }

View File

@@ -61,7 +61,7 @@ class PresenceManager extends CachedManager {
async fetch() { async fetch() {
const data = await this.client.api.presences.get(); const data = await this.client.api.presences.get();
// https://docs.discord.food/resources/presence#endpoints // https://docs.discord.food/resources/presence#endpoints
data.presences.forEach(presence => { data.presences.forEach((presence) => {
this._add(presence, true); this._add(presence, true);
}); });
return this.cache; return this.cache;

View File

@@ -19,7 +19,10 @@ class ReactionManager extends CachedManager {
} }
_add(data, cache) { _add(data, cache) {
return super._add(data, cache, { id: data.emoji.id ?? data.emoji.name, extras: [this.message] }); return super._add(data, cache, {
id: data.emoji.id ?? data.emoji.name,
extras: [this.message],
});
} }
/** /**
@@ -59,7 +62,10 @@ class ReactionManager extends CachedManager {
* @returns {Promise<Message>} * @returns {Promise<Message>}
*/ */
async removeAll() { async removeAll() {
await this.client.api.channels(this.message.channelId).messages(this.message.id).reactions.delete(); await this.client.api
.channels(this.message.channelId)
.messages(this.message.id)
.reactions.delete();
return this.message; return this.message;
} }
} }

View File

@@ -42,9 +42,15 @@ class ReactionUserManager extends CachedManager {
*/ */
async fetch({ limit = 100, after, type = 'NORMAL' } = {}) { async fetch({ limit = 100, after, type = 'NORMAL' } = {}) {
const message = this.reaction.message; const message = this.reaction.message;
const data = await this.client.api.channels[message.channelId].messages[message.id].reactions[ const data = await this.client.api.channels[message.channelId].messages[
this.reaction.emoji.identifier message.id
].get({ query: { limit, after, type: typeof type == 'number' ? type : ReactionTypes[type] } }); ].reactions[this.reaction.emoji.identifier].get({
query: {
limit,
after,
type: typeof type == 'number' ? type : ReactionTypes[type],
},
});
const users = new Collection(); const users = new Collection();
for (const rawUser of data) { for (const rawUser of data) {
const user = this.client.users._add(rawUser); const user = this.client.users._add(rawUser);
@@ -63,7 +69,9 @@ class ReactionUserManager extends CachedManager {
const userId = this.client.users.resolveId(user); const userId = this.client.users.resolveId(user);
if (!userId) throw new Error('REACTION_RESOLVE_USER'); if (!userId) throw new Error('REACTION_RESOLVE_USER');
const message = this.reaction.message; const message = this.reaction.message;
await this.client.api.channels[message.channelId].messages[message.id].reactions[this.reaction.emoji.identifier][ await this.client.api.channels[message.channelId].messages[
message.id
].reactions[this.reaction.emoji.identifier][
userId === this.client.user.id ? '@me' : userId userId === this.client.user.id ? '@me' : userId
].delete(); ].delete();
return this.reaction; return this.reaction;

View File

@@ -37,7 +37,7 @@ class RelationshipManager extends BaseManager {
*/ */
get friendCache() { get friendCache() {
const users = this.cache const users = this.cache
.filter(value => value === RelationshipTypes.FRIEND) .filter((value) => value === RelationshipTypes.FRIEND)
.map((_, key) => [key, this.client.users.cache.get(key)]); .map((_, key) => [key, this.client.users.cache.get(key)]);
return new Collection(users); return new Collection(users);
} }
@@ -49,7 +49,7 @@ class RelationshipManager extends BaseManager {
*/ */
get blockedCache() { get blockedCache() {
const users = this.cache const users = this.cache
.filter(value => value === RelationshipTypes.BLOCKED) .filter((value) => value === RelationshipTypes.BLOCKED)
.map((_, key) => [key, this.client.users.cache.get(key)]); .map((_, key) => [key, this.client.users.cache.get(key)]);
return new Collection(users); return new Collection(users);
} }
@@ -61,7 +61,7 @@ class RelationshipManager extends BaseManager {
*/ */
get incomingCache() { get incomingCache() {
const users = this.cache const users = this.cache
.filter(value => value === RelationshipTypes.PENDING_INCOMING) .filter((value) => value === RelationshipTypes.PENDING_INCOMING)
.map((_, key) => [key, this.client.users.cache.get(key)]); .map((_, key) => [key, this.client.users.cache.get(key)]);
return new Collection(users); return new Collection(users);
} }
@@ -73,7 +73,7 @@ class RelationshipManager extends BaseManager {
*/ */
get outgoingCache() { get outgoingCache() {
const users = this.cache const users = this.cache
.filter(value => value === RelationshipTypes.PENDING_OUTGOING) .filter((value) => value === RelationshipTypes.PENDING_OUTGOING)
.map((_, key) => [key, this.client.users.cache.get(key)]); .map((_, key) => [key, this.client.users.cache.get(key)]);
return new Collection(users); return new Collection(users);
} }
@@ -172,9 +172,11 @@ class RelationshipManager extends BaseManager {
// eslint-disable-next-line no-unreachable // eslint-disable-next-line no-unreachable
const id = this.resolveId(user); const id = this.resolveId(user);
if ( if (
![RelationshipTypes.FRIEND, RelationshipTypes.BLOCKED, RelationshipTypes.PENDING_OUTGOING].includes( ![
this.cache.get(id), RelationshipTypes.FRIEND,
) RelationshipTypes.BLOCKED,
RelationshipTypes.PENDING_OUTGOING,
].includes(this.cache.get(id))
) { ) {
return Promise.resolve(false); return Promise.resolve(false);
} }
@@ -222,9 +224,11 @@ class RelationshipManager extends BaseManager {
// eslint-disable-next-line no-unreachable // eslint-disable-next-line no-unreachable
const id = this.resolveId(user); const id = this.resolveId(user);
// Check if already friends // Check if already friends
if (this.cache.get(id) === RelationshipTypes.FRIEND) return Promise.resolve(false); if (this.cache.get(id) === RelationshipTypes.FRIEND)
return Promise.resolve(false);
// Check if outgoing request // Check if outgoing request
if (this.cache.get(id) === RelationshipTypes.PENDING_OUTGOING) return Promise.resolve(false); if (this.cache.get(id) === RelationshipTypes.PENDING_OUTGOING)
return Promise.resolve(false);
await this.client.api.users['@me'].relationships[id].put({ await this.client.api.users['@me'].relationships[id].put({
data: { confirm_stranger_request: true }, data: { confirm_stranger_request: true },
DiscordContext: { location: 'Friends' }, DiscordContext: { location: 'Friends' },
@@ -240,7 +244,8 @@ class RelationshipManager extends BaseManager {
*/ */
async setNickname(user, nickname = null) { async setNickname(user, nickname = null) {
const id = this.resolveId(user); const id = this.resolveId(user);
if (this.cache.get(id) !== RelationshipTypes.FRIEND) return Promise.resolve(false); if (this.cache.get(id) !== RelationshipTypes.FRIEND)
return Promise.resolve(false);
await this.client.api.users['@me'].relationships[id].patch({ await this.client.api.users['@me'].relationships[id].patch({
data: { data: {
nickname: typeof nickname === 'string' ? nickname : null, nickname: typeof nickname === 'string' ? nickname : null,
@@ -264,7 +269,8 @@ class RelationshipManager extends BaseManager {
// eslint-disable-next-line no-unreachable // eslint-disable-next-line no-unreachable
const id = this.resolveId(user); const id = this.resolveId(user);
// Check // Check
if (this.cache.get(id) === RelationshipTypes.BLOCKED) return Promise.resolve(false); if (this.cache.get(id) === RelationshipTypes.BLOCKED)
return Promise.resolve(false);
await this.client.api.users['@me'].relationships[id].put({ await this.client.api.users['@me'].relationships[id].put({
data: { data: {
type: RelationshipTypes.BLOCKED, type: RelationshipTypes.BLOCKED,

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