Compare commits

..

No commits in common. "master" and "12.1.0" have entirely different histories.

137 changed files with 1443 additions and 15291 deletions

View file

@ -1,12 +1,11 @@
{
"root": true,
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": ["import"],
"parserOptions": {
"ecmaVersion": 2020
"ecmaVersion": 2019
},
"env": {
"es2020": true,
"es6": true,
"node": true
},
"overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }],
@ -27,8 +26,7 @@
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf",
"arrowParens": "avoid"
"endOfLine": "lf"
}
],
"strict": ["error", "global"],

View file

@ -11,8 +11,8 @@ is a great boon to your development process.
To get ready to work on the codebase, please do the following:
1. Fork & clone the repository, and make sure you're on the **master** branch
2. Run `npm ci`
2. Run `npm install`
3. If you're working on voice, also run `npm install @discordjs/opus` or `npm install opusscript`
4. Code your heart out!
5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare) (Make sure you follow the [conventional commit format](https://github.com/discordjs/discord.js-next/blob/master/.github/COMMIT_CONVENTION.md))
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare)

View file

@ -28,12 +28,6 @@ You won't receive any basic help here.
- Operating system:
- Priority this issue should have please be realistic and elaborate if possible:
**Relevant client options:**
- partials: none
- gateway intents: none
- other: none
<!--
If this applies to you, please check the respective checkbox: [ ] becomes [x].
You don't have to modify the text to suit your particular situation if you want to

View file

@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Discord server
- name: discord.js discord server
url: https://discord.gg/bRCvFy9
about: Have questions or need support? Please go to the Discord server, as issues that are just support-related will be closed and redirected there.
about: Please use this Discord Server to ask questions and get support. We don't typically answer questions here and they will likely be closed and redirected to the Discord server.

18
.github/tsc.json vendored
View file

@ -1,18 +0,0 @@
{
"problemMatcher": [
{
"owner": "tsc",
"pattern": [
{
"regexp": "^(?:\\s+\\d+\\>)?([^\\s].*)\\((\\d+),(\\d+)\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}

View file

@ -1,27 +0,0 @@
name: "CodeQL"
on:
push:
pull_request:
schedule:
- cron: '0 */12 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 2
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View file

@ -15,13 +15,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@master
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@master
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Build and deploy documentation
uses: discordjs/action-docs@v1
@ -35,13 +35,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@master
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@master
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Build and deploy webpack
uses: discordjs/action-webpack@v1

View file

@ -10,13 +10,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Run ESLint
uses: icrawl/action-eslint@v1
@ -28,38 +28,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Run TSLint
run: npm run lint:typings
typescript:
name: TypeScript
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm ci
- name: Register Problem Matcher
run: echo "##[add-matcher].github/tsc.json"
- name: Run TypeScript compiler
run: npm run test:typescript
docs:
name: Documentation
runs-on: ubuntu-latest
@ -67,13 +46,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Test documentation
run: npm run docs:test

View file

@ -8,13 +8,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Run ESLint
uses: icrawl/action-eslint@v1
@ -26,38 +26,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Run TSLint
run: npm run lint:typings
typescript:
name: TypeScript
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm ci
- name: Register Problem Matcher
run: echo "##[add-matcher].github/tsc.json"
- name: Run TypeScript compiler
run: npm run test:typescript
docs:
name: Documentation
runs-on: ubuntu-latest
@ -65,13 +44,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 12
- name: Install dependencies
run: npm ci
run: npm install
- name: Test documentation
run: npm run docs:test

3
.gitignore vendored
View file

@ -1,6 +1,7 @@
# Packages
node_modules/
yarn.lock
package-lock.json
# Log files
logs/
@ -17,7 +18,5 @@ deploy/deploy_key.pub
# Miscellaneous
.tmp/
.vscode/
.idea/
docs/docs.json
typings/index.js
webpack/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-lock=false

View file

@ -5,7 +5,7 @@
</p>
<br />
<p>
<a href="https://discord.gg/bRCvFy9"><img src="https://img.shields.io/discord/222078108977594368?color=7289da&logo=discord&logoColor=white" alt="Discord server" /></a>
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/workflows/Testing/badge.svg" alt="Build status" /></a>
@ -19,7 +19,6 @@
## Table of contents
- [Changes](#changes)
- [About](#about)
- [Installation](#installation)
- [Audio engines](#audio-engines)
@ -30,14 +29,10 @@
- [Contributing](#contributing)
- [Help](#help)
## Changes
This fork has the inline replies and slash commands for **testing** purposes.
## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
[Discord API](https://discord.com/developers/docs/intro).
[Discord API](https://discordapp.com/developers/docs/intro).
- Object-oriented
- Predictable abstractions
@ -46,7 +41,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation
**Node.js 14.0.0 or newer is required.**
**Node.js 12.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
@ -62,7 +57,7 @@ For production bots, using @discordjs/opus should be considered a necessity, esp
### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/erlpack`)
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
@ -81,7 +76,7 @@ client.on('ready', () => {
client.on('message', msg => {
if (msg.content === 'ping') {
msg.channel.send('pong');
msg.reply('pong');
}
});

View file

@ -33,7 +33,7 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
@ -68,7 +68,7 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
@ -105,13 +105,13 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
The results are the same as the URL examples:
![Image showing result](/static/attachment-example2.png)
![Image showing result](/static/attachment-example1.png)
But what if you have a buffer from an image? Or a text document? Well, it's the same as sending a local file or a URL!
@ -154,7 +154,7 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```

View file

@ -23,9 +23,9 @@ client.on('message', message => {
// If the message is "what is my avatar"
if (message.content === 'what is my avatar') {
// Send the user's avatar URL
message.channel.send(message.author.displayAvatarURL());
message.reply(message.author.displayAvatarURL());
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

View file

@ -36,5 +36,5 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

View file

@ -28,5 +28,5 @@ client.on('guildMemberAdd', member => {
channel.send(`Welcome to the server, ${member}`);
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

View file

@ -33,7 +33,7 @@ client.on('message', message => {
// If we have a user mentioned
if (user) {
// Now we get the member from the user
const member = message.guild.members.resolve(user);
const member = message.guild.member(user);
// If the member is in the guild
if (member) {
/**
@ -45,28 +45,28 @@ client.on('message', message => {
.kick('Optional reason that will display in the audit logs')
.then(() => {
// We let the message author know we were able to kick the person
message.channel.send(`Successfully kicked ${user.tag}`);
message.reply(`Successfully kicked ${user.tag}`);
})
.catch(err => {
// An error happened
// This is generally due to the bot not being able to kick the member,
// either due to missing permissions or role hierarchy
message.channel.send('I was unable to kick the member');
message.reply('I was unable to kick the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.channel.send("That user isn't in this guild!");
message.reply("That user isn't in this guild!");
}
// Otherwise, if no user was mentioned
} else {
message.channel.send("You didn't mention the user to kick!");
message.reply("You didn't mention the user to kick!");
}
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
@ -105,7 +105,7 @@ client.on('message', message => {
// If we have a user mentioned
if (user) {
// Now we get the member from the user
const member = message.guild.members.resolve(user);
const member = message.guild.member(user);
// If the member is in the guild
if (member) {
/**
@ -121,28 +121,28 @@ client.on('message', message => {
})
.then(() => {
// We let the message author know we were able to ban the person
message.channel.send(`Successfully banned ${user.tag}`);
message.reply(`Successfully banned ${user.tag}`);
})
.catch(err => {
// An error happened
// This is generally due to the bot not being able to ban the member,
// either due to missing permissions or role hierarchy
message.channel.send('I was unable to ban the member');
message.reply('I was unable to ban the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.channel.send("That user isn't in this guild!");
message.reply("That user isn't in this guild!");
}
} else {
// Otherwise, if no user was mentioned
message.channel.send("You didn't mention the user to ban!");
message.reply("You didn't mention the user to ban!");
}
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```

View file

@ -27,5 +27,5 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discord.com/developers/applications
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

View file

@ -6,13 +6,8 @@
// Import the discord.js module
const Discord = require('discord.js');
/*
* Create a new webhook
* The Webhooks ID and token can be found in the URL, when you request that URL, or in the response body.
* https://discord.com/api/webhooks/12345678910/T0kEn0fw3Bh00K
* ^^^^^^^^^^ ^^^^^^^^^^^^
* Webhook ID Webhook Token
*/
// Create a new webhook
const hook = new Discord.WebhookClient('webhook id', 'webhook token');
// Send a message using the webhook

View file

@ -4,7 +4,7 @@ These questions are some of the most frequently asked.
## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`
Update to Node.js 14.0.0 or newer.
Update to Node.js 12.0.0 or newer.
## How do I get voice working?

View file

@ -5,7 +5,7 @@
</p>
<br />
<p>
<a href="https://discord.gg/bRCvFy9"><img src="https://img.shields.io/discord/222078108977594368?color=7289da&logo=discord&logoColor=white" alt="Discord server" /></a>
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://travis-ci.org/discordjs/discord.js"><img src="https://travis-ci.org/discordjs/discord.js.svg" alt="Build status" /></a>
@ -24,7 +24,7 @@ Welcome to the discord.js v12 documentation.
## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
[Discord API](https://discord.com/developers/docs/intro).
[Discord API](https://discordapp.com/developers/docs/intro).
- Object-oriented
- Predictable abstractions
@ -33,7 +33,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation
**Node.js 14.0.0 or newer is required.**
**Node.js 12.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
@ -49,7 +49,7 @@ For production bots, using @discordjs/opus should be considered a necessity, esp
### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/erlpack`)
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
@ -68,7 +68,7 @@ client.on('ready', () => {
client.on('message', msg => {
if (msg.content === 'ping') {
msg.channel.send('pong');
msg.reply('pong');
}
});

View file

@ -37,7 +37,7 @@ client.on('message', async message => {
if (message.member.voice.channel) {
const connection = await message.member.voice.channel.join();
} else {
message.channel.send('You need to join a voice channel first!');
message.reply('You need to join a voice channel first!');
}
}
});

View file

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

11518
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,17 @@
{
"name": "discord.js",
"version": "12.5.0",
"version": "12.1.0",
"description": "A powerful library for interacting with the Discord API",
"main": "./src/index",
"types": "./typings/index.d.ts",
"exports": {
".": [
{
"require": "./src/index.js",
"import": "./esm/discord.mjs"
},
"./src/index.js"
],
"./esm": "./esm/discord.mjs"
},
"scripts": {
"test": "npm run lint && npm run docs:test && npm run lint:typings",
"test:typescript": "tsc",
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:typings": "tslint typings/index.d.ts",
"prettier": "prettier --write src/**/*.js typings/**/*.ts",
"prettier": "prettier --write --single-quote --print-width 120 --trailing-comma all --end-of-line lf src/**/*.js typings/**/*.ts",
"build:browser": "webpack",
"prepublishOnly": "npm run test && cross-env NODE_ENV=production npm run build:browser"
},
@ -47,40 +36,68 @@
"runkitExampleFilename": "./docs/examples/ping.js",
"unpkg": "./webpack/discord.min.js",
"dependencies": {
"@discordjs/collection": "^0.1.6",
"@discordjs/form-data": "^3.0.1",
"@discordjs/collection": "^0.1.5",
"abort-controller": "^3.0.0",
"node-fetch": "^2.6.1",
"prism-media": "^1.2.2",
"form-data": "^3.0.0",
"node-fetch": "^2.6.0",
"prism-media": "^1.2.0",
"setimmediate": "^1.0.5",
"tweetnacl": "^1.0.3",
"ws": "^7.3.1"
"ws": "^7.2.1"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"erlpack": "discordapp/erlpack",
"libsodium-wrappers": "^0.7.6",
"sodium": "^3.0.2",
"utf-8-validate": "^5.0.2",
"zlib-sync": "^0.1.6"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"erlpack": {
"optional": true
},
"libsodium-wrappers": {
"optional": true
},
"sodium": {
"optional": true
},
"utf-8-validate": {
"optional": true
},
"zlib-sync": {
"optional": true
}
},
"devDependencies": {
"@commitlint/cli": "^11.0.0",
"@commitlint/config-angular": "^11.0.0",
"@types/node": "^12.12.6",
"@types/ws": "^7.2.7",
"@commitlint/cli": "^8.3.5",
"@commitlint/config-angular": "^8.3.4",
"@types/node": "^10.12.24",
"@types/ws": "^7.2.1",
"cross-env": "^7.0.2",
"discord.js-docgen": "git+https://github.com/discordjs/docgen.git",
"dtslint": "^4.0.4",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.3.0",
"jest": "^26.6.0",
"discord.js-docgen": "discordjs/docgen",
"dtslint": "^3.0.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.2",
"husky": "^4.2.3",
"jest": "^25.1.0",
"json-filter-loader": "^1.0.0",
"lint-staged": "^10.4.2",
"prettier": "^2.1.2",
"terser-webpack-plugin": "^4.2.3",
"tslint": "^6.1.3",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
"lint-staged": "^10.0.8",
"prettier": "^1.19.1",
"terser-webpack-plugin": "^1.2.2",
"tslint": "^6.0.0",
"typescript": "^3.8.2",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11"
},
"engines": {
"node": ">=14.0.0"
"node": ">=12.0.0"
},
"browser": {
"@discordjs/opus": false,
@ -110,9 +127,9 @@
"src/client/voice/receiver/PacketHandler.js": false,
"src/client/voice/receiver/Receiver.js": false,
"src/client/voice/util/PlayInterface.js": false,
"src/client/voice/util/Secretbox.js": false,
"src/client/voice/util/Silence.js": false,
"src/client/voice/util/VolumeInterface.js": false,
"src/util/Sodium.js": false
"src/client/voice/util/VolumeInterface.js": false
},
"husky": {
"hooks": {
@ -122,7 +139,7 @@
},
"lint-staged": {
"*.js": "eslint --fix",
"*.ts": "prettier --write"
"*.ts": "prettier --write --single-quote --print-width 120 --trailing-comma all --end-of-line lf"
},
"commitlint": {
"extends": [
@ -152,12 +169,5 @@
]
]
}
},
"prettier": {
"singleQuote": true,
"printWidth": 120,
"trailingComma": "all",
"endOfLine": "lf",
"arrowParens": "avoid"
}
}

View file

@ -139,28 +139,6 @@ class BaseClient extends EventEmitter {
this._immediates.delete(immediate);
}
/**
* Increments max listeners by one, if they are not zero.
* @private
*/
incrementMaxListeners() {
const maxListeners = this.getMaxListeners();
if (maxListeners !== 0) {
this.setMaxListeners(maxListeners + 1);
}
}
/**
* Decrements max listeners by one, if they are not zero.
* @private
*/
decrementMaxListeners() {
const maxListeners = this.getMaxListeners();
if (maxListeners !== 0) {
this.setMaxListeners(maxListeners - 1);
}
}
toJSON(...props) {
return Util.flatten(this, { domain: false }, ...props);
}

View file

@ -1,19 +1,17 @@
'use strict';
const BaseClient = require('./BaseClient');
const InteractionClient = require('./InteractionClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
const { Error, TypeError, RangeError } = require('../errors');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const ChannelManager = require('../managers/ChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildManager = require('../managers/GuildManager');
const UserManager = require('../managers/UserManager');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const ClientApplication = require('../structures/ClientApplication');
const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
@ -104,12 +102,6 @@ class Client extends BaseClient {
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
: null;
/**
* The interaction client.
* @type {InteractionClient}
*/
this.interactionClient = new InteractionClient(options, this);
/**
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {UserManager}
@ -143,8 +135,7 @@ class Client extends BaseClient {
Object.defineProperty(this, 'token', { writable: true });
if (!browser && !this.token && 'DISCORD_TOKEN' in process.env) {
/**
* Authorization token for the logged in bot.
* If present, this defaults to `process.env.DISCORD_TOKEN` when instantiating the client
* Authorization token for the logged in bot
* <warn>This should be kept private at all times.</warn>
* @type {?string}
*/
@ -173,11 +164,11 @@ class Client extends BaseClient {
/**
* All custom emojis that the client has access to, mapped by their IDs
* @type {BaseGuildEmojiManager}
* @type {GuildEmojiManager}
* @readonly
*/
get emojis() {
const emojis = new BaseGuildEmojiManager(this);
const emojis = new GuildEmojiManager({ client: this });
for (const guild of this.guilds.cache.values()) {
if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji);
}
@ -204,7 +195,7 @@ class Client extends BaseClient {
/**
* Logs the client in, establishing a websocket connection to Discord.
* @param {string} [token=this.token] Token of the account to log in with
* @param {string} token Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token');
@ -262,23 +253,6 @@ class Client extends BaseClient {
.then(data => new Invite(this, data));
}
/**
* Obtains a template from Discord.
* @param {GuildTemplateResolvable} template Template code or URL
* @returns {Promise<GuildTemplate>}
* @example
* client.fetchGuildTemplate('https://discord.new/FKvmczH2HyUf')
* .then(template => console.log(`Obtained template with code: ${template.code}`))
* .catch(console.error);
*/
fetchGuildTemplate(template) {
const code = DataResolver.resolveGuildTemplateCode(template);
return this.api.guilds
.templates(code)
.get()
.then(data => new GuildTemplate(this, data));
}
/**
* Obtains a webhook from Discord.
* @param {Snowflake} id ID of the webhook
@ -366,7 +340,7 @@ class Client extends BaseClient {
}
/**
* Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds.
* Obtains a guild preview from Discord, only available for public guilds.
* @param {GuildResolvable} guild The guild to fetch the preview for
* @returns {Promise<GuildPreview>}
*/
@ -381,43 +355,28 @@ class Client extends BaseClient {
/**
* Generates a link that can be used to invite the bot to a guild.
* @param {InviteGenerationOptions|PermissionResolvable} [options] Permissions to request
* @param {PermissionResolvable} [permissions] Permissions to request
* @returns {Promise<string>}
* @example
* client.generateInvite({
* permissions: ['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'],
* })
* client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'])
* .then(link => console.log(`Generated bot invite link: ${link}`))
* .catch(console.error);
*/
async generateInvite(options = {}) {
if (Array.isArray(options) || ['string', 'number'].includes(typeof options) || options instanceof Permissions) {
process.emitWarning(
'Client#generateInvite: Generate invite with an options object instead of a PermissionResolvable',
'DeprecationWarning',
);
options = { permissions: options };
}
async generateInvite(permissions) {
permissions = Permissions.resolve(permissions);
const application = await this.fetchApplication();
const query = new URLSearchParams({
client_id: application.id,
permissions: Permissions.resolve(options.permissions),
permissions: permissions,
scope: 'bot',
});
if (typeof options.disableGuildSelect === 'boolean') {
query.set('disable_guild_select', options.disableGuildSelect.toString());
}
if (typeof options.guild !== 'undefined') {
const guildID = this.guilds.resolveID(options.guild);
if (!guildID) throw new TypeError('INVALID_TYPE', 'options.guild', 'GuildResolvable');
query.set('guild_id', guildID);
}
return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
}
toJSON() {
return super.toJSON({
readyAt: false,
presences: false,
});
}
@ -457,13 +416,6 @@ class Client extends BaseClient {
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number');
}
if (
typeof options.messageEditHistoryMaxSize !== 'number' ||
isNaN(options.messageEditHistoryMaxSize) ||
options.messageEditHistoryMaxSize < -1
) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageEditHistoryMaxSize', 'a number greater than or equal to -1');
}
if (typeof options.fetchAllMembers !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean');
}
@ -490,14 +442,6 @@ class Client extends BaseClient {
module.exports = Client;
/**
* Options for {@link Client#generateInvite}.
* @typedef {Object} InviteGenerationOptions
* @property {PermissionResolvable} [permissions] Permissions to request
* @property {GuildResolvable} [guild] Guild to preselect
* @property {boolean} [disableGuildSelect] Whether to disable the guild selection
*/
/**
* Emitted for general warnings.
* @event Client#warn

View file

@ -1,207 +0,0 @@
'use strict';
const BaseClient = require('./BaseClient');
const ApplicationCommand = require('../structures/ApplicationCommand');
const Interaction = require('../structures/Interaction');
const { Events, ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants');
let sodium;
/**
* Interaction client is used for interactions.
*
* @example
* const client = new InteractionClient({
* token: ABC,
* publicKey: XYZ,
* });
*
* client.on('interactionCreate', () => {
* // automatically handles long responses
* if (will take a long time) {
* doSomethingLong.then((d) => {
* interaction.reply({
* content: 'wow that took long',
* });
* });
* } else {
* interaction.reply('hi!');
* }
* });
* ```
*/
class InteractionClient extends BaseClient {
/**
* @param {Options} options Options for the client.
* @param {undefined} client For internal use.
*/
constructor(options, client) {
super(options);
Object.defineProperty(this, 'token', {
value: options.token,
writable: true,
});
Object.defineProperty(this, 'clientID', {
value: options.clientID,
writable: true,
});
Object.defineProperty(this, 'publicKey', {
value: options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined,
writable: true,
});
// Compat for direct usage
this.client = client || this;
this.interactionClient = this;
}
/**
* Get registered slash commands.
* @param {Snowflake} [guildID] Optional guild ID.
* @returns {Command[]}
*/
async getCommands(guildID) {
let path = this.client.api.applications('@me');
if (guildID) {
path = path.guilds(guildID);
}
const commands = await path.commands.get();
return commands.map(c => new ApplicationCommand(this, c, guildID));
}
/**
* Create a command.
* @param {Object} command The command description.
* @param {Snowflake?} guildID Optional guild ID.
* @returns {Promise<ApplicationCommand>} The created command.
*/
async createCommand(command, guildID) {
let path = this.client.api.applications(this.client.user.id);
if (guildID) {
path = path.guilds(guildID);
}
const c = await path.commands.post({
data: {
name: command.name,
description: command.description,
options: command.options.map(function m(o) {
return {
type: ApplicationCommandOptionType[o.type],
name: o.name,
description: o.description,
default: o.default,
required: o.required,
choices: o.choices,
options: o.options ? o.options.map(m) : undefined,
};
}),
},
});
return new ApplicationCommand(this, c, guildID);
}
handle(data) {
switch (data.type) {
case InteractionType.PING:
return {
type: InteractionResponseType.PONG,
};
case InteractionType.APPLICATION_COMMAND: {
let timedOut = false;
let resolve;
const directPromise = new Promise(r => {
resolve = r;
this.client.setTimeout(() => {
timedOut = true;
r({
type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE,
});
}, 250);
});
const syncHandle = {
acknowledge() {
if (!timedOut) {
resolve({
type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE,
});
}
},
reply(resolved) {
if (timedOut) {
return false;
}
resolve({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: resolved.data,
});
return true;
},
};
const interaction = new Interaction(this.client, data, syncHandle);
/**
* Emitted when an interaction is created.
* @event Client#interactionCreate
* @param {Interaction} interaction The interaction which was created.
*/
this.client.emit(Events.INTERACTION_CREATE, interaction);
return directPromise;
}
default:
throw new RangeError('Invalid interaction data');
}
}
/**
* An express-like middleware factory which can be used
* with webhook interactions.
* @returns {Function} The middleware function.
*/
middleware() {
return async (req, res) => {
const timestamp = req.get('x-signature-timestamp');
const signature = req.get('x-signature-ed25519');
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const body = Buffer.concat(chunks);
if (sodium === undefined) {
sodium = require('../util/Sodium');
}
if (
!sodium.methods.verify(
Buffer.from(signature, 'hex'),
Buffer.concat([Buffer.from(timestamp), body]),
this.publicKey,
)
) {
res.status(403).end();
return;
}
const data = JSON.parse(body.toString());
const result = await this.handle(data);
res.status(200).end(JSON.stringify(result));
};
}
async handleFromGateway(data) {
const result = await this.handle(data);
await this.client.api.interactions(data.id, data.token).callback.post({
data: result,
});
}
}
module.exports = InteractionClient;

View file

@ -81,25 +81,23 @@ class GenericAction {
}
getMember(data, guild) {
return this.getPayload(data, guild.members, data.user.id, PartialTypes.GUILD_MEMBER);
const id = data.user.id;
return this.getPayload(
{
user: {
id,
},
},
guild.members,
id,
PartialTypes.GUILD_MEMBER,
);
}
getUser(data) {
const id = data.user_id;
return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER);
}
getUserFromMember(data) {
if (data.guild_id && data.member && data.member.user) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (guild) {
return guild.members.add(data.member).user;
} else {
return this.client.users.add(data.member.user);
}
}
return this.getUser(data);
}
}
module.exports = GenericAction;

View file

@ -20,7 +20,6 @@ class ActionsManager {
this.register(require('./InviteCreate'));
this.register(require('./InviteDelete'));
this.register(require('./GuildMemberRemove'));
this.register(require('./GuildMemberUpdate'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
@ -36,8 +35,6 @@ class ActionsManager {
this.register(require('./GuildChannelsPositionUpdate'));
this.register(require('./GuildIntegrationsUpdate'));
this.register(require('./WebhooksUpdate'));
this.register(require('./TypingStart'));
this.register(require('./InteractionCreate'));
}
register(Action) {

View file

@ -5,14 +5,13 @@ const { Events } = require('../../util/Constants');
class GuildEmojiCreateAction extends Action {
handle(guild, createdEmoji) {
const already = guild.emojis.cache.has(createdEmoji.id);
const emoji = guild.emojis.add(createdEmoji);
/**
* Emitted whenever a custom emoji is created in a guild.
* @event Client#emojiCreate
* @param {GuildEmoji} emoji The emoji that was created
*/
if (!already) this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
return { emoji };
}
}

View file

@ -9,7 +9,7 @@ class GuildMemberRemoveAction extends Action {
const guild = client.guilds.cache.get(data.guild_id);
let member = null;
if (guild) {
member = this.getMember({ user: data.user }, guild);
member = this.getMember(data, guild);
guild.memberCount--;
if (member) {
member.deleted = true;

View file

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

View file

@ -1,15 +0,0 @@
'use strict';
const Action = require('./Action');
class InteractionCreateAction extends Action {
handle(data) {
this.client.interactionClient.handleFromGateway(data).catch(e => {
this.client.emit('error', e);
});
return {};
}
}
module.exports = InteractionCreateAction;

View file

@ -9,7 +9,7 @@ class InviteCreateAction extends Action {
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
const guild = client.guilds.cache.get(data.guild_id);
if (!channel) return false;
if (!channel && !guild) return false;
const inviteData = Object.assign(data, { channel, guild });
const invite = new Invite(client, inviteData);

View file

@ -8,17 +8,14 @@ const { PartialTypes } = require('../../util/Constants');
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id',
// If originating from a guild
guild_id: 'id',
member: { ..., user: { ... } } }
channel_id: 'id' } }
*/
class MessageReactionAdd extends Action {
handle(data) {
if (!data.emoji) return false;
const user = this.getUserFromMember(data);
const user = this.getUser(data);
if (!user) return false;
// Verify channel
@ -31,8 +28,6 @@ class MessageReactionAdd extends Action {
// Verify reaction
if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false;
const existing = message.reactions.cache.get(data.emoji.id || data.emoji.name);
if (existing && existing.users.cache.has(user.id)) return { message, reaction: existing, user };
const reaction = message.reactions.add({
emoji: data.emoji,
count: message.partial ? null : 0,

View file

@ -7,8 +7,7 @@ const { Events } = require('../../util/Constants');
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id',
guild_id: 'id' }
channel_id: 'id' } }
*/
class MessageReactionRemove extends Action {

View file

@ -9,9 +9,9 @@ class MessageUpdateAction extends Action {
const { id, channel_id, guild_id, author, timestamp, type } = data;
const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel);
if (message) {
const old = message.patch(data);
message.patch(data);
return {
old,
old: message._edits[0],
updated: message,
};
}

View file

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

View file

@ -13,7 +13,6 @@ class UserUpdateAction extends Action {
if (!oldUser.equals(newUser)) {
/**
* Emitted whenever a user's details (e.g. username) are changed.
* Triggered by the Discord gateway events USER_UPDATE, GUILD_MEMBER_UPDATE, and PRESENCE_UPDATE.
* @event Client#userUpdate
* @param {User} oldUser The user before the update
* @param {User} newUser The user after the update

View file

@ -1,15 +1,14 @@
'use strict';
const Action = require('./Action');
const VoiceState = require('../../structures/VoiceState');
const { Events } = require('../../util/Constants');
const Structures = require('../../util/Structures');
class VoiceStateUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const VoiceState = Structures.get('VoiceState');
// Update the state
const oldState = guild.voiceStates.cache.has(data.user_id)
? guild.voiceStates.cache.get(data.user_id)._clone()

View file

@ -18,7 +18,6 @@ const { Events } = require('../../util/Constants');
* }
* ```
* @implements {PlayInterface}
* @extends {EventEmitter}
*/
class VoiceBroadcast extends EventEmitter {
constructor(client) {

View file

@ -144,6 +144,7 @@ class VoiceConnection extends EventEmitter {
/**
* Sets whether the voice connection should display as "speaking", "soundshare" or "none".
* @param {BitFieldResolvable} value The new speaking state
* @private
*/
setSpeaking(value) {
if (this.speaking.equals(value)) return;
@ -165,7 +166,7 @@ class VoiceConnection extends EventEmitter {
/**
* The voice state of this connection
* @type {?VoiceState}
* @type {VoiceState}
*/
get voice() {
return this.channel.guild.voice;
@ -203,8 +204,8 @@ class VoiceConnection extends EventEmitter {
* Set the token and endpoint required to connect to the voice servers.
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @returns {void}
* @private
* @returns {void}
*/
setTokenAndEndpoint(token, endpoint) {
this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`);
@ -473,11 +474,7 @@ class VoiceConnection extends EventEmitter {
}
onStartSpeaking({ user_id, ssrc, speaking }) {
this.ssrcMap.set(+ssrc, {
...(this.ssrcMap.get(+ssrc) || {}),
userID: user_id,
speaking: speaking,
});
this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking });
}
/**
@ -505,7 +502,7 @@ class VoiceConnection extends EventEmitter {
}
if (guild && user && !speaking.equals(old)) {
const member = guild.members.resolve(user);
const member = guild.member(user);
if (member) {
/**
* Emitted once a guild member changes speaking state.

View file

@ -1,7 +1,7 @@
'use strict';
const { Writable } = require('stream');
const secretbox = require('../../../util/Sodium');
const secretbox = require('../util/Secretbox');
const Silence = require('../util/Silence');
const VolumeInterface = require('../util/VolumeInterface');
@ -56,7 +56,7 @@ class StreamDispatcher extends Writable {
* The broadcast controlling this dispatcher, if any
* @type {?VoiceBroadcast}
*/
this.broadcast = this.streams.broadcast || null;
this.broadcast = this.streams.broadcast;
this._pausedTime = 0;
this._silentPausedTime = 0;

View file

@ -189,11 +189,7 @@ class VoiceWebSocket extends EventEmitter {
this.emit('sessionDescription', packet.d);
break;
case VoiceOPCodes.CLIENT_CONNECT:
this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
userID: packet.d.user_id,
speaking: 0,
hasVideo: Boolean(packet.d.video_ssrc),
});
this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id);
break;
case VoiceOPCodes.CLIENT_DISCONNECT:
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);

View file

@ -1,9 +1,7 @@
'use strict';
const EventEmitter = require('events');
const sodium = require('../../../util/Sodium');
const Speaking = require('../../../util/Speaking');
const { SILENCE_FRAME } = require('../util/Silence');
const secretbox = require('../util/Secretbox');
// The delay between packets when a user is considered to have stopped speaking
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
@ -58,7 +56,7 @@ class PacketHandler extends EventEmitter {
}
// Open packet
let packet = sodium.methods.open(buffer.slice(12, end), this.nonce, secret_key);
let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key);
if (!packet) return new Error('Failed to decrypt voice packet');
packet = Buffer.from(packet);
@ -86,31 +84,8 @@ class PacketHandler extends EventEmitter {
const userStat = this.connection.ssrcMap.get(ssrc);
if (!userStat) return;
let opusPacket;
const streamInfo = this.streams.get(userStat.userID);
// If the user is in video, we need to check if the packet is just silence
if (userStat.hasVideo) {
opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
// Only emit an error if we were actively receiving packets from this user
if (streamInfo) {
this.emit('error', opusPacket);
return;
}
}
if (SILENCE_FRAME.equals(opusPacket)) {
// If this is a silence frame, pretend we never received it
return;
}
}
let speakingTimeout = this.speakingTimeouts.get(ssrc);
if (typeof speakingTimeout === 'undefined') {
// Ensure at least the speaking bit is set.
// As the object is by reference, it's only needed once per client re-connect.
if (userStat.speaking === 0) {
userStat.speaking = Speaking.FLAGS.SPEAKING;
}
this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking });
speakingTimeout = this.receiver.connection.client.setTimeout(() => {
try {
@ -126,17 +101,15 @@ class PacketHandler extends EventEmitter {
speakingTimeout.refresh();
}
if (streamInfo) {
const { stream } = streamInfo;
if (!opusPacket) {
opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
this.emit('error', opusPacket);
return;
}
}
stream.push(opusPacket);
let stream = this.streams.get(userStat.userID);
if (!stream) return;
stream = stream.stream;
const opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
this.emit('error', opusPacket);
return;
}
stream.push(opusPacket);
}
}

View file

@ -5,19 +5,16 @@ const libs = {
open: sodium.api.crypto_secretbox_open_easy,
close: sodium.api.crypto_secretbox_easy,
random: n => sodium.randombytes_buf(n),
verify: sodium.api.crypto_sign_verify_detached,
}),
'libsodium-wrappers': sodium => ({
open: sodium.crypto_secretbox_open_easy,
close: sodium.crypto_secretbox_easy,
random: n => sodium.randombytes_buf(n),
verify: sodium.crypto_sign_verify_detached,
}),
tweetnacl: tweetnacl => ({
open: tweetnacl.secretbox.open,
close: tweetnacl.secretbox,
random: n => tweetnacl.randomBytes(n),
verify: (s, d, p) => tweetnacl.sign.detached.verify(d, s, p),
}),
};

View file

@ -10,6 +10,4 @@ class Silence extends Readable {
}
}
Silence.SILENCE_FRAME = SILENCE_FRAME;
module.exports = Silence;

View file

@ -18,13 +18,15 @@ const BeforeReadyWhitelist = [
WSEvents.GUILD_MEMBER_REMOVE,
];
const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1).map(Number);
const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes)
.slice(1)
.map(Number);
const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007];
/**
* The WebSocket manager for this client.
* <info>This class forwards raw dispatch events,
* read more about it here {@link https://discord.com/developers/docs/topics/gateway}</info>
* read more about it here {@link https://discordapp.com/developers/docs/topics/gateway}</info>
* @extends EventEmitter
*/
class WebSocketManager extends EventEmitter {
@ -43,7 +45,7 @@ class WebSocketManager extends EventEmitter {
* The gateway this manager uses
* @type {?string}
*/
this.gateway = null;
this.gateway = undefined;
/**
* The amount of shards this manager handles
@ -76,7 +78,7 @@ class WebSocketManager extends EventEmitter {
/**
* The current status of this WebSocketManager
* @type {Status}
* @type {number}
*/
this.status = Status.IDLE;
@ -98,11 +100,11 @@ class WebSocketManager extends EventEmitter {
* The current session limit of the client
* @private
* @type {?Object}
* @property {number} total Total number of identifies available
* @property {number} remaining Number of identifies remaining
* @property {number} reset_after Number of milliseconds after which the limit resets
* @prop {number} total Total number of identifies available
* @prop {number} remaining Number of identifies remaining
* @prop {number} reset_after Number of milliseconds after which the limit resets
*/
this.sessionStartLimit = null;
this.sessionStartLimit = undefined;
}
/**
@ -212,7 +214,7 @@ class WebSocketManager extends EventEmitter {
if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
// These event codes cannot be resumed
shard.sessionID = null;
shard.sessionID = undefined;
}
/**

View file

@ -56,10 +56,10 @@ class WebSocketShard extends EventEmitter {
/**
* The current session ID of the shard
* @type {?string}
* @type {string}
* @private
*/
this.sessionID = null;
this.sessionID = undefined;
/**
* The previous heartbeat ping of the shard
@ -83,7 +83,6 @@ class WebSocketShard extends EventEmitter {
/**
* Contains the rate limit queue and metadata
* @name WebSocketShard#ratelimit
* @type {Object}
* @private
*/
@ -99,7 +98,6 @@ class WebSocketShard extends EventEmitter {
/**
* The WebSocket connection for the current shard
* @name WebSocketShard#connection
* @type {?WebSocket}
* @private
*/
@ -112,7 +110,6 @@ class WebSocketShard extends EventEmitter {
/**
* The compression to use
* @name WebSocketShard#inflate
* @type {?Inflate}
* @private
*/
@ -120,15 +117,13 @@ class WebSocketShard extends EventEmitter {
/**
* The HELLO timeout
* @name WebSocketShard#helloTimeout
* @type {?NodeJS.Timeout}
* @type {?NodeJS.Timer}
* @private
*/
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
Object.defineProperty(this, 'helloTimeout', { value: undefined, writable: true });
/**
* If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
* @type {boolean}
* @private
*/
@ -136,23 +131,20 @@ class WebSocketShard extends EventEmitter {
/**
* A set of guild IDs this shard expects to receive
* @name WebSocketShard#expectedGuilds
* @type {?Set<string>}
* @private
*/
Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
Object.defineProperty(this, 'expectedGuilds', { value: undefined, writable: true });
/**
* The ready timeout
* @name WebSocketShard#readyTimeout
* @type {?NodeJS.Timeout}
* @type {?NodeJS.Timer}
* @private
*/
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
Object.defineProperty(this, 'readyTimeout', { value: undefined, writable: true });
/**
* Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @type {number}
* @private
*/
@ -415,7 +407,6 @@ class WebSocketShard extends EventEmitter {
this.identify();
break;
case OPCodes.RECONNECT:
this.debug('[RECONNECT] Discord asked us to reconnect');
this.destroy({ closeCode: 4000 });
break;
case OPCodes.INVALID_SESSION:
@ -428,7 +419,7 @@ class WebSocketShard extends EventEmitter {
// Reset the sequence
this.sequence = -1;
// Reset the session ID as it's invalid
this.sessionID = null;
this.sessionID = undefined;
// Set the status to reconnecting
this.status = Status.RECONNECTING;
// Finally, emit the INVALID_SESSION event
@ -457,7 +448,7 @@ class WebSocketShard extends EventEmitter {
// Step 0. Clear the ready timeout, if it exists
if (this.readyTimeout) {
this.manager.client.clearTimeout(this.readyTimeout);
this.readyTimeout = null;
this.readyTimeout = undefined;
}
// Step 1. If we don't have any other guilds pending, we are ready
if (!this.expectedGuilds.size) {
@ -480,7 +471,7 @@ class WebSocketShard extends EventEmitter {
this.debug(`Shard did not receive any more guild packets in 15 seconds.
Unavailable guild count: ${this.expectedGuilds.size}`);
this.readyTimeout = null;
this.readyTimeout = undefined;
this.status = Status.READY;
@ -498,7 +489,7 @@ class WebSocketShard extends EventEmitter {
if (this.helloTimeout) {
this.debug('Clearing the HELLO timeout.');
this.manager.client.clearTimeout(this.helloTimeout);
this.helloTimeout = null;
this.helloTimeout = undefined;
}
return;
}
@ -519,7 +510,7 @@ class WebSocketShard extends EventEmitter {
if (this.heartbeatInterval) {
this.debug('Clearing the heartbeat interval.');
this.manager.client.clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
this.heartbeatInterval = undefined;
}
return;
}
@ -631,7 +622,7 @@ class WebSocketShard extends EventEmitter {
/**
* Adds a packet to the queue to be sent to the gateway.
* <warn>If you use this method, make sure you understand that you need to provide
* a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* Do not use this method if you don't know what you're doing.</warn>
* @param {Object} data The full packet to send
* @param {boolean} [important=false] If this packet should be added first in queue
@ -734,7 +725,7 @@ class WebSocketShard extends EventEmitter {
// Step 5: Reset the sequence and session ID if requested
if (reset) {
this.sequence = -1;
this.sessionID = null;
this.sessionID = undefined;
}
// Step 6: reset the ratelimit data

View file

@ -10,21 +10,13 @@ module.exports = (client, { d: data }) => {
for (const member of data.members) members.set(member.user.id, guild.members.add(member));
if (data.presences) {
for (const presence of data.presences) guild.presences.add(Object.assign(presence, { guild }));
for (const presence of data.presences) guild.presences.cache.add(Object.assign(presence, { guild }));
}
/**
* Emitted whenever a chunk of guild members is received (all members come from the same guild).
* @event Client#guildMembersChunk
* @param {Collection<Snowflake, GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
* @param {Object} chunk Properties of the received chunk
* @param {number} chunk.index Index of the received chunk
* @param {number} chunk.count Number of chunks the client should receive
* @param {?string} chunk.nonce Nonce for this chunk
*/
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild, {
count: data.chunk_count,
index: data.chunk_index,
nonce: data.nonce,
});
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild);
};

View file

@ -1,5 +1,22 @@
'use strict';
module.exports = (client, packet, shard) => {
client.actions.GuildMemberUpdate.handle(packet.d, shard);
const { Status, Events } = require('../../../util/Constants');
module.exports = (client, { d: data }, shard) => {
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const member = guild.members.cache.get(data.user.id);
if (member) {
const old = member._update(data);
if (shard.status === Status.READY) {
/**
* Emitted whenever a guild member changes - i.e. new role, removed role, nickname.
* @event Client#guildMemberUpdate
* @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update
*/
client.emit(Events.GUILD_MEMBER_UPDATE, old, member);
}
}
}
};

View file

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

View file

@ -1,5 +1,49 @@
'use strict';
module.exports = (client, packet) => {
client.actions.TypingStart.handle(packet.d);
const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => {
const channel = client.channels.cache.get(data.channel_id);
const user = client.users.cache.get(data.user_id);
const timestamp = new Date(data.timestamp * 1000);
if (channel && user) {
if (channel.type === 'voice') {
client.emit(Events.WARN, `Discord sent a typing packet to a voice channel ${channel.id}`);
return;
}
if (channel._typing.has(user.id)) {
const typing = channel._typing.get(user.id);
typing.lastTimestamp = timestamp;
typing.elapsedTime = Date.now() - typing.since;
client.clearTimeout(typing.timeout);
typing.timeout = tooLate(channel, user);
} else {
const since = new Date();
const lastTimestamp = new Date();
channel._typing.set(user.id, {
user,
since,
lastTimestamp,
elapsedTime: Date.now() - since,
timeout: tooLate(channel, user),
});
/**
* Emitted whenever a user starts typing in a channel.
* @event Client#typingStart
* @param {Channel} channel The channel the user started typing in
* @param {User} user The user that started typing
*/
client.emit(Events.TYPING_START, channel, user);
}
}
};
function tooLate(channel, user) {
return channel.client.setTimeout(() => {
channel._typing.delete(user.id);
}, 10000);
}

View file

@ -21,16 +21,11 @@ const Messages = {
DISALLOWED_INTENTS: 'Privileged intent provided is not enabled or whitelisted.',
SHARDING_NO_SHARDS: 'No shards have been spawned.',
SHARDING_IN_PROCESS: 'Shards are still being spawned.',
SHARDING_SHARD_NOT_FOUND: id => `Shard ${id} could not be found.`,
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`,
SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`,
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_DISCONNECTED: id => `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) =>
`Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`,
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
COLOR_CONVERT: 'Unable to convert color to a number.',
@ -71,7 +66,7 @@ const Messages = {
IMAGE_SIZE: size => `Invalid image size: ${size}`,
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 fit in an unsigned 64-bit integer.',
TYPING_COUNT: 'Count must be at least 1',
@ -104,8 +99,6 @@ const Messages = {
DELETE_GROUP_DM_CHANNEL: "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.',
};
for (const [name, message] of Object.entries(Messages)) register(name, message);

View file

@ -6,7 +6,6 @@ module.exports = {
// "Root" classes (starting points)
BaseClient: require('./client/BaseClient'),
Client: require('./client/Client'),
InteractionClient: require('./client/InteractionClient'),
Shard: require('./sharding/Shard'),
ShardClientUtil: require('./sharding/ShardClientUtil'),
ShardingManager: require('./sharding/ShardingManager'),
@ -29,12 +28,10 @@ module.exports = {
SnowflakeUtil: require('./util/Snowflake'),
Structures: require('./util/Structures'),
SystemChannelFlags: require('./util/SystemChannelFlags'),
UserFlags: require('./util/UserFlags'),
Util: Util,
version: require('../package.json').version,
// Managers
BaseGuildEmojiManager: require('./managers/BaseGuildEmojiManager'),
ChannelManager: require('./managers/ChannelManager'),
GuildChannelManager: require('./managers/GuildChannelManager'),
GuildEmojiManager: require('./managers/GuildEmojiManager'),
@ -42,7 +39,6 @@ module.exports = {
GuildMemberManager: require('./managers/GuildMemberManager'),
GuildMemberRoleManager: require('./managers/GuildMemberRoleManager'),
GuildManager: require('./managers/GuildManager'),
ReactionManager: require('./managers/ReactionManager'),
ReactionUserManager: require('./managers/ReactionUserManager'),
MessageManager: require('./managers/MessageManager'),
PresenceManager: require('./managers/PresenceManager'),
@ -58,8 +54,6 @@ module.exports = {
splitMessage: Util.splitMessage,
// Structures
Application: require('./structures/interfaces/Application'),
ApplicationCommand: require('./structures/ApplicationCommand'),
Base: require('./structures/Base'),
Activity: require('./structures/Presence').Activity,
APIMessage: require('./structures/APIMessage'),
@ -80,9 +74,7 @@ module.exports = {
GuildEmoji: require('./structures/GuildEmoji'),
GuildMember: require('./structures/GuildMember'),
GuildPreview: require('./structures/GuildPreview'),
GuildTemplate: require('./structures/GuildTemplate'),
Integration: require('./structures/Integration'),
Interaction: require('./structures/Interaction'),
Invite: require('./structures/Invite'),
Message: require('./structures/Message'),
MessageAttachment: require('./structures/MessageAttachment'),

View file

@ -1,80 +0,0 @@
'use strict';
const BaseManager = require('./BaseManager');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { parseEmoji } = require('../util/Util');
/**
* Holds methods to resolve GuildEmojis and stores their cache.
* @extends {BaseManager}
*/
class BaseGuildEmojiManager extends BaseManager {
constructor(client, iterable) {
super(client, iterable, GuildEmoji);
}
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name BaseGuildEmojiManager#cache
*/
/**
* Data that can be resolved into a GuildEmoji object. This can be:
* * A custom emoji ID
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
* Resolves an EmojiResolvable to an Emoji object.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
/**
* Resolves an EmojiResolvable to an Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
return super.resolveID(emoji);
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
* * An EmojiResolvable
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveIdentifier(emoji) {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
const res = parseEmoji(emoji);
if (res && res.name.length) {
emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`;
}
if (!emoji.includes('%')) return encodeURIComponent(emoji);
return emoji;
}
return null;
}
}
module.exports = BaseGuildEmojiManager;

View file

@ -74,7 +74,6 @@ class ChannelManager extends BaseManager {
* Obtains a channel from Discord, or the channel cache if it's already available.
* @param {Snowflake} id ID of the channel
* @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Channel>}
* @example
* // Fetch a channel by its id
@ -82,11 +81,9 @@ class ChannelManager extends BaseManager {
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
async fetch(id, cache = true, force = false) {
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
async fetch(id, cache = true) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
const data = await this.client.api.channels(id).get();
return this.add(data, null, cache);

View file

@ -46,7 +46,7 @@ class GuildChannelManager extends BaseManager {
* @memberof GuildChannelManager
* @instance
* @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
* @returns {?GuildChannel}
* @returns {?Channel}
*/
/**

View file

@ -1,18 +1,19 @@
'use strict';
const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const BaseManager = require('./BaseManager');
const { TypeError } = require('../errors');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const Collection = require('../util/Collection');
const DataResolver = require('../util/DataResolver');
/**
* Manages API methods for GuildEmojis and stores their cache.
* @extends {BaseGuildEmojiManager}
* @extends {BaseManager}
*/
class GuildEmojiManager extends BaseGuildEmojiManager {
class GuildEmojiManager extends BaseManager {
constructor(guild, iterable) {
super(guild.client, iterable);
super(guild.client, iterable, GuildEmoji);
/**
* The guild this manager belongs to
* @type {Guild}
@ -20,6 +21,12 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
this.guild = guild;
}
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name GuildEmojiManager#cache
*/
add(data, cache) {
return super.add(data, cache, { extras: [this.guild] });
}
@ -66,6 +73,57 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
.emojis.post({ data, reason })
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji);
}
/**
* Data that can be resolved into an GuildEmoji object. This can be:
* * A custom emoji ID
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
* Resolves an EmojiResolvable to an Emoji object.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
/**
* Resolves an EmojiResolvable to an Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
return super.resolveID(emoji);
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * An EmojiResolvable
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveIdentifier(emoji) {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
if (!emoji.includes('%')) return encodeURIComponent(emoji);
else return emoji;
}
return null;
}
}
module.exports = GuildEmojiManager;

View file

@ -8,7 +8,6 @@ const GuildMember = require('../structures/GuildMember');
const Invite = require('../structures/Invite');
const Role = require('../structures/Role');
const {
ChannelTypes,
Events,
VerificationLevels,
DefaultMessageNotifications,
@ -130,8 +129,6 @@ class GuildManager extends BaseManager {
* <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild
* @param {Object} [options] Options for the creating
* @param {number} [options.afkChannelID] The ID of the AFK channel
* @param {number} [options.afkTimeout] The AFK timeout in seconds
* @param {PartialChannelData[]} [options.channels] The channels for this guild
* @param {DefaultMessageNotifications} [options.defaultMessageNotifications] The default message notifications
* for the guild
@ -140,22 +137,18 @@ class GuildManager extends BaseManager {
* @param {string} [options.region] The region for the server, defaults to the closest one available
* @param {PartialRoleData[]} [options.roles] The roles for this guild,
* the first element of this array is used to change properties of the guild's everyone role.
* @param {number} [options.systemChannelID] The ID of the system channel
* @param {VerificationLevel} [options.verificationLevel] The verification level for the guild
* @returns {Promise<Guild>} The guild that was created
*/
async create(
name,
{
afkChannelID,
afkTimeout,
channels = [],
defaultMessageNotifications,
explicitContentFilter,
icon = null,
region,
roles = [],
systemChannelID,
verificationLevel,
} = {},
) {
@ -170,7 +163,6 @@ class GuildManager extends BaseManager {
explicitContentFilter = ExplicitContentFilterLevels.indexOf(explicitContentFilter);
}
for (const channel of channels) {
if (channel.type) channel.type = ChannelTypes[channel.type.toUpperCase()];
channel.parent_id = channel.parentID;
delete channel.parentID;
if (!channel.permissionOverwrites) continue;
@ -195,11 +187,8 @@ class GuildManager extends BaseManager {
verification_level: verificationLevel,
default_message_notifications: defaultMessageNotifications,
explicit_content_filter: explicitContentFilter,
roles,
channels,
afk_channel_id: afkChannelID,
afk_timeout: afkTimeout,
system_channel_id: systemChannelID,
roles,
},
})
.then(data => {
@ -207,46 +196,21 @@ class GuildManager extends BaseManager {
const handleGuild = guild => {
if (guild.id === data.id) {
this.client.clearTimeout(timeout);
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.decrementMaxListeners();
this.client.clearTimeout(timeout);
resolve(guild);
}
};
this.client.incrementMaxListeners();
this.client.on(Events.GUILD_CREATE, handleGuild);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.decrementMaxListeners();
resolve(this.client.guilds.add(data));
}, 10000);
return undefined;
}, reject),
);
}
/**
* Obtains a guild from Discord, or the guild cache if it's already available.
* @param {Snowflake} id ID of the guild
* @param {boolean} [cache=true] Whether to cache the new guild object if it isn't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Guild>}
* @example
* // Fetch a guild by its id
* client.guilds.fetch('222078108977594368')
* .then(guild => console.log(guild.name))
* .catch(console.error);
*/
async fetch(id, cache = true, force = false) {
if (!force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const data = await this.client.api.guilds(id).get({ query: { with_counts: true } });
return this.add(data, cache);
}
}
module.exports = GuildManager;

View file

@ -1,11 +1,10 @@
'use strict';
const BaseManager = require('./BaseManager');
const { Error, TypeError, RangeError } = require('../errors');
const { Error, TypeError } = require('../errors');
const GuildMember = require('../structures/GuildMember');
const Collection = require('../util/Collection');
const { Events, OPCodes } = require('../util/Constants');
const SnowflakeUtil = require('../util/Snowflake');
/**
* Manages API methods for GuildMembers and stores their cache.
@ -68,7 +67,6 @@ class GuildMemberManager extends BaseManager {
* @typedef {Object} FetchMemberOptions
* @property {UserResolvable} user The user to fetch
* @property {boolean} [cache=true] Whether or not to cache the fetched member
* @property {boolean} [force=false] Whether to skip the cache check and request the API
*/
/**
@ -78,9 +76,6 @@ class GuildMemberManager extends BaseManager {
* @property {?string} query Limit fetch to members with similar usernames
* @property {number} [limit=0] Maximum number of members to request
* @property {boolean} [withPresences=false] Whether or not to include the presences
* @property {number} [time=120e3] Timeout for receipt of members
* @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
* @property {boolean} [force=false] Whether to skip the cache check and request the API
*/
/**
@ -100,11 +95,6 @@ class GuildMemberManager extends BaseManager {
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single member without checking cache
* guild.members.fetch({ user, force: true })
* .then(console.log)
* .catch(console.error)
* @example
* // Fetch a single member without caching
* guild.members.fetch({ user, cache: false })
* .then(console.log)
@ -143,7 +133,6 @@ class GuildMemberManager extends BaseManager {
* @param {number} [options.days=7] Number of days of inactivity required to kick
* @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them
* @param {boolean} [options.count=true] Whether or not to return the number of users that have been kicked.
* @param {RoleResolvable[]} [options.roles=[]] Array of roles to bypass the "...and no roles" constraint when pruning
* @param {string} [options.reason] Reason for this prune
* @returns {Promise<number|null>} The number of members that were/will be kicked
* @example
@ -156,39 +145,16 @@ class GuildMemberManager extends BaseManager {
* guild.members.prune({ days: 1, reason: 'too many people!' })
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error);
* @example
* // Include members with a specified role
* guild.members.prune({ days: 7, roles: ['657259391652855808'] })
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error);
*/
prune({ days = 7, dry = false, count: compute_prune_count = true, roles = [], reason } = {}) {
prune({ days = 7, dry = false, count = true, reason } = {}) {
if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
const query = { days };
const resolvedRoles = [];
for (const role of roles) {
const resolvedRole = this.guild.roles.resolveID(role);
if (!resolvedRole) {
return Promise.reject(new TypeError('INVALID_TYPE', 'roles', 'Array of Roles or Snowflakes', true));
}
resolvedRoles.push(resolvedRole);
}
if (resolvedRoles.length) {
query.include_roles = dry ? resolvedRoles.join(',') : resolvedRoles;
}
const endpoint = this.client.api.guilds(this.guild.id).prune;
if (dry) {
return endpoint.get({ query, reason }).then(data => data.pruned);
}
return endpoint
.post({
data: { ...query, compute_prune_count },
return this.client.api
.guilds(this.guild.id)
.prune[dry ? 'get' : 'post']({
query: {
days,
compute_prune_count: count,
},
reason,
})
.then(data => data.pruned);
@ -198,7 +164,7 @@ class GuildMemberManager extends BaseManager {
* Bans a user from the guild.
* @param {UserResolvable} user The user to ban
* @param {Object} [options] Options for the ban
* @param {number} [options.days=0] Number of days of messages to delete, must be between 0 and 7
* @param {number} [options.days=0] Number of days of messages to delete
* @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
* If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
@ -210,13 +176,12 @@ class GuildMemberManager extends BaseManager {
* .catch(console.error);
*/
ban(user, options = { days: 0 }) {
if (typeof options !== 'object') return Promise.reject(new TypeError('INVALID_TYPE', 'options', 'object', true));
if (options.days) options.delete_message_days = options.days;
if (options.days) options['delete-message-days'] = options.days;
const id = this.client.users.resolveID(user);
if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true));
return this.client.api
.guilds(this.guild.id)
.bans[id].put({ data: options })
.bans[id].put({ query: options })
.then(() => {
if (user instanceof GuildMember) return user;
const _user = this.client.users.resolve(id);
@ -248,12 +213,9 @@ class GuildMemberManager extends BaseManager {
.then(() => this.client.users.resolve(user));
}
_fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return Promise.resolve(existing);
}
_fetchSingle({ user, cache }) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return Promise.resolve(existing);
return this.client.api
.guilds(this.guild.id)
.members(user)
@ -261,22 +223,13 @@ class GuildMemberManager extends BaseManager {
.then(data => this.add(data, cache));
}
_fetchMany({
limit = 0,
withPresences: presences = false,
user: user_ids,
query,
time = 120e3,
nonce = SnowflakeUtil.generate(),
force = false,
} = {}) {
_fetchMany({ limit = 0, withPresences: presences = false, user: user_ids, query } = {}) {
return new Promise((resolve, reject) => {
if (this.guild.memberCount === this.cache.size && !query && !limit && !presences && !user_ids && !force) {
if (this.guild.memberCount === this.cache.size && !query && !limit && !presences && !user_ids) {
resolve(this.cache);
return;
}
if (!query && !user_ids) query = '';
if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
this.guild.shard.send({
op: OPCodes.REQUEST_GUILD_MEMBERS,
d: {
@ -284,41 +237,33 @@ class GuildMemberManager extends BaseManager {
presences,
user_ids,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
const option = query || limit || presences || user_ids;
let i = 0;
const handler = (members, _, chunk) => {
const handler = (members, guild) => {
if (guild.id !== this.guild.id) return;
timeout.refresh();
if (chunk.nonce !== nonce) return;
i++;
for (const member of members.values()) {
if (option) fetchedMembers.set(member.id, member);
}
if (
this.guild.memberCount <= this.cache.size ||
(option && members.size < 1000) ||
(limit && fetchedMembers.size >= limit) ||
i === chunk.count
(limit && fetchedMembers.size >= limit)
) {
this.client.clearTimeout(timeout);
this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.decrementMaxListeners();
this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
let fetched = option ? fetchedMembers : this.cache;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
resolve(fetched);
}
};
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.decrementMaxListeners();
const timeout = this.guild.client.setTimeout(() => {
this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
reject(new Error('GUILD_MEMBERS_TIMEOUT'));
}, time);
this.client.incrementMaxListeners();
this.client.on(Events.GUILD_MEMBERS_CHUNK, handler);
}, 120e3);
this.guild.client.on(Events.GUILD_MEMBERS_CHUNK, handler);
});
}
}

View file

@ -90,7 +90,12 @@ class GuildMemberRoleManager {
} else {
roleOrRoles = this.guild.roles.resolve(roleOrRoles);
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',
true,
);
}
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason });

View file

@ -1,7 +1,6 @@
'use strict';
const BaseManager = require('./BaseManager');
const { TypeError } = require('../errors');
const Message = require('../structures/Message');
const Collection = require('../util/Collection');
const LimitedCollection = require('../util/LimitedCollection');
@ -46,7 +45,6 @@ class MessageManager extends BaseManager {
* Those need to be fetched separately in such a case.</info>
* @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters.
* @param {boolean} [cache=true] Whether to cache the message(s)
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Message>|Promise<Collection<Snowflake, Message>>}
* @example
* // Get message
@ -64,8 +62,8 @@ class MessageManager extends BaseManager {
* .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`))
* .catch(console.error);
*/
fetch(message, cache = true, force = false) {
return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache);
fetch(message, cache = true) {
return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache);
}
/**
@ -76,7 +74,7 @@ class MessageManager extends BaseManager {
* @returns {Promise<Collection<Snowflake, Message>>}
* @example
* // Get pinned messages
* channel.messages.fetchPinned()
* channel.fetchPinned()
* .then(messages => console.log(`Received ${messages.size} messages`))
* .catch(console.error);
*/
@ -117,21 +115,20 @@ class MessageManager extends BaseManager {
* Deletes a message, even if it's not cached.
* @param {MessageResolvable} message The message to delete
* @param {string} [reason] Reason for deleting this message, if it does not belong to the client user
* @returns {Promise<void>}
*/
async delete(message, reason) {
message = this.resolveID(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).messages(message).delete({ reason });
if (message) {
await this.client.api
.channels(this.channel.id)
.messages(message)
.delete({ reason });
}
}
async _fetchId(messageID, cache, force) {
if (!force) {
const existing = this.cache.get(messageID);
if (existing && !existing.partial) return existing;
}
async _fetchId(messageID, cache) {
const existing = this.cache.get(messageID);
if (existing && !existing.partial) return existing;
const data = await this.client.api.channels[this.channel.id].messages[messageID].get();
return this.add(data, cache);
}

View file

@ -24,7 +24,7 @@ class ReactionManager extends BaseManager {
/**
* The reaction cache of this manager
* @type {Collection<string|Snowflake, MessageReaction>}
* @type {Collection<Snowflake, MessageReaction>}
* @name ReactionManager#cache
*/

View file

@ -48,16 +48,16 @@ class ReactionUserManager extends BaseManager {
/**
* Removes a user from this reaction.
* @param {UserResolvable} [user=this.client.user] The user to remove the reaction of
* @param {UserResolvable} [user=this.reaction.message.client.user] The user to remove the reaction of
* @returns {Promise<MessageReaction>}
*/
remove(user = this.client.user) {
const userID = this.client.users.resolveID(user);
if (!userID) return Promise.reject(new Error('REACTION_RESOLVE_USER'));
remove(user = this.reaction.message.client.user) {
const message = this.reaction.message;
return this.client.api.channels[message.channel.id].messages[message.id].reactions[this.reaction.emoji.identifier][
userID === this.client.user.id ? '@me' : userID
]
const userID = message.client.users.resolveID(user);
if (!userID) return Promise.reject(new Error('REACTION_RESOLVE_USER'));
return message.client.api.channels[message.channel.id].messages[message.id].reactions[
this.reaction.emoji.identifier
][userID === message.client.user.id ? '@me' : userID]
.delete()
.then(() => this.reaction);
}

View file

@ -2,7 +2,6 @@
const BaseManager = require('./BaseManager');
const Role = require('../structures/Role');
const Collection = require('../util/Collection');
const Permissions = require('../util/Permissions');
const { resolveColor } = require('../util/Util');
@ -32,10 +31,9 @@ class RoleManager extends BaseManager {
/**
* Obtains one or more roles from Discord, or the role cache if they're already available.
* @param {Snowflake} [id] ID of the role to fetch
* @param {boolean} [cache=true] Whether to cache the new role object(s) if they weren't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<?Role|Collection<Snowflake, Role>>}
* @param {Snowflake} [id] ID or IDs of the role(s)
* @param {boolean} [cache=true] Whether to cache the new roles objects if it weren't already
* @returns {Promise<Role|RoleManager>}
* @example
* // Fetch all roles from the guild
* message.guild.roles.fetch()
@ -47,17 +45,16 @@ class RoleManager extends BaseManager {
* .then(role => console.log(`The role color is: ${role.color}`))
* .catch(console.error);
*/
async fetch(id, cache = true, force = false) {
if (id && !force) {
async fetch(id, cache = true) {
if (id) {
const existing = this.cache.get(id);
if (existing) return existing;
}
// We cannot fetch a single role, as of this commit's date, Discord API throws with 405
const data = await this.client.api.guilds(this.guild.id).roles.get();
const roles = new Collection();
for (const role of data) roles.set(role.id, this.add(role, cache));
return id ? roles.get(id) || null : roles;
const roles = await this.client.api.guilds(this.guild.id).roles.get();
for (const role of roles) this.add(role, cache);
return id ? this.cache.get(id) || null : this;
}
/**
@ -128,11 +125,11 @@ class RoleManager extends BaseManager {
/**
* The `@everyone` role of the guild
* @type {Role}
* @type {?Role}
* @readonly
*/
get everyone() {
return this.cache.get(this.guild.id);
return this.cache.get(this.guild.id) || null;
}
/**

View file

@ -55,15 +55,11 @@ class UserManager extends BaseManager {
* Obtains a user from Discord, or the user cache if it's already available.
* @param {Snowflake} id ID of the user
* @param {boolean} [cache=true] Whether to cache the new user object if it isn't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<User>}
*/
async fetch(id, cache = true, force = false) {
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
async fetch(id, cache = true) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
const data = await this.client.api.users(id).get();
return this.add(data, cache);
}

View file

@ -1,6 +1,7 @@
'use strict';
const BaseManager = require('./BaseManager');
const VoiceState = require('../structures/VoiceState');
/**
* Manages API methods for VoiceStates and stores their cache.
@ -8,7 +9,7 @@ const BaseManager = require('./BaseManager');
*/
class VoiceStateManager extends BaseManager {
constructor(guild, iterable) {
super(guild.client, iterable, { name: 'VoiceState' });
super(guild.client, iterable, VoiceState);
/**
* The guild this manager belongs to
* @type {Guild}
@ -26,7 +27,7 @@ class VoiceStateManager extends BaseManager {
const existing = this.cache.get(data.user_id);
if (existing) return existing._patch(data);
const entry = new this.holds(this.guild, data);
const entry = new VoiceState(this.guild, data);
if (cache) this.cache.set(data.user_id, entry);
return entry;
}

View file

@ -1,8 +1,8 @@
'use strict';
const https = require('https');
const FormData = require('@discordjs/form-data');
const AbortController = require('abort-controller');
const FormData = require('form-data');
const fetch = require('node-fetch');
const { browser, UserAgent } = require('../util/Constants');
@ -15,13 +15,11 @@ class APIRequest {
this.method = method;
this.route = options.route;
this.options = options;
this.retries = 0;
let queryString = '';
if (options.query) {
const query = Object.entries(options.query)
.filter(([, value]) => ![null, 'null', 'undefined'].includes(value) && typeof value !== 'undefined')
.flatMap(([key, value]) => (Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]]));
// Filter out undefined query options
const query = Object.entries(options.query).filter(([, value]) => value !== null && typeof value !== 'undefined');
queryString = new URLSearchParams(query).toString();
}
this.path = `${path}${queryString && `?${queryString}`}`;

View file

@ -1,95 +0,0 @@
/**
* MIT License
*
* Copyright (c) 2020 kyranet, discord.js
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
'use strict';
// TODO(kyranet, vladfrangu): replace this with discord.js v13's core AsyncQueue.
/**
* An async queue that preserves the stack and prevents lock-ups.
* @private
*/
class AsyncQueue {
constructor() {
/**
* The promises array.
* @type {Array<{promise: Promise<void>, resolve: Function}>}
* @private
*/
this.promises = [];
}
/**
* The remaining amount of queued promises
* @type {number}
*/
get remaining() {
return this.promises.length;
}
/**
* Waits for last promise and queues a new one.
* @returns {Promise<void>}
* @example
* const queue = new AsyncQueue();
* async function request(url, options) {
* await queue.wait();
* try {
* const result = await fetch(url, options);
* // Do some operations with 'result'
* } finally {
* // Remove first entry from the queue and resolve for the next entry
* queue.shift();
* }
* }
*
* request(someUrl1, someOptions1); // Will call fetch() immediately
* request(someUrl2, someOptions2); // Will call fetch() after the first finished
* request(someUrl3, someOptions3); // Will call fetch() after the second finished
*/
wait() {
const next = this.promises.length ? this.promises[this.promises.length - 1].promise : Promise.resolve();
let resolve;
const promise = new Promise(res => {
resolve = res;
});
this.promises.push({
resolve,
promise,
});
return next;
}
/**
* Frees the queue's lock for the next item to process.
*/
shift() {
const deferred = this.promises.shift();
if (typeof deferred !== 'undefined') deferred.resolve();
}
}
module.exports = AsyncQueue;

View file

@ -35,6 +35,19 @@ class RESTManager {
return Endpoints.CDN(this.client.options.http.cdn);
}
push(handler, apiRequest) {
return new Promise((resolve, reject) => {
handler
.push({
request: apiRequest,
resolve,
reject,
retries: 0,
})
.catch(reject);
});
}
request(method, url, options = {}) {
const apiRequest = new APIRequest(this, method, url, options);
let handler = this.handlers.get(apiRequest.route);
@ -44,11 +57,7 @@ class RESTManager {
this.handlers.set(apiRequest.route, handler);
}
return handler.push(apiRequest);
}
get endpoint() {
return this.client.options.http.api;
return this.push(handler, apiRequest);
}
set endpoint(endpoint) {

View file

@ -1,6 +1,5 @@
'use strict';
const AsyncQueue = require('./AsyncQueue');
const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError');
const {
@ -26,31 +25,46 @@ function calculateReset(reset, serverDate) {
class RequestHandler {
constructor(manager) {
this.manager = manager;
this.queue = new AsyncQueue();
this.busy = false;
this.queue = [];
this.reset = -1;
this.remaining = -1;
this.limit = -1;
this.retryAfter = -1;
}
async push(request) {
await this.queue.wait();
try {
return await this.execute(request);
} finally {
this.queue.shift();
push(request) {
if (this.busy) {
this.queue.push(request);
return this.run();
} else {
return this.execute(request);
}
}
run() {
if (this.queue.length === 0) return Promise.resolve();
return this.execute(this.queue.shift());
}
get limited() {
return Boolean(this.manager.globalTimeout) || (this.remaining <= 0 && Date.now() < this.reset);
}
get _inactive() {
return this.queue.remaining === 0 && !this.limited;
return this.queue.length === 0 && !this.limited && this.busy !== true;
}
async execute(request) {
async execute(item) {
// Insert item back to the beginning if currently busy
if (this.busy) {
this.queue.unshift(item);
return null;
}
this.busy = true;
const { reject, request, resolve } = item;
// After calculations and requests have been done, pre-emptively stop further requests
if (this.limited) {
const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
@ -88,13 +102,9 @@ class RequestHandler {
try {
res = await request.make();
} catch (error) {
// Retry the specified number of times for request abortions
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path);
}
request.retries++;
return this.execute(request);
// NodeFetch error expected for all "operational" errors, such as 500 status code
this.busy = false;
return reject(new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path));
}
if (res && res.headers) {
@ -110,7 +120,7 @@ class RequestHandler {
this.retryAfter = retryAfter ? Number(retryAfter) : -1;
// https://github.com/discordapp/discord-api-docs/issues/182
if (request.route.includes('reactions')) {
if (item.request.route.includes('reactions')) {
this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250;
}
@ -127,46 +137,43 @@ class RequestHandler {
}
}
// Handle 2xx and 3xx responses
// Finished handling headers, safe to unlock manager
this.busy = false;
if (res.ok) {
const success = await parseResponse(res);
// Nothing wrong with the request, proceed with the next one
return parseResponse(res);
}
// Handle 4xx responses
if (res.status >= 400 && res.status < 500) {
// Handle ratelimited requests
if (res.status === 429) {
// A ratelimit was hit - this should never happen
this.manager.client.emit('debug', `429 hit on route ${request.route}`);
await Util.delayFor(this.retryAfter);
return this.execute(request);
}
// Handle possible malformed requests
let data;
try {
data = await parseResponse(res);
} catch (err) {
throw new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path);
}
throw new DiscordAPIError(request.path, data, request.method, res.status);
}
// Handle 5xx responses
if (res.status >= 500 && res.status < 600) {
resolve(success);
return this.run();
} else if (res.status === 429) {
// A ratelimit was hit - this should never happen
this.queue.unshift(item);
this.manager.client.emit('debug', `429 hit on route ${item.request.route}`);
await Util.delayFor(this.retryAfter);
return this.run();
} else if (res.status >= 500 && res.status < 600) {
// Retry the specified number of times for possible serverside issues
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(res.statusText, res.constructor.name, res.status, request.method, request.path);
if (item.retries === this.manager.client.options.retryLimit) {
return reject(
new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path),
);
} else {
item.retries++;
this.queue.unshift(item);
return this.run();
}
} else {
// Handle possible malformed requests
try {
const data = await parseResponse(res);
if (res.status >= 400 && res.status < 500) {
return reject(new DiscordAPIError(request.path, data, request.method, res.status));
}
return null;
} catch (err) {
return reject(new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path));
}
request.retries++;
return this.execute(request);
}
// Fallback in the rare case a status code outside the range 200..=599 is returned
return null;
}
}

View file

@ -44,7 +44,7 @@ class Shard extends EventEmitter {
/**
* Arguments for the shard's process executable (only when {@link ShardingManager#mode} is `process`)
* @type {string[]}
* @type {?string[]}
*/
this.execArgv = manager.execArgv;
@ -124,9 +124,6 @@ class Shard extends EventEmitter {
.on('exit', this._exitListener);
}
this._evals.clear();
this._fetches.clear();
/**
* Emitted upon the creation of the shard's child process/worker.
* @event Shard#spawn
@ -228,10 +225,6 @@ class Shard extends EventEmitter {
* .catch(console.error);
*/
fetchClientValue(prop) {
// Shard is dead (maybe respawning), don't cache anything and error immediately
if (!this.process && !this.worker) return Promise.reject(new Error('SHARDING_NO_CHILD_EXISTS', this.id));
// Cached promise from previous call
if (this._fetches.has(prop)) return this._fetches.get(prop);
const promise = new Promise((resolve, reject) => {
@ -262,10 +255,6 @@ class Shard extends EventEmitter {
* @returns {Promise<*>} Result of the script execution
*/
eval(script) {
// Shard is dead (maybe respawning), don't cache anything and error immediately
if (!this.process && !this.worker) return Promise.reject(new Error('SHARDING_NO_CHILD_EXISTS', this.id));
// Cached promise from previous call
if (this._evals.has(script)) return this._evals.get(script);
const promise = new Promise((resolve, reject) => {
@ -334,20 +323,18 @@ class Shard extends EventEmitter {
// Shard is requesting a property fetch
if (message._sFetchProp) {
const resp = { _sFetchProp: message._sFetchProp, _sFetchPropShard: message._sFetchPropShard };
this.manager.fetchClientValues(message._sFetchProp, message._sFetchPropShard).then(
results => this.send({ ...resp, _result: results }),
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
this.manager.fetchClientValues(message._sFetchProp).then(
results => this.send({ _sFetchProp: message._sFetchProp, _result: results }),
err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }),
);
return;
}
// Shard is requesting an eval broadcast
if (message._sEval) {
const resp = { _sEval: message._sEval, _sEvalShard: message._sEvalShard };
this.manager.broadcastEval(message._sEval, message._sEvalShard).then(
results => this.send({ ...resp, _result: results }),
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
this.manager.broadcastEval(message._sEval).then(
results => this.send({ _sEval: message._sEval, _result: results }),
err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }),
);
return;
}

View file

@ -79,7 +79,6 @@ class ShardClientUtil {
* Sends a message to the master process.
* @param {*} message Message to send
* @returns {Promise<void>}
* @emits Shard#message
*/
send(message) {
return new Promise((resolve, reject) => {
@ -96,29 +95,28 @@ class ShardClientUtil {
}
/**
* Fetches a client property value of each shard, or a given shard.
* Fetches a client property value of each shard.
* @param {string} prop Name of the client property to get, using periods for nesting
* @param {number} [shard] Shard to fetch property from, all if undefined
* @returns {Promise<*>|Promise<Array<*>>}
* @returns {Promise<Array<*>>}
* @example
* client.shard.fetchClientValues('guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
* @see {@link ShardingManager#fetchClientValues}
*/
fetchClientValues(prop, shard) {
fetchClientValues(prop) {
return new Promise((resolve, reject) => {
const parent = this.parentPort || process;
const listener = message => {
if (!message || message._sFetchProp !== prop || message._sFetchPropShard !== shard) return;
if (!message || message._sFetchProp !== prop) return;
parent.removeListener('message', listener);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
parent.on('message', listener);
this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => {
this.send({ _sFetchProp: prop }).catch(err => {
parent.removeListener('message', listener);
reject(err);
});
@ -126,30 +124,29 @@ class ShardClientUtil {
}
/**
* Evaluates a script or function on all shards, or a given shard, in the context of the {@link Client}s.
* Evaluates a script or function on all shards, in the context of the {@link Clients}.
* @param {string|Function} script JavaScript to run on each shard
* @param {number} [shard] Shard to run script on, all if undefined
* @returns {Promise<*>|Promise<Array<*>>} Results of the script execution
* @returns {Promise<Array<*>>} Results of the script execution
* @example
* client.shard.broadcastEval('this.guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
* @see {@link ShardingManager#broadcastEval}
*/
broadcastEval(script, shard) {
broadcastEval(script) {
return new Promise((resolve, reject) => {
const parent = this.parentPort || process;
script = typeof script === 'function' ? `(${script})(this)` : script;
const listener = message => {
if (!message || message._sEval !== script || message._sEvalShard !== shard) return;
if (!message || message._sEval !== script) return;
parent.removeListener('message', listener);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
parent.on('message', listener);
this.send({ _sEval: script, _sEvalShard: shard }).catch(err => {
this.send({ _sEval: script }).catch(err => {
parent.removeListener('message', listener);
reject(err);
});
@ -226,18 +223,6 @@ class ShardClientUtil {
}
return this._singleton;
}
/**
* Get the shard ID for a given guild ID.
* @param {Snowflake} guildID Snowflake guild ID to get shard ID for
* @param {number} shardCount Number of shards
* @returns {number}
*/
static shardIDForGuildID(guildID, shardCount) {
const shard = Number(BigInt(guildID) >> 22n) % shardCount;
if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildID, shardCount);
return shard;
}
}
module.exports = ShardClientUtil;

View file

@ -20,7 +20,9 @@ const Util = require('../util/Util');
class ShardingManager extends EventEmitter {
/**
* The mode to spawn shards with for a {@link ShardingManager}: either "process" to use child processes, or
* "worker" to use [Worker threads](https://nodejs.org/api/worker_threads.html).
* "worker" to use workers. The "worker" mode relies on the experimental
* [Worker threads](https://nodejs.org/api/worker_threads.html) functionality that is present in Node v10.5.0 or
* newer. Node must be started with the `--experimental-worker` flag to expose it.
* @typedef {Object} ShardingManagerMode
*/
@ -222,48 +224,30 @@ class ShardingManager extends EventEmitter {
}
/**
* Evaluates a script on all shards, or a given shard, in the context of the {@link Client}s.
* Evaluates a script on all shards, in the context of the {@link Client}s.
* @param {string} script JavaScript to run on each shard
* @param {number} [shard] Shard to run on, all if undefined
* @returns {Promise<*>|Promise<Array<*>>} Results of the script execution
* @returns {Promise<Array<*>>} Results of the script execution
*/
broadcastEval(script, shard) {
return this._performOnShards('eval', [script], shard);
broadcastEval(script) {
const promises = [];
for (const shard of this.shards.values()) promises.push(shard.eval(script));
return Promise.all(promises);
}
/**
* Fetches a client property value of each shard, or a given shard.
* Fetches a client property value of each shard.
* @param {string} prop Name of the client property to get, using periods for nesting
* @param {number} [shard] Shard to fetch property from, all if undefined
* @returns {Promise<*>|Promise<Array<*>>}
* @returns {Promise<Array<*>>}
* @example
* manager.fetchClientValues('guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
*/
fetchClientValues(prop, shard) {
return this._performOnShards('fetchClientValue', [prop], shard);
}
/**
* Runs a method with given arguments on all shards, or a given shard.
* @param {string} method Method name to run on each shard
* @param {Array<*>} args Arguments to pass through to the method call
* @param {number} [shard] Shard to run on, all if undefined
* @returns {Promise<*>|Promise<Array<*>>} Results of the method execution
* @private
*/
_performOnShards(method, args, shard) {
fetchClientValues(prop) {
if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS'));
if (this.shards.size !== this.shardList.length) return Promise.reject(new Error('SHARDING_IN_PROCESS'));
if (typeof shard === 'number') {
if (this.shards.has(shard)) return this.shards.get(shard)[method](...args);
return Promise.reject(new Error('SHARDING_SHARD_NOT_FOUND', shard));
}
const promises = [];
for (const sh of this.shards.values()) promises.push(sh[method](...args));
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
return Promise.all(promises);
}

View file

@ -74,21 +74,13 @@ class APIMessage {
return this.target instanceof Message;
}
/**
* Whether or not the target is an interaction
* @type {boolean}
* @readonly
*/
get isInteraction() {
const Interaction = require('./Interaction');
return this.target instanceof Interaction;
}
/**
* Makes the content of this message.
* @returns {?(string|string[])}
*/
makeContent() {
const GuildMember = require('./GuildMember');
let content;
if (this.options.content === null) {
content = '';
@ -96,16 +88,14 @@ class APIMessage {
content = Util.resolveString(this.options.content);
}
if (typeof content !== 'string') return content;
const disableMentions =
typeof this.options.disableMentions === 'undefined'
? this.target.client.options.disableMentions
: this.options.disableMentions;
if (disableMentions === 'all') {
content = Util.removeMentions(content);
content = Util.removeMentions(content || '');
} else if (disableMentions === 'everyone') {
content = content.replace(/@([^<>@ ]*)/gmsu, (match, target) => {
content = (content || '').replace(/@([^<>@ ]*)/gmsu, (match, target) => {
if (target.match(/^[&!]?\d+$/)) {
return `@${target}`;
} else {
@ -118,18 +108,29 @@ class APIMessage {
const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false;
const splitOptions = isSplit ? { ...this.options.split } : undefined;
if (content) {
let mentionPart = '';
if (this.options.reply && !this.isUser && this.target.type !== 'dm') {
const id = this.target.client.users.resolveID(this.options.reply);
mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `;
if (isSplit) {
splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`;
}
}
if (content || mentionPart) {
if (isCode) {
const codeName = typeof this.options.code === 'string' ? this.options.code : '';
content = `\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content)}\n\`\`\``;
content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content || '')}\n\`\`\``;
if (isSplit) {
splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`;
splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`;
}
} else if (mentionPart) {
content = `${mentionPart}${content || ''}`;
}
if (isSplit) {
content = Util.splitMessage(content, splitOptions);
content = Util.splitMessage(content || '', splitOptions);
}
}
@ -148,11 +149,8 @@ class APIMessage {
let nonce;
if (typeof this.options.nonce !== 'undefined') {
nonce = this.options.nonce;
// eslint-disable-next-line max-len
if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
throw new RangeError('MESSAGE_NONCE_TYPE');
}
nonce = parseInt(this.options.nonce);
if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
}
const embedLikes = [];
@ -176,29 +174,6 @@ class APIMessage {
if (this.isMessage) {
// eslint-disable-next-line eqeqeq
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield;
} else if (this.isInteraction) {
flags = this.options.ephemeral ? MessageFlags.EPHEMERAL : undefined;
}
let allowedMentions =
typeof this.options.allowedMentions === 'undefined'
? this.target.client.options.allowedMentions
: this.options.allowedMentions;
if (allowedMentions) {
allowedMentions = Util.cloneObject(allowedMentions);
allowedMentions.replied_user = allowedMentions.repliedUser;
delete allowedMentions.repliedUser;
}
let message_reference;
if (typeof this.options.replyTo !== 'undefined') {
const message_id = this.isMessage
? this.target.channel.messages.resolveID(this.options.replyTo)
: this.target.messages.resolveID(this.options.replyTo);
if (message_id) {
message_reference = { message_id };
}
}
this.data = {
@ -209,10 +184,8 @@ class APIMessage {
embeds,
username,
avatar_url: avatarURL,
allowed_mentions:
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
allowed_mentions: this.options.allowedMentions,
flags,
message_reference,
};
return this;
}
@ -266,8 +239,8 @@ class APIMessage {
data = { ...this.data, content: this.data.content[i] };
opt = { ...this.options, content: this.data.content[i] };
} else {
data = { content: this.data.content[i], tts: this.data.tts, allowed_mentions: this.options.allowedMentions };
opt = { content: this.data.content[i], tts: this.data.tts, allowedMentions: this.options.allowedMentions };
data = { content: this.data.content[i], tts: this.data.tts };
opt = { content: this.data.content[i], tts: this.data.tts };
}
const apiMessage = new APIMessage(this.target, opt);

View file

@ -1,102 +0,0 @@
'use strict';
const Base = require('./Base');
const { ApplicationCommandOptionType } = require('../util/Constants');
const Snowflake = require('../util/Snowflake');
/**
* Represents an application command, see {@link InteractionClient}.
* @extends {Base}
*/
class ApplicationCommand extends Base {
constructor(client, data, guildID) {
super(client);
/**
* The ID of the guild this command is part of, if any.
* @type {Snowflake?}
* @readonly
*/
this.guildID = guildID || null;
this._patch(data);
}
_patch(data) {
/**
* The ID of this command.
* @type {Snowflake}
* @readonly
*/
this.id = data.id;
/**
* The ID of the application which owns this command.
* @type {Snowflake}
* @readonly
*/
this.appplicationID = data.application_id;
/**
* The name of this command.
* @type {string}
* @readonly
*/
this.name = data.name;
/**
* The description of this command.
* @type {string}
* @readonly
*/
this.description = data.description;
/**
* The options of this command.
* @type {Object[]}
* @readonly
*/
this.options = data.options?.map(function m(o) {
return {
type: ApplicationCommandOptionType[o.type],
name: o.name,
description: o.description,
default: o.default,
required: o.required,
choices: o.choices,
options: o.options ? o.options.map(m) : undefined,
};
});
}
/**
* The timestamp the command was created at.
* @type {number}
* @readonly
*/
get createdTimestamp() {
return Snowflake.deconstruct(this.id).timestamp;
}
/**
* The time the command was created at.
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Delete this command.
*/
async delete() {
let path = this.client.api.applications('@me');
if (this.guildID) {
path = path.guilds(this.guildID);
}
await path.commands(this.id).delete();
}
}
module.exports = ApplicationCommand;

View file

@ -4,7 +4,6 @@ const Util = require('../util/Util');
/**
* Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models).
* @abstract
*/
class Base {
constructor(client) {

View file

@ -5,7 +5,6 @@ const Emoji = require('./Emoji');
/**
* Parent class for {@link GuildEmoji} and {@link GuildPreviewEmoji}.
* @extends {Emoji}
* @abstract
*/
class BaseGuildEmoji extends Emoji {
constructor(client, data, guild) {
@ -17,10 +16,6 @@ class BaseGuildEmoji extends Emoji {
*/
this.guild = guild;
this.requiresColons = null;
this.managed = null;
this.available = null;
/**
* Array of role ids this emoji is active for
* @name BaseGuildEmoji#_roles
@ -35,29 +30,26 @@ class BaseGuildEmoji extends Emoji {
_patch(data) {
if (data.name) this.name = data.name;
if (typeof data.require_colons !== 'undefined') {
/**
* Whether or not this emoji requires colons surrounding it
* @type {?boolean}
*/
this.requiresColons = data.require_colons;
}
/**
* Whether or not this emoji requires colons surrounding it
* @type {boolean}
* @name GuildEmoji#requiresColons
*/
if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons;
if (typeof data.managed !== 'undefined') {
/**
* Whether this emoji is managed by an external service
* @type {?boolean}
*/
this.managed = data.managed;
}
/**
* Whether this emoji is managed by an external service
* @type {boolean}
* @name GuildEmoji#managed
*/
if (typeof data.managed !== 'undefined') this.managed = data.managed;
if (typeof data.available !== 'undefined') {
/**
* Whether this emoji is available
* @type {?boolean}
*/
this.available = data.available;
}
/**
* Whether this emoji is available
* @type {boolean}
* @name GuildEmoji#available
*/
if (typeof data.available !== 'undefined') this.available = data.available;
if (data.roles) this._roles = data.roles;
}

View file

@ -9,7 +9,7 @@ const GuildChannel = require('./GuildChannel');
class CategoryChannel extends GuildChannel {
/**
* Channels that are a part of this category
* @type {Collection<Snowflake, GuildChannel>}
* @type {?Collection<Snowflake, GuildChannel>}
* @readonly
*/
get children() {

View file

@ -7,7 +7,6 @@ const Snowflake = require('../util/Snowflake');
/**
* Represents any channel on Discord.
* @extends {Base}
* @abstract
*/
class Channel extends Base {
constructor(client, data) {
@ -91,19 +90,10 @@ class Channel extends Base {
/**
* Fetches this channel.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Channel>}
*/
fetch(force = false) {
return this.client.channels.fetch(this.id, true, force);
}
/**
* Indicates whether this channel is text-based.
* @returns {boolean}
*/
isText() {
return 'messages' in this;
fetch() {
return this.client.channels.fetch(this.id, true);
}
static create(client, data, guild) {

View file

@ -1,15 +1,46 @@
'use strict';
const Base = require('./Base');
const Team = require('./Team');
const Application = require('./interfaces/Application');
const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants');
const Snowflake = require('../util/Snowflake');
const AssetTypes = Object.keys(ClientApplicationAssetTypes);
/**
* Represents a Client OAuth2 Application.
* @extends {Application}
* @extends {Base}
*/
class ClientApplication extends Application {
class ClientApplication extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) {
super._patch(data);
/**
* The ID of the app
* @type {Snowflake}
*/
this.id = data.id;
/**
* The name of the app
* @type {string}
*/
this.name = data.name;
/**
* The app's description
* @type {string}
*/
this.description = data.description;
/**
* The app's icon hash
* @type {string}
*/
this.icon = data.icon;
/**
* The app's cover image
@ -41,6 +72,85 @@ class ClientApplication extends Application {
*/
this.owner = data.team ? new Team(this.client, data.team) : data.owner ? this.client.users.add(data.owner) : null;
}
/**
* The timestamp the app was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return Snowflake.deconstruct(this.id).timestamp;
}
/**
* The time the app was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* A link to the application's icon.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} URL to the icon
*/
iconURL({ format, size } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size });
}
/**
* A link to this application's cover image.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} URL to the cover image
*/
coverImage({ format, size } = {}) {
if (!this.cover) return null;
return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size });
}
/**
* Asset data.
* @typedef {Object} ClientAsset
* @property {Snowflake} id The asset ID
* @property {string} name The asset name
* @property {string} type The asset type
*/
/**
* Gets the clients rich presence assets.
* @returns {Promise<Array<ClientAsset>>}
*/
fetchAssets() {
return this.client.api.oauth2
.applications(this.id)
.assets.get()
.then(assets =>
assets.map(a => ({
id: a.id,
name: a.name,
type: AssetTypes[a.type - 1],
})),
);
}
/**
* When concatenated with a string, this automatically returns the application's name instead of the
* ClientApplication object.
* @returns {string}
* @example
* // Logs: Application name: My App
* console.log(`Application name: ${application}`);
*/
toString() {
return this.name;
}
toJSON() {
return super.toJSON({ createdTimestamp: true });
}
}
module.exports = ClientApplication;

View file

@ -94,9 +94,11 @@ class ClientUser extends Structures.get('User') {
* @property {PresenceStatusData} [status] Status of the user
* @property {boolean} [afk] Whether the user is AFK
* @property {Object} [activity] Activity the user is playing
* @property {Object|string} [activity.application] An application object or application id
* @property {string} [activity.application.id] The id of the application
* @property {string} [activity.name] Name of the activity
* @property {ActivityType|number} [activity.type] Type of the activity
* @property {string} [activity.url] Twitch / YouTube stream URL
* @property {string} [activity.url] Stream url
* @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on
*/
@ -139,10 +141,10 @@ class ClientUser extends Structures.get('User') {
}
/**
* Options for setting an activity.
* Options for setting an activity
* @typedef ActivityOptions
* @type {Object}
* @property {string} [url] Twitch / YouTube stream URL
* @property {string} [url] Twitch stream URL
* @property {ActivityType|number} [type] Type of the activity
* @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on
*/

View file

@ -61,11 +61,10 @@ class DMChannel extends Channel {
/**
* Fetch this DMChannel.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<DMChannel>}
*/
fetch(force = false) {
return this.recipient.createDM(force);
fetch() {
return this.recipient.createDM();
}
/**

View file

@ -82,7 +82,7 @@ class Emoji extends Base {
* @example
* // Send a custom emoji from a guild:
* const emoji = guild.emojis.cache.first();
* msg.channel.send(`Hello! ${emoji}`);
* msg.reply(`Hello! ${emoji}`);
* @example
* // Send the emoji used in a reaction to the channel the reaction is part of
* reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`);

View file

@ -1,15 +1,12 @@
'use strict';
const { deprecate } = require('util');
const Base = require('./Base');
const GuildAuditLogs = require('./GuildAuditLogs');
const GuildPreview = require('./GuildPreview');
const GuildTemplate = require('./GuildTemplate');
const Integration = require('./Integration');
const Invite = require('./Invite');
const VoiceRegion = require('./VoiceRegion');
const Webhook = require('./Webhook');
const { Error, TypeError } = require('../errors');
const GuildChannelManager = require('../managers/GuildChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildMemberManager = require('../managers/GuildMemberManager');
@ -18,7 +15,6 @@ const RoleManager = require('../managers/RoleManager');
const VoiceStateManager = require('../managers/VoiceStateManager');
const Collection = require('../util/Collection');
const {
browser,
ChannelTypes,
DefaultMessageNotifications,
PartialTypes,
@ -133,17 +129,11 @@ class Guild extends Base {
this.icon = data.icon;
/**
* The hash of the guild invite splash image
* The hash of the guild splash image (VIP only)
* @type {?string}
*/
this.splash = data.splash;
/**
* The hash of the guild discovery splash image
* @type {?string}
*/
this.discoverySplash = data.discovery_splash;
/**
* The region the guild is located in
* @type {string}
@ -157,7 +147,7 @@ class Guild extends Base {
this.memberCount = data.member_count || this.memberCount;
/**
* Whether the guild is "large" (has more than large_threshold members, 50 by default)
* Whether the guild is "large" (has more than 250 members)
* @type {boolean}
*/
this.large = Boolean('large' in data ? data.large : this.large);
@ -167,17 +157,15 @@ class Guild extends Base {
* * ANIMATED_ICON
* * BANNER
* * COMMERCE
* * COMMUNITY
* * DISCOVERABLE
* * FEATURABLE
* * INVITE_SPLASH
* * PUBLIC
* * NEWS
* * PARTNERED
* * RELAY_ENABLED
* * VANITY_URL
* * VERIFIED
* * VIP_REGIONS
* * WELCOME_SCREEN_ENABLED
* @typedef {string} Features
*/
@ -214,7 +202,6 @@ class Guild extends Base {
/**
* Whether embedded images are enabled on this guild
* @type {boolean}
* @deprecated
*/
this.embedEnabled = data.embed_enabled;
@ -233,38 +220,35 @@ class Guild extends Base {
*/
this.premiumTier = data.premium_tier;
/**
* The total number of users currently boosting this server
* @type {?number}
* @name Guild#premiumSubscriptionCount
*/
if (typeof data.premium_subscription_count !== 'undefined') {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count;
}
if (typeof data.widget_enabled !== 'undefined') {
/**
* Whether widget images are enabled on this guild
* @type {?boolean}
*/
this.widgetEnabled = data.widget_enabled;
}
/**
* Whether widget images are enabled on this guild
* @type {?boolean}
* @name Guild#widgetEnabled
*/
if (typeof data.widget_enabled !== 'undefined') this.widgetEnabled = data.widget_enabled;
if (typeof data.widget_channel_id !== 'undefined') {
/**
* The widget channel ID, if enabled
* @type {?string}
*/
this.widgetChannelID = data.widget_channel_id;
}
/**
* The widget channel ID, if enabled
* @type {?string}
* @name Guild#widgetChannelID
*/
if (typeof data.widget_channel_id !== 'undefined') this.widgetChannelID = data.widget_channel_id;
if (typeof data.embed_channel_id !== 'undefined') {
/**
* The embed channel ID, if enabled
* @type {?string}
* @deprecated
*/
this.embedChannelID = data.embed_channel_id;
}
/**
* The embed channel ID, if enabled
* @type {?string}
* @name Guild#embedChannelID
*/
if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id;
/**
* The verification level of the guild
@ -303,64 +287,28 @@ class Guild extends Base {
*/
this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze();
if (typeof data.max_members !== 'undefined') {
/**
* The maximum amount of members the guild can have
* @type {?number}
*/
this.maximumMembers = data.max_members;
} else if (typeof this.maximumMembers === 'undefined') {
this.maximumMembers = null;
}
if (typeof data.max_presences !== 'undefined') {
/**
* The maximum amount of presences the guild can have
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
*/
this.maximumPresences = data.max_presences || 25000;
} else if (typeof this.maximumPresences === 'undefined') {
this.maximumPresences = null;
}
if (typeof data.approximate_member_count !== 'undefined') {
/**
* The approximate amount of members the guild has
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
*/
this.approximateMemberCount = data.approximate_member_count;
} else if (typeof this.approximateMemberCount === 'undefined') {
this.approximateMemberCount = null;
}
if (typeof data.approximate_presence_count !== 'undefined') {
/**
* The approximate amount of presences the guild has
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
*/
this.approximatePresenceCount = data.approximate_presence_count;
} else if (typeof this.approximatePresenceCount === 'undefined') {
this.approximatePresenceCount = null;
}
/**
* The maximum amount of members the guild can have
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
* @name Guild#maximumMembers
*/
if (typeof data.max_members !== 'undefined') this.maximumMembers = data.max_members || 250000;
/**
* The vanity invite code of the guild, if any
* The maximum amount of presences the guild can have
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
* @name Guild#maximumPresences
*/
if (typeof data.max_presences !== 'undefined') this.maximumPresences = data.max_presences || 25000;
/**
* The vanity URL code of the guild, if any
* @type {?string}
*/
this.vanityURLCode = data.vanity_url_code;
/* eslint-disable max-len */
/**
* The use count of the vanity URL code of the guild, if any
* <info>You will need to fetch this parameter using {@link Guild#fetchVanityData} if you want to receive it</info>
* @type {?number}
*/
this.vanityURLUses = null;
/* eslint-enable max-len */
/**
* The description of the guild, if any
* @type {?string}
@ -379,22 +327,18 @@ class Guild extends Base {
/**
* The ID of the rules channel for the guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?Snowflake}
*/
this.rulesChannelID = data.rules_channel_id;
/**
* The ID of the community updates channel for the guild
* The ID of the public updates channel for the guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?Snowflake}
*/
this.publicUpdatesChannelID = data.public_updates_channel_id;
/**
* The preferred locale of the guild, defaults to `en-US`
* @type {string}
*/
this.preferredLocale = data.preferred_locale;
if (data.channels) {
this.channels.cache.clear();
for (const rawChannel of data.channels) {
@ -519,14 +463,11 @@ class Guild extends Base {
* @readonly
*/
get nameAcronym() {
return this.name
.replace(/'s /g, ' ')
.replace(/\w+/g, e => e[0])
.replace(/\s/g, '');
return this.name.replace(/\w+/g, name => name[0]).replace(/\s/g, '');
}
/**
* The URL to this guild's invite splash image.
* The URL to this guild's splash.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -535,16 +476,6 @@ class Guild extends Base {
return this.client.rest.cdn.Splash(this.id, this.splash, format, size);
}
/**
* The URL to this guild's discovery splash image.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
discoverySplashURL({ format, size } = {}) {
if (!this.discoverySplash) return null;
return this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, format, size);
}
/**
* The owner of the guild
* @type {?GuildMember}
@ -590,7 +521,6 @@ class Guild extends Base {
* Embed channel for this guild
* @type {?TextChannel}
* @readonly
* @deprecated
*/
get embedChannel() {
return this.client.channels.cache.get(this.embedChannelID) || null;
@ -598,6 +528,7 @@ class Guild extends Base {
/**
* Rules channel for this guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?TextChannel}
* @readonly
*/
@ -607,6 +538,7 @@ class Guild extends Base {
/**
* Public updates channel for this guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?TextChannel}
* @readonly
*/
@ -637,6 +569,18 @@ class Guild extends Base {
return this.voiceStates.cache.get(this.client.user.id);
}
/**
* Returns the GuildMember form of a User object, if the user is present in the guild.
* @param {UserResolvable} user The user that you want to obtain the GuildMember of
* @returns {?GuildMember}
* @example
* // Get the guild member of a user
* const member = guild.member(message.author);
*/
member(user) {
return this.members.resolve(user);
}
/**
* Fetches this guild.
* @returns {Promise<Guild>}
@ -644,7 +588,7 @@ class Guild extends Base {
fetch() {
return this.client.api
.guilds(this.id)
.get({ query: { with_counts: true } })
.get()
.then(data => {
this._patch(data);
return this;
@ -698,8 +642,6 @@ class Guild extends Base {
/**
* Fetches a collection of integrations to this guild.
* Resolves with a collection mapping integrations by their ids.
* @param {Object} [options] Options for fetching integrations
* @param {boolean} [options.includeApplications] Whether to include bot and Oauth2 webhook integrations
* @returns {Promise<Collection<string, Integration>>}
* @example
* // Fetch integrations
@ -707,14 +649,10 @@ class Guild extends Base {
* .then(integrations => console.log(`Fetched ${integrations.size} integrations`))
* .catch(console.error);
*/
fetchIntegrations({ includeApplications = false } = {}) {
fetchIntegrations() {
return this.client.api
.guilds(this.id)
.integrations.get({
query: {
include_applications: includeApplications,
},
})
.integrations.get()
.then(data =>
data.reduce(
(collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)),
@ -723,20 +661,6 @@ class Guild extends Base {
);
}
/**
* Fetches a collection of templates from this guild.
* Resolves with a collection mapping templates by their codes.
* @returns {Promise<Collection<string, GuildTemplate>>}
*/
fetchTemplates() {
return this.client.api
.guilds(this.id)
.templates.get()
.then(templates =>
templates.reduce((col, data) => col.set(data.code, new GuildTemplate(this.client, data)), new Collection()),
);
}
/**
* The data for creating an integration.
* @typedef {Object} IntegrationData
@ -757,19 +681,6 @@ class Guild extends Base {
.then(() => this);
}
/**
* Creates a template for the guild.
* @param {string} name The name for the template
* @param {string} [description] The description for the template
* @returns {Promise<GuildTemplate>}
*/
createTemplate(name, description) {
return this.client.api
.guilds(this.id)
.templates.post({ data: { name, description } })
.then(data => new GuildTemplate(this.client, data));
}
/**
* Fetches a collection of invites to this guild.
* Resolves with a collection mapping invites by their codes.
@ -800,7 +711,7 @@ class Guild extends Base {
}
/**
* Obtains a guild preview for this guild from Discord.
* Obtains a guild preview for this guild from Discord, only available for public guilds.
* @returns {Promise<GuildPreview>}
*/
fetchPreview() {
@ -814,7 +725,6 @@ class Guild extends Base {
* Fetches the vanity url invite code to this guild.
* Resolves with a string matching the vanity url invite code, not the full url.
* @returns {Promise<string>}
* @deprecated
* @example
* // Fetch invites
* guild.fetchVanityCode()
@ -824,36 +734,13 @@ class Guild extends Base {
* .catch(console.error);
*/
fetchVanityCode() {
return this.fetchVanityData().then(vanity => vanity.code);
}
/**
* An object containing information about a guild's vanity invite.
* @typedef {Object} Vanity
* @property {?string} code Vanity invite code
* @property {?number} uses How many times this invite has been used
*/
/**
* Fetches the vanity url invite object to this guild.
* Resolves with an object containing the vanity url invite code and the use count
* @returns {Promise<Vanity>}
* @example
* // Fetch invite data
* guild.fetchVanityData()
* .then(res => {
* console.log(`Vanity URL: https://discord.gg/${res.code} with ${res.uses} uses`);
* })
* .catch(console.error);
*/
async fetchVanityData() {
if (!this.features.includes('VANITY_URL')) {
throw new Error('VANITY_URL');
return Promise.reject(new Error('VANITY_URL'));
}
const data = await this.client.api.guilds(this.id, 'vanity-url').get();
this.vanityURLUses = data.uses;
return data;
return this.client.api
.guilds(this.id, 'vanity-url')
.get()
.then(res => res.code);
}
/**
@ -892,23 +779,15 @@ class Guild extends Base {
}
/**
* Data for the Guild Widget object
* @typedef {Object} GuildWidget
* @property {boolean} enabled Whether the widget is enabled
* @property {?GuildChannel} channel The widget channel
*/
/**
* The Guild Widget object
* @typedef {Object} GuildWidgetData
* @property {boolean} enabled Whether the widget is enabled
* @property {?GuildChannelResolvable} channel The widget channel
* The Guild Embed object
* @typedef {Object} GuildEmbedData
* @property {boolean} enabled Whether the embed is enabled
* @property {?GuildChannel} channel The embed channel
*/
/**
* Fetches the guild embed.
* @returns {Promise<GuildWidget>}
* @deprecated
* @returns {Promise<GuildEmbedData>}
* @example
* // Fetches the guild embed
* guild.fetchEmbed()
@ -916,26 +795,13 @@ class Guild extends Base {
* .catch(console.error);
*/
fetchEmbed() {
return this.fetchWidget();
}
/**
* Fetches the guild widget.
* @returns {Promise<GuildWidget>}
* @example
* // Fetches the guild widget
* guild.fetchWidget()
* .then(widget => console.log(`The widget is ${widget.enabled ? 'enabled' : 'disabled'}`))
* .catch(console.error);
*/
async fetchWidget() {
const data = await this.client.api.guilds(this.id).widget.get();
this.widgetEnabled = this.embedEnabled = data.enabled;
this.widgetChannelID = this.embedChannelID = data.channel_id;
return {
enabled: data.enabled,
channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null,
};
return this.client.api
.guilds(this.id)
.embed.get()
.then(data => ({
enabled: data.enabled,
channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null,
}));
}
/**
@ -982,25 +848,29 @@ class Guild extends Base {
* @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`)
* @returns {Promise<GuildMember>}
*/
async addMember(user, options) {
addMember(user, options) {
user = this.client.users.resolveID(user);
if (!user) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
if (this.members.cache.has(user)) return this.members.cache.get(user);
if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable'));
if (this.members.cache.has(user)) return Promise.resolve(this.members.cache.get(user));
options.access_token = options.accessToken;
if (options.roles) {
const roles = [];
for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) {
role = this.roles.resolve(role);
if (!role) {
throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
return Promise.reject(
new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true),
);
}
roles.push(role.id);
}
options.roles = roles;
}
const data = await this.client.api.guilds(this.id).members(user).put({ data: options });
// Data is an empty buffer if the member is already part of the guild.
return data instanceof (browser ? ArrayBuffer : Buffer) ? this.members.fetch(user) : this.members.add(data);
return this.client.api
.guilds(this.id)
.members(user)
.put({ data: options })
.then(data => this.members.add(data));
}
/**
@ -1015,14 +885,10 @@ class Guild extends Base {
* @property {number} [afkTimeout] The AFK timeout of the guild
* @property {Base64Resolvable} [icon] The icon of the guild
* @property {GuildMemberResolvable} [owner] The owner of the guild
* @property {Base64Resolvable} [splash] The invite splash image of the guild
* @property {Base64Resolvable} [discoverySplash] The discovery splash image of the guild
* @property {Base64Resolvable} [splash] The splash screen of the guild
* @property {Base64Resolvable} [banner] The banner of the guild
* @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild
* @property {ChannelResolvable} [rulesChannel] The rules channel of the guild
* @property {ChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild
* @property {string} [preferredLocale] The preferred locale of the guild
*/
/**
@ -1059,7 +925,6 @@ class Guild extends Base {
if (typeof data.icon !== 'undefined') _data.icon = data.icon;
if (data.owner) _data.owner_id = this.client.users.resolveID(data.owner);
if (data.splash) _data.splash = data.splash;
if (data.discoverySplash) _data.discovery_splash = data.discoverySplash;
if (data.banner) _data.banner = data.banner;
if (typeof data.explicitContentFilter !== 'undefined') {
_data.explicit_content_filter =
@ -1076,13 +941,6 @@ class Guild extends Base {
if (typeof data.systemChannelFlags !== 'undefined') {
_data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags);
}
if (typeof data.rulesChannel !== 'undefined') {
_data.rules_channel_id = this.client.channels.resolveID(data.rulesChannel);
}
if (typeof data.publicUpdatesChannel !== 'undefined') {
_data.public_updates_channel_id = this.client.channels.resolveID(data.publicUpdatesChannel);
}
if (data.preferredLocale) _data.preferred_locale = data.preferredLocale;
return this.client.api
.guilds(this.id)
.patch({ data: _data, reason })
@ -1129,7 +987,7 @@ class Guild extends Base {
* @example
* // Edit the guild name
* guild.setName('Discord Guild')
* .then(updated => console.log(`Updated guild name to ${updated.name}`))
* .then(updated => console.log(`Updated guild name to ${guild}`))
* .catch(console.error);
*/
setName(name, reason) {
@ -1242,9 +1100,9 @@ class Guild extends Base {
}
/**
* Sets a new guild invite splash image.
* @param {Base64Resolvable|BufferResolvable} splash The new invite splash image of the guild
* @param {string} [reason] Reason for changing the guild's invite splash image
* Sets a new guild splash screen.
* @param {Base64Resolvable|BufferResolvable} splash The new splash screen of the guild
* @param {string} [reason] Reason for changing the guild's splash screen
* @returns {Promise<Guild>}
* @example
* // Edit the guild splash
@ -1256,21 +1114,6 @@ class Guild extends Base {
return this.edit({ splash: await DataResolver.resolveImage(splash), reason });
}
/**
* Sets a new guild discovery splash image.
* @param {Base64Resolvable|BufferResolvable} discoverySplash The new discovery splash image of the guild
* @param {string} [reason] Reason for changing the guild's discovery splash image
* @returns {Promise<Guild>}
* @example
* // Edit the guild discovery splash
* guild.setDiscoverySplash('./discoverysplash.png')
* .then(updated => console.log('Updated the guild discovery splash'))
* .catch(console.error);
*/
async setDiscoverySplash(discoverySplash, reason) {
return this.edit({ discoverySplash: await DataResolver.resolveImage(discoverySplash), reason });
}
/**
* Sets a new guild banner.
* @param {Base64Resolvable|BufferResolvable} banner The new banner of the guild
@ -1285,51 +1128,6 @@ class Guild extends Base {
return this.edit({ banner: await DataResolver.resolveImage(banner), reason });
}
/**
* Edits the rules channel of the guild.
* @param {ChannelResolvable} rulesChannel The new rules channel
* @param {string} [reason] Reason for changing the guild's rules channel
* @returns {Promise<Guild>}
* @example
* // Edit the guild rules channel
* guild.setRulesChannel(channel)
* .then(updated => console.log(`Updated guild rules channel to ${guild.rulesChannel.name}`))
* .catch(console.error);
*/
setRulesChannel(rulesChannel, reason) {
return this.edit({ rulesChannel }, reason);
}
/**
* Edits the community updates channel of the guild.
* @param {ChannelResolvable} publicUpdatesChannel The new community updates channel
* @param {string} [reason] Reason for changing the guild's community updates channel
* @returns {Promise<Guild>}
* @example
* // Edit the guild community updates channel
* guild.setPublicUpdatesChannel(channel)
* .then(updated => console.log(`Updated guild community updates channel to ${guild.publicUpdatesChannel.name}`))
* .catch(console.error);
*/
setPublicUpdatesChannel(publicUpdatesChannel, reason) {
return this.edit({ publicUpdatesChannel }, reason);
}
/**
* Edits the preferred locale of the guild.
* @param {string} preferredLocale The new preferred locale of the guild
* @param {string} [reason] Reason for changing the guild's preferred locale
* @returns {Promise<Guild>}
* @example
* // Edit the guild preferred locale
* guild.setPreferredLocale('en-US')
* .then(updated => console.log(`Updated guild preferred locale to ${guild.preferredLocale}`))
* .catch(console.error);
*/
setPreferredLocale(preferredLocale, reason) {
return this.edit({ preferredLocale }, reason);
}
/**
* The data needed for updating a channel's position.
* @typedef {Object} ChannelPosition
@ -1404,51 +1202,23 @@ class Guild extends Base {
/**
* Edits the guild's embed.
* @param {GuildWidgetData} embed The embed for the guild
* @param {GuildEmbedData} embed The embed for the guild
* @param {string} [reason] Reason for changing the guild's embed
* @returns {Promise<Guild>}
* @deprecated
*/
setEmbed(embed, reason) {
return this.setWidget(embed, reason);
}
/**
* Edits the guild's widget.
* @param {GuildWidgetData} widget The widget for the guild
* @param {string} [reason] Reason for changing the guild's widget
* @returns {Promise<Guild>}
*/
setWidget(widget, reason) {
return this.client.api
.guilds(this.id)
.widget.patch({
.embed.patch({
data: {
enabled: widget.enabled,
channel_id: this.channels.resolveID(widget.channel),
enabled: embed.enabled,
channel_id: this.channels.resolveID(embed.channel),
},
reason,
})
.then(() => this);
}
/**
* Get the commands associated with this guild.
* @returns {ApplicationCommand[]} A list of commands.
*/
getCommands() {
return this.client.interactionClient.getCommands(this.id);
}
/**
* Create a command. See {@link InteractionClient}.
* @param {Object} command The command description.
* @returns {ApplicationCommand} The created command.
*/
createCommand(command) {
return this.client.interactionClient.createCommand(command, this.id);
}
/**
* Leaves the guild.
* @returns {Promise<Guild>}
@ -1497,7 +1267,6 @@ class Guild extends Base {
this.id === guild.id &&
this.available === guild.available &&
this.splash === guild.splash &&
this.discoverySplash === guild.discoverySplash &&
this.region === guild.region &&
this.name === guild.name &&
this.memberCount === guild.memberCount &&
@ -1542,7 +1311,6 @@ class Guild extends Base {
});
json.iconURL = this.iconURL();
json.splashURL = this.splashURL();
json.discoverySplashURL = this.discoverySplashURL();
json.bannerURL = this.bannerURL();
return json;
}
@ -1565,24 +1333,9 @@ class Guild extends Base {
_sortedChannels(channel) {
const category = channel.type === ChannelTypes.CATEGORY;
return Util.discordSort(
this.channels.cache.filter(
c =>
(['text', 'news', 'store'].includes(channel.type)
? ['text', 'news', 'store'].includes(c.type)
: c.type === channel.type) &&
(category || c.parent === channel.parent),
),
this.channels.cache.filter(c => c.type === channel.type && (category || c.parent === channel.parent)),
);
}
}
Guild.prototype.setEmbed = deprecate(Guild.prototype.setEmbed, 'Guild#setEmbed: Use setWidget instead');
Guild.prototype.fetchEmbed = deprecate(Guild.prototype.fetchEmbed, 'Guild#fetchEmbed: Use fetchWidget instead');
Guild.prototype.fetchVanityCode = deprecate(
Guild.prototype.fetchVanityCode,
'Guild#fetchVanityCode: Use fetchVanityData() instead',
);
module.exports = Guild;

View file

@ -24,7 +24,7 @@ const Util = require('../util/Util');
/**
* Key mirror of all available audit log targets.
* @name GuildAuditLogs.Targets
* @type {Object<string, string>}
* @type {AuditLogTargetType}
*/
const Targets = {
ALL: 'ALL',
@ -84,7 +84,7 @@ const Targets = {
/**
* All available actions keyed under their names to their numeric values.
* @name GuildAuditLogs.Actions
* @type {Object<string, number>}
* @type {AuditLogAction}
*/
const Actions = {
ALL: null,

View file

@ -17,7 +17,6 @@ const Util = require('../util/Util');
* - {@link NewsChannel}
* - {@link StoreChannel}
* @extends {Channel}
* @abstract
*/
class GuildChannel extends Channel {
/**
@ -53,7 +52,7 @@ class GuildChannel extends Channel {
* The ID of the category parent of this channel
* @type {?Snowflake}
*/
this.parentID = data.parent_id || null;
this.parentID = data.parent_id;
/**
* A map of permission overwrites in this channel for roles and users
@ -228,7 +227,7 @@ class GuildChannel extends Channel {
*/
updateOverwrite(userOrRole, options, reason) {
userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole);
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'));
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true));
const existing = this.permissionOverwrites.get(userOrRole.id);
if (existing) return existing.update(options, reason).then(() => this);
@ -251,7 +250,7 @@ class GuildChannel extends Channel {
*/
createOverwrite(userOrRole, options, reason) {
userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole);
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'));
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true));
const type = userOrRole instanceof Role ? 'role' : 'member';
const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options);
@ -299,7 +298,7 @@ class GuildChannel extends Channel {
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel
* @property {?Snowflake} [parentID] The parent ID of the channel
* @property {Snowflake} [parentID] The parent ID of the channel
* @property {boolean} [lockPermissions]
* Lock the permissions of the channel to what the parent's permissions are
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
@ -335,22 +334,8 @@ class GuildChannel extends Channel {
});
}
let permission_overwrites;
if (data.permissionOverwrites) {
permission_overwrites = data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
if (data.lockPermissions) {
if (data.parentID) {
const newParent = this.guild.channels.resolve(data.parentID);
if (newParent && newParent.type === 'category') {
permission_overwrites = newParent.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
} else if (this.parent) {
permission_overwrites = this.parent.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
}
const permission_overwrites =
data.permissionOverwrites && data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
const newData = await this.client.api.channels(this.id).patch({
data: {
@ -413,7 +398,7 @@ class GuildChannel extends Channel {
/**
* Sets a new topic for the guild channel.
* @param {?string} topic The new topic for the guild channel
* @param {string} topic The new topic for the guild channel
* @param {string} [reason] Reason for changing the guild channel's topic
* @returns {Promise<GuildChannel>}
* @example
@ -514,7 +499,7 @@ class GuildChannel extends Channel {
* @param {boolean} [options.nsfw=this.nsfw] Whether the new channel is nsfw (only text)
* @param {number} [options.bitrate=this.bitrate] Bitrate of the new channel in bits (only voice)
* @param {number} [options.userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice)
* @param {number} [options.rateLimitPerUser=this.rateLimitPerUser] Ratelimit per user for the new channel (only text)
* @param {number} [options.rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text)
* @param {ChannelResolvable} [options.parent=this.parent] Parent of the new channel
* @param {string} [options.reason] Reason for cloning this channel
* @returns {Promise<GuildChannel>}

View file

@ -11,19 +11,13 @@ const Permissions = require('../util/Permissions');
*/
class GuildEmoji extends BaseGuildEmoji {
/**
* @name GuildEmoji
* @kind constructor
* @memberof GuildEmoji
* @param {Client} client The instantiating client
* @param {Object} data The data for the guild emoji
* @param {Guild} guild The guild the guild emoji is part of
*/
constructor(client, data, guild) {
super(client, data, guild);
/**
* The user who created this emoji
* @type {?User}
*/
this.author = null;
}
/**
* The guild this emoji is part of
@ -37,11 +31,6 @@ class GuildEmoji extends BaseGuildEmoji {
return clone;
}
_patch(data) {
super._patch(data);
if (typeof data.user !== 'undefined') this.author = this.client.users.add(data.user);
}
/**
* Whether the emoji is deletable by the client user
* @type {boolean}
@ -65,18 +54,20 @@ class GuildEmoji extends BaseGuildEmoji {
* Fetches the author for this emoji
* @returns {Promise<User>}
*/
async fetchAuthor() {
fetchAuthor() {
if (this.managed) {
throw new Error('EMOJI_MANAGED');
return Promise.reject(new Error('EMOJI_MANAGED'));
} else {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
if (!this.guild.me) return Promise.reject(new Error('GUILD_UNCACHED_ME'));
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) {
throw new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild);
return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild));
}
}
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();
this._patch(data);
return this.author;
return this.client.api
.guilds(this.guild.id)
.emojis(this.id)
.get()
.then(emoji => this.client.users.add(emoji.user));
}
/**

View file

@ -1,12 +1,13 @@
'use strict';
const Base = require('./Base');
const { Presence } = require('./Presence');
const Role = require('./Role');
const VoiceState = require('./VoiceState');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { Error } = require('../errors');
const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager');
const Permissions = require('../util/Permissions');
let Structures;
/**
* Represents a member of a guild on Discord.
@ -28,6 +29,13 @@ class GuildMember extends Base {
*/
this.guild = guild;
/**
* The user that this guild member instance represents
* @type {User}
* @name GuildMember#user
*/
if (data.user) this.user = client.users.add(data.user, true);
/**
* The timestamp the member joined the guild at
* @type {?number}
@ -58,29 +66,23 @@ class GuildMember extends Base {
*/
this.deleted = false;
/**
* The nickname of this member, if they have one
* @type {?string}
*/
this.nickname = null;
this._roles = [];
if (data) this._patch(data);
}
_patch(data) {
if ('user' in data) {
/**
* The user that this guild member instance represents
* @type {User}
*/
this.user = this.client.users.add(data.user, true);
}
/**
* The nickname of this member, if they have one
* @type {?string}
* @name GuildMember#nickname
*/
if (typeof data.nick !== 'undefined') this.nickname = data.nick;
if ('nick' in data) this.nickname = data.nick;
if ('joined_at' in data) this.joinedTimestamp = new Date(data.joined_at).getTime();
if ('premium_since' in data) this.premiumSinceTimestamp = new Date(data.premium_since).getTime();
if ('roles' in data) this._roles = data.roles;
if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime();
if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime();
if (data.user) this.user = this.guild.client.users.add(data.user);
if (data.roles) this._roles = data.roles;
}
_clone() {
@ -123,8 +125,6 @@ class GuildMember extends Base {
* @readonly
*/
get voice() {
if (!Structures) Structures = require('../util/Structures');
const VoiceState = Structures.get('VoiceState');
return this.guild.voiceStates.cache.get(this.id) || new VoiceState(this.guild, { user_id: this.id });
}
@ -152,8 +152,6 @@ class GuildMember extends Base {
* @readonly
*/
get presence() {
if (!Structures) Structures = require('../util/Structures');
const Presence = Structures.get('Presence');
return (
this.guild.presences.cache.get(this.id) ||
new Presence(this.client, {
@ -196,7 +194,7 @@ class GuildMember extends Base {
/**
* The nickname of this member, or their username if they don't have one
* @type {?string}
* @type {string}
* @readonly
*/
get displayName() {
@ -267,8 +265,7 @@ class GuildMember extends Base {
*/
hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) {
if (checkOwner && this.user.id === this.guild.ownerID) return true;
const permissions = new Permissions(this.roles.cache.map(role => role.permissions));
return permissions.has(permission, checkAdmin);
return this.roles.cache.some(r => r.permissions.has(permission, checkAdmin));
}
/**
@ -359,7 +356,7 @@ class GuildMember extends Base {
/**
* Bans this guild member.
* @param {Object} [options] Options for the ban
* @param {number} [options.days=0] Number of days of messages to delete, must be between 0 and 7
* @param {number} [options.days=0] Number of days of messages to delete
* @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember>}
* @example
@ -374,11 +371,10 @@ class GuildMember extends Base {
/**
* Fetches this GuildMember.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<GuildMember>}
*/
fetch(force = false) {
return this.guild.members.fetch({ user: this.id, cache: true, force });
fetch() {
return this.guild.members.fetch(this.id, true);
}
/**

View file

@ -5,7 +5,7 @@ const GuildPreviewEmoji = require('./GuildPreviewEmoji');
const Collection = require('../util/Collection');
/**
* Represents the data about the guild any bot can preview, connected to the specified guild.
* Represents the data about the guild any bot can preview, connected to the specified public guild.
* @extends {Base}
*/
class GuildPreview extends Base {
@ -18,37 +18,37 @@ class GuildPreview extends Base {
}
/**
* Builds the guild with the provided data.
* @param {*} data The raw data of the guild
* Builds the public guild with the provided data.
* @param {*} data The raw data of the public guild
* @private
*/
_patch(data) {
/**
* The id of this guild
* The id of this public guild
* @type {string}
*/
this.id = data.id;
/**
* The name of this guild
* The name of this public guild
* @type {string}
*/
this.name = data.name;
/**
* The icon of this guild
* The icon of this public guild
* @type {?string}
*/
this.icon = data.icon;
/**
* The splash icon of this guild
* The splash icon of this public guild
* @type {?string}
*/
this.splash = data.splash;
/**
* The discovery splash icon of this guild
* The discovery splash icon of this public guild
* @type {?string}
*/
this.discoverySplash = data.discovery_splash;
@ -60,26 +60,26 @@ class GuildPreview extends Base {
this.features = data.features;
/**
* The approximate count of members in this guild
* The approximate count of members in this public guild
* @type {number}
*/
this.approximateMemberCount = data.approximate_member_count;
/**
* The approximate count of online members in this guild
* The approximate count of online members in this public guild
* @type {number}
*/
this.approximatePresenceCount = data.approximate_presence_count;
/**
* The description for this guild
* The description for this public guild
* @type {?string}
*/
this.description = data.description || null;
this.description = data.description;
if (!this.emojis) {
/**
* Collection of emojis belonging to this guild
* Collection of emojis belonging to this public guild
* @type {Collection<Snowflake, GuildPreviewEmoji>}
*/
this.emojis = new Collection();
@ -92,7 +92,7 @@ class GuildPreview extends Base {
}
/**
* The URL to this guild's splash.
* The URL to this public guild's splash.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -102,7 +102,7 @@ class GuildPreview extends Base {
}
/**
* The URL to this guild's discovery splash.
* The URL to this public guild's discovery splash.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -112,7 +112,7 @@ class GuildPreview extends Base {
}
/**
* The URL to this guild's icon.
* The URL to this public guild's icon.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -122,7 +122,7 @@ class GuildPreview extends Base {
}
/**
* Fetches this guild.
* Fetches this public guild.
* @returns {Promise<GuildPreview>}
*/
fetch() {

View file

@ -1,225 +0,0 @@
'use strict';
const Base = require('./Base');
const { Events } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
/**
* Represents the template for a guild.
* @extends {Base}
*/
class GuildTemplate extends Base {
/**
* @param {Client} client The instantiating client
* @param {Object} data The raw data for the template
*/
constructor(client, data) {
super(client);
this._patch(data);
}
/**
* Builds or updates the template with the provided data.
* @param {Object} data The raw data for the template
* @returns {GuildTemplate}
* @private
*/
_patch(data) {
/**
* The unique code of this template
* @type {string}
*/
this.code = data.code;
/**
* The name of this template
* @type {string}
*/
this.name = data.name;
/**
* The description of this template
* @type {?string}
*/
this.description = data.description;
/**
* The amount of times this template has been used
* @type {number}
*/
this.usageCount = data.usage_count;
/**
* The ID of the user that created this template
* @type {Snowflake}
*/
this.creatorID = data.creator_id;
/**
* The user that created this template
* @type {User}
*/
this.creator = this.client.users.add(data.creator);
/**
* The time of when this template was created at
* @type {Date}
*/
this.createdAt = new Date(data.created_at);
/**
* The time of when this template was last synced to the guild
* @type {Date}
*/
this.updatedAt = new Date(data.updated_at);
/**
* The ID of the guild that this template belongs to
* @type {Snowflake}
*/
this.guildID = data.source_guild_id;
/**
* The data of the guild that this template would create
* @type {Object}
* @see {@link https://discord.com/developers/docs/resources/guild#guild-resource}
*/
this.serializedGuild = data.serialized_source_guild;
/**
* Whether this template has unsynced changes
* @type {?boolean}
*/
this.unSynced = 'is_dirty' in data ? Boolean(data.is_dirty) : null;
return this;
}
/**
* Creates a guild based from this template.
* <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild
* @param {BufferResolvable|Base64Resolvable} [icon] The icon for the guild
* @returns {Promise<Guild>}
*/
async createGuild(name, icon) {
const { client } = this;
const data = await client.api.guilds.templates(this.code).post({
data: {
name,
icon: await DataResolver.resolveImage(icon),
},
});
// eslint-disable-next-line consistent-return
return new Promise(resolve => {
const createdGuild = client.guilds.cache.get(data.id);
if (createdGuild) return resolve(createdGuild);
const resolveGuild = guild => {
client.off(Events.GUILD_CREATE, handleGuild);
client.decrementMaxListeners();
resolve(guild);
};
const handleGuild = guild => {
if (guild.id === data.id) {
client.clearTimeout(timeout);
resolveGuild(guild);
}
};
client.incrementMaxListeners();
client.on(Events.GUILD_CREATE, handleGuild);
const timeout = client.setTimeout(() => resolveGuild(client.guilds.add(data)), 10000);
});
}
/**
* Updates the metadata on this template.
* @param {Object} options Options for the template
* @param {string} [options.name] The name of this template
* @param {string} [options.description] The description of this template
* @returns {Promise<GuildTemplate>}
*/
edit({ name, description } = {}) {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.patch({ data: { name, description } })
.then(data => this._patch(data));
}
/**
* Deletes this template.
* @returns {Promise<GuildTemplate>}
*/
delete() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.delete()
.then(() => this);
}
/**
* Syncs this template to the current state of the guild.
* @returns {Promise<GuildTemplate>}
*/
sync() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.put()
.then(data => this._patch(data));
}
/**
* The timestamp of when this template was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return this.createdAt.getTime();
}
/**
* The timestamp of when this template was last synced to the guild
* @type {number}
* @readonly
*/
get updatedTimestamp() {
return this.updatedAt.getTime();
}
/**
* The guild that this template belongs to
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.cache.get(this.guildID) || null;
}
/**
* The URL of this template
* @type {string}
* @readonly
*/
get url() {
return `${this.client.options.http.template}/${this.code}`;
}
/**
* When concatenated with a string, this automatically returns the templates's code instead of the template object.
* @returns {string}
* @example
* // Logs: Template: FKvmczH2HyUf
* console.log(`Template: ${guildTemplate}!`);
*/
toString() {
return this.code;
}
}
module.exports = GuildTemplate;

View file

@ -1,7 +1,6 @@
'use strict';
const Base = require('./Base');
const IntegrationApplication = require('./IntegrationApplication');
/**
* The information account for an integration
@ -59,15 +58,11 @@ class Integration extends Base {
*/
this.role = this.guild.roles.cache.get(data.role_id);
if (data.user) {
/**
* The user for this integration
* @type {?User}
*/
this.user = this.client.users.add(data.user);
} else {
this.user = null;
}
/**
* The user for this integration
* @type {User}
*/
this.user = this.client.users.add(data.user);
/**
* The account integration information
@ -95,20 +90,6 @@ class Integration extends Base {
* @type {number}
*/
this.expireGracePeriod = data.expire_grace_period;
if ('application' in data) {
if (this.application) {
this.application._patch(data.application);
} else {
/**
* The application for this integration
* @type {?IntegrationApplication}
*/
this.application = new IntegrationApplication(this.client, data.application);
}
} else if (!this.application) {
this.application = null;
}
}
/**

View file

@ -1,25 +0,0 @@
'use strict';
const Application = require('./interfaces/Application');
/**
* Represents an Integration's OAuth2 Application.
* @extends {Application}
*/
class IntegrationApplication extends Application {
_patch(data) {
super._patch(data);
if (typeof data.bot !== 'undefined') {
/**
* The bot {@link User user} for this application
* @type {?User}
*/
this.bot = this.client.users.add(data.bot);
} else if (!this.bot) {
this.bot = null;
}
}
}
module.exports = IntegrationApplication;

View file

@ -1,133 +0,0 @@
'use strict';
const APIMessage = require('./APIMessage');
const Base = require('./Base');
const Snowflake = require('../util/Snowflake');
/**
* Represents an interaction, see {@link InteractionClient}.
* @extends {Base}
*/
class Interaction extends Base {
constructor(client, data, syncHandle) {
super(client);
this.syncHandle = syncHandle;
this._patch(data);
}
_patch(data) {
/**
* The ID of this interaction.
* @type {Snowflake}
* @readonly
*/
this.id = data.id;
/**
* The token of this interaction.
* @type {string}
* @readonly
*/
this.token = data.token;
/**
* The ID of the invoked command.
* @type {Snowflake}
* @readonly
*/
this.commandID = data.data.id;
/**
* The name of the invoked command.
* @type {string}
* @readonly
*/
this.commandName = data.data.name;
/**
* The options passed to the command.
* @type {Object}
* @readonly
*/
this.options = data.data.options;
/**
* The channel this interaction was sent in.
* @type {?Channel}
* @readonly
*/
this.channel = this.client.channels?.cache.get(data.channel_id) || null;
/**
* The guild this interaction was sent in, if any.
* @type {?Guild}
* @readonly
*/
this.guild = data.guild_id ? this.client.guilds?.cache.get(data.guild_id) : null;
/**
* If this interaction was sent in a guild, the member which sent it.
* @type {?Member}
* @readonly
*/
this.member = data.member ? this.guild?.members.add(data.member, false) : null;
}
/**
* The timestamp the interaction was created at.
* @type {number}
* @readonly
*/
get createdTimestamp() {
return Snowflake.deconstruct(this.id).timestamp;
}
/**
* The time the interaction was created at.
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Acknowledge this interaction without content.
*/
async acknowledge() {
await this.syncHandle.acknowledge();
}
/**
* Reply to this interaction.
* @param {(StringResolvable | APIMessage)?} content The content for the message.
* @param {(MessageOptions | MessageAdditions)?} options The options to provide.
*/
async reply(content, options) {
let apiMessage;
if (content instanceof APIMessage) {
apiMessage = content.resolveData();
} else {
apiMessage = APIMessage.create(this, content, options).resolveData();
if (Array.isArray(apiMessage.data.content)) {
throw new Error('Message is too long');
}
}
const resolved = await apiMessage.resolveFiles();
if (!this.syncHandle.reply(resolved)) {
const clientID =
this.client.interactionClient.clientID || (await this.client.api.oauth2.applications('@me').get()).id;
await this.client.api.webhooks(clientID, this.token).post({
auth: false,
data: resolved.data,
files: resolved.files,
});
}
}
}
module.exports = Interaction;

View file

@ -13,7 +13,6 @@ const Collection = require('../util/Collection');
const { MessageTypes } = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/Snowflake');
const Util = require('../util/Util');
/**
@ -24,14 +23,14 @@ class Message extends Base {
/**
* @param {Client} client The instantiating client
* @param {Object} data The data for the message
* @param {TextChannel|DMChannel|NewsChannel} channel The channel the message was sent in
* @param {TextChannel|DMChannel} channel The channel the message was sent in
*/
constructor(client, data, channel) {
super(client);
/**
* The channel that the message was sent in
* @type {TextChannel|DMChannel|NewsChannel}
* @type {TextChannel|DMChannel}
*/
this.channel = channel;
@ -51,62 +50,35 @@ class Message extends Base {
*/
this.id = data.id;
if ('type' in data) {
/**
* The type of the message
* @type {?MessageType}
*/
this.type = MessageTypes[data.type];
/**
* The type of the message
* @type {MessageType}
*/
this.type = MessageTypes[data.type];
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {?boolean}
*/
this.system = data.type !== 0;
} else if (typeof this.type !== 'string') {
this.system = null;
this.type = null;
}
/**
* The content of the message
* @type {string}
*/
this.content = data.content;
if ('content' in data) {
/**
* The content of the message
* @type {?string}
*/
this.content = data.content;
} else if (typeof this.content !== 'string') {
this.content = null;
}
/**
* The author of the message
* @type {?User}
*/
this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null;
if ('author' in data) {
/**
* The author of the message
* @type {?User}
*/
this.author = this.client.users.add(data.author, !data.webhook_id);
} else if (!this.author) {
this.author = null;
}
/**
* Whether or not this message is pinned
* @type {boolean}
*/
this.pinned = data.pinned;
if ('pinned' in data) {
/**
* Whether or not this message is pinned
* @type {?boolean}
*/
this.pinned = Boolean(data.pinned);
} else if (typeof this.pinned !== 'boolean') {
this.pinned = null;
}
if ('tts' in data) {
/**
* Whether or not the message was Text-To-Speech
* @type {?boolean}
*/
this.tts = data.tts;
} else if (typeof this.tts !== 'boolean') {
this.tts = null;
}
/**
* Whether or not the message was Text-To-Speech
* @type {boolean}
*/
this.tts = data.tts;
/**
* A random number or string used for checking message delivery
@ -114,7 +86,13 @@ class Message extends Base {
* lost if re-fetched</warn>
* @type {?string}
*/
this.nonce = 'nonce' in data ? data.nonce : null;
this.nonce = data.nonce;
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {boolean}
*/
this.system = data.type !== 0;
/**
* A list of embeds in the message - e.g. YouTube Player
@ -137,13 +115,13 @@ class Message extends Base {
* The timestamp the message was sent at
* @type {number}
*/
this.createdTimestamp = SnowflakeUtil.deconstruct(this.id).timestamp;
this.createdTimestamp = new Date(data.timestamp).getTime();
/**
* The timestamp the message was last edited at (if applicable)
* @type {?number}
*/
this.editedTimestamp = 'edited_timestamp' in data ? new Date(data.edited_timestamp).getTime() : null;
this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
/**
* A manager of the reactions belonging to this message
@ -205,11 +183,11 @@ class Message extends Base {
this.flags = new MessageFlags(data.flags).freeze();
/**
* Reference data sent in a crossposted message or inline reply.
* Reference data sent in a crossposted message.
* @typedef {Object} MessageReference
* @property {string} channelID ID of the channel the message was referenced
* @property {?string} guildID ID of the guild the message was referenced
* @property {?string} messageID ID of the message that was referenced
* @property {string} channelID ID of the channel the message was crossposted from
* @property {?string} guildID ID of the guild the message was crossposted from
* @property {?string} messageID ID of the message that was crossposted
*/
/**
@ -223,10 +201,6 @@ class Message extends Base {
messageID: data.message_reference.message_id,
}
: null;
if (data.referenced_message) {
this.channel.messages.add(data.referenced_message);
}
}
/**
@ -239,18 +213,13 @@ class Message extends Base {
}
/**
* Updates the message and returns the old message.
* Updates the message.
* @param {Object} data Raw Discord message update data
* @returns {Message}
* @private
*/
patch(data) {
const clone = this._clone();
const { messageEditHistoryMaxSize } = this.client.options;
if (messageEditHistoryMaxSize !== 0) {
const editsLimit = messageEditHistoryMaxSize === -1 ? Infinity : messageEditHistoryMaxSize;
if (this._edits.unshift(clone) > editsLimit) this._edits.pop();
}
this._edits.unshift(clone);
if ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime();
if ('content' in data) this.content = data.content;
@ -271,14 +240,12 @@ class Message extends Base {
this.mentions = new Mentions(
this,
'mentions' in data ? data.mentions : this.mentions.users,
'mention_roles' in data ? data.mention_roles : this.mentions.roles,
'mentions_roles' in data ? data.mentions_roles : this.mentions.roles,
'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone,
'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels,
);
this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze();
return clone;
}
/**
@ -288,7 +255,7 @@ class Message extends Base {
* @readonly
*/
get member() {
return this.guild ? this.guild.members.resolve(this.author) || null : null;
return this.guild ? this.guild.member(this.author) || null : null;
}
/**
@ -324,7 +291,7 @@ class Message extends Base {
* @readonly
*/
get url() {
return `https://discord.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`;
return `https://discordapp.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`;
}
/**
@ -429,42 +396,12 @@ class Message extends Base {
);
}
/**
* The Message this crosspost/reply/pin-add references, if cached
* @type {?Message}
* @readonly
*/
get referencedMessage() {
if (!this.reference) return null;
const referenceChannel = this.client.channels.resolve(this.reference.channelID);
if (!referenceChannel) return null;
return referenceChannel.messages.resolve(this.reference.messageID);
}
/**
* Whether the message is crosspostable by the client user
* @type {boolean}
* @readonly
*/
get crosspostable() {
return (
this.channel.type === 'news' &&
!this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) &&
this.type === 'DEFAULT' &&
this.channel.viewable &&
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.SEND_MESSAGES) &&
(this.author.id === this.client.user.id ||
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES))
);
}
/**
* Options that can be passed into editMessage.
* @typedef {Object} MessageEditOptions
* @property {string} [content] Content to be edited
* @property {MessageEmbed|Object} [embed] An embed to be added/edited
* @property {Object} [embed] An embed to be added/edited
* @property {string|boolean} [code] Language for optional codeblock formatting to apply
* @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
*/
/**
@ -488,57 +425,27 @@ class Message extends Base {
});
}
/**
* Publishes a message in an announcement channel to all channels following it.
* @returns {Promise<Message>}
* @example
* // Crosspost a message
* if (message.channel.type === 'news') {
* message.crosspost()
* .then(() => console.log('Crossposted message'))
* .catch(console.error);
* }
*/
async crosspost() {
await this.client.api.channels(this.channel.id).messages(this.id).crosspost.post();
return this;
}
/**
* Pins this message to the channel's pinned messages.
* @param {Object} [options] Options for pinning
* @param {string} [options.reason] Reason for pinning
* @returns {Promise<Message>}
* @example
* // Pin a message with a reason
* message.pin({ reason: 'important' })
* .then(console.log)
* .catch(console.error)
*/
pin(options) {
pin() {
return this.client.api
.channels(this.channel.id)
.pins(this.id)
.put(options)
.put()
.then(() => this);
}
/**
* Unpins this message from the channel's pinned messages.
* @param {Object} [options] Options for unpinning
* @param {string} [options.reason] Reason for unpinning
* @returns {Promise<Message>}
* @example
* // Unpin a message with a reason
* message.unpin({ reason: 'no longer relevant' })
* .then(console.log)
* .catch(console.error)
*/
unpin(options) {
unpin() {
return this.client.api
.channels(this.channel.id)
.pins(this.id)
.delete(options)
.delete()
.then(() => this);
}
@ -585,12 +492,12 @@ class Message extends Base {
* @returns {Promise<Message>}
* @example
* // Delete a message
* message.delete({ timeout: 5000 })
* .then(msg => console.log(`Deleted message from ${msg.author.username} after 5 seconds`))
* message.delete()
* .then(msg => console.log(`Deleted message from ${msg.author.username}`))
* .catch(console.error);
*/
delete(options = {}) {
if (typeof options !== 'object') return Promise.reject(new TypeError('INVALID_TYPE', 'options', 'object', true));
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
const { timeout = 0, reason } = options;
if (timeout <= 0) {
return this.channel.messages.delete(this.id, reason).then(() => this);
@ -604,29 +511,30 @@ class Message extends Base {
}
/**
* Send an inline reply to this message.
* Replies to the message.
* @param {StringResolvable|APIMessage} [content=''] The content for the message
* @param {MessageOptions|MessageAdditions} [options] The additional options to provide
* @param {MessageResolvable} [options.replyTo=this] The message to reply to
* @param {MessageOptions|MessageAdditions} [options={}] The options to provide
* @returns {Promise<Message|Message[]>}
* @example
* // Reply to a message
* message.reply('Hey, I\'m a reply!')
* .then(() => console.log(`Sent a reply to ${message.author.username}`))
* .catch(console.error);
*/
reply(content, options) {
return this.channel.send(
content instanceof APIMessage
? content
: APIMessage.transformOptions(content, options, {
replyTo: this,
}),
: APIMessage.transformOptions(content, options, { reply: this.member || this.author }),
);
}
/**
* Fetch this message.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Message>}
*/
fetch(force = false) {
return this.channel.messages.fetch(this.id, true, force);
fetch() {
return this.channel.messages.fetch(this.id, true);
}
/**

View file

@ -42,7 +42,7 @@ class MessageCollector extends Collector {
this._handleChannelDeletion = this._handleChannelDeletion.bind(this);
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
this.client.incrementMaxListeners();
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1);
this.client.on(Events.MESSAGE_CREATE, this.handleCollect);
this.client.on(Events.MESSAGE_DELETE, this.handleDispose);
this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
@ -55,7 +55,7 @@ class MessageCollector extends Collector {
this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion);
this.client.decrementMaxListeners();
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1);
});
}

View file

@ -7,13 +7,6 @@ const Util = require('../util/Util');
* Represents an embed in a message (image/video preview, rich embed, etc.)
*/
class MessageEmbed {
/**
* @name MessageEmbed
* @kind constructor
* @memberof MessageEmbed
* @param {MessageEmbed|Object} [data={}] MessageEmbed to clone or raw embed data
*/
constructor(data = {}, skipValidation = false) {
this.setup(data, skipValidation);
}
@ -29,40 +22,39 @@ class MessageEmbed {
* * `link` - a link embed
* @type {string}
*/
this.type = data.type || 'rich';
this.type = data.type;
/**
* The title of this embed
* @type {?string}
*/
this.title = 'title' in data ? data.title : null;
this.title = data.title;
/**
* The description of this embed
* @type {?string}
*/
this.description = 'description' in data ? data.description : null;
this.description = data.description;
/**
* The URL of this embed
* @type {?string}
*/
this.url = 'url' in data ? data.url : null;
this.url = data.url;
/**
* The color of this embed
* @type {?number}
*/
this.color = 'color' in data ? Util.resolveColor(data.color) : null;
this.color = Util.resolveColor(data.color);
/**
* The timestamp of this embed
* @type {?number}
*/
this.timestamp = 'timestamp' in data ? new Date(data.timestamp).getTime() : null;
this.timestamp = data.timestamp ? new Date(data.timestamp).getTime() : null;
/**
* Represents a field of a MessageEmbed
* @typedef {Object} EmbedField
* @property {string} name The name of this field
* @property {string} value The value of this field
@ -79,7 +71,6 @@ class MessageEmbed {
}
/**
* Represents the thumbnail of a MessageEmbed
* @typedef {Object} MessageEmbedThumbnail
* @property {string} url URL for this thumbnail
* @property {string} proxyURL ProxyURL for this thumbnail
@ -101,7 +92,6 @@ class MessageEmbed {
: null;
/**
* Represents the image of a MessageEmbed
* @typedef {Object} MessageEmbedImage
* @property {string} url URL for this image
* @property {string} proxyURL ProxyURL for this image
@ -123,7 +113,6 @@ class MessageEmbed {
: null;
/**
* Represents the video of a MessageEmbed
* @typedef {Object} MessageEmbedVideo
* @property {string} url URL of this video
* @property {string} proxyURL ProxyURL for this video
@ -146,7 +135,6 @@ class MessageEmbed {
: null;
/**
* Represents the author field of a MessageEmbed
* @typedef {Object} MessageEmbedAuthor
* @property {string} name The name of this author
* @property {string} url URL of this author
@ -168,7 +156,6 @@ class MessageEmbed {
: null;
/**
* Represents the provider of a MessageEmbed
* @typedef {Object} MessageEmbedProvider
* @property {string} name The name of this provider
* @property {string} url URL of this provider
@ -186,7 +173,6 @@ class MessageEmbed {
: null;
/**
* Represents the footer field of a MessageEmbed
* @typedef {Object} MessageEmbedFooter
* @property {string} text The text of this footer
* @property {string} iconURL URL of the icon for this footer

View file

@ -79,14 +79,14 @@ class MessageMentions {
}
/**
* Cached members for {@link MessageMentions#members}
* Cached members for {@link MessageMention#members}
* @type {?Collection<Snowflake, GuildMember>}
* @private
*/
this._members = null;
/**
* Cached channels for {@link MessageMentions#channels}
* Cached channels for {@link MessageMention#channels}
* @type {?Collection<Snowflake, GuildChannel>}
* @private
*/
@ -138,7 +138,7 @@ class MessageMentions {
if (!this.guild) return null;
this._members = new Collection();
this.users.forEach(user => {
const member = this.guild.members.resolve(user);
const member = this.guild.member(user);
if (member) this._members.set(member.user.id, member);
});
return this._members;
@ -164,7 +164,7 @@ class MessageMentions {
/**
* Checks if a user, guild member, role, or channel is mentioned.
* Takes into account user mentions, role mentions, and @everyone/@here mentions.
* @param {UserResolvable|RoleResolvable|GuildChannelResolvable} data User/Role/Channel to check
* @param {UserResolvable|GuildMember|Role|GuildChannel} data User/GuildMember/Role/Channel to check
* @param {Object} [options] Options
* @param {boolean} [options.ignoreDirect=false] - Whether to ignore direct mentions to the item
* @param {boolean} [options.ignoreRoles=false] - Whether to ignore role mentions to a guild member
@ -179,11 +179,7 @@ class MessageMentions {
}
if (!ignoreDirect) {
const id =
this.client.users.resolveID(data) ||
(this.guild && this.guild.roles.resolveID(data)) ||
this.client.channels.resolveID(data);
const id = data.id || data;
return this.users.has(id) || this.channels.has(id) || this.roles.has(id);
}

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