mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
Merge branch 'master' into refactor/webpacks
This commit is contained in:
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -14,4 +14,4 @@ To get ready to work on the codebase, please do the following:
|
||||
3. If you're working on voice, also run `npm install node-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/hydrabolt/discord.js/compare)
|
||||
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare)
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "typings"]
|
||||
path = typings
|
||||
url = https://github.com/zajrik/discord.js-typings
|
||||
url = https://github.com/discordjs/discord.js-typings
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -175,7 +175,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2017 Amish Shah
|
||||
Copyright 2015 - 2018 Amish Shah
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
42
README.md
42
README.md
@@ -8,12 +8,12 @@
|
||||
<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/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
<a href="https://travis-ci.org/discordjs/discord.js"><img src="https://travis-ci.org/discordjs/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
<a href="https://www.patreon.com/discordjs"><img src="https://img.shields.io/badge/donate-patreon-F96854.svg" alt="Patreon" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
|
||||
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="npm installnfo" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -30,9 +30,9 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
|
||||
**Node.js 8.0.0 or newer is required.**
|
||||
Ignore any warnings about unmet peer dependencies, as they're all optional.
|
||||
|
||||
Without voice support: `npm i discord.js`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm i discord.js node-opus`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm i discord.js opusscript`
|
||||
Without voice support: `npm install discord.js`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
|
||||
|
||||
### Audio engines
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
@@ -40,13 +40,13 @@ Using opusscript is only recommended for development environments where node-opu
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
### Optional packages
|
||||
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm i zlib-sync`)
|
||||
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm i discordapp/erlpack`)
|
||||
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`)
|
||||
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
|
||||
- One of the following packages can be installed for faster voice packet encryption and decryption:
|
||||
- [sodium](https://www.npmjs.com/package/sodium) (`npm i sodium`)
|
||||
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm i libsodium-wrappers`)
|
||||
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm i uws`)
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm i bufferutil`)
|
||||
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
|
||||
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
|
||||
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws`)
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`)
|
||||
|
||||
## Example usage
|
||||
```js
|
||||
@@ -54,34 +54,34 @@ const Discord = require('discord.js');
|
||||
const client = new Discord.Client();
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
});
|
||||
|
||||
client.on('message', message => {
|
||||
if (message.content === 'ping') {
|
||||
message.reply('pong');
|
||||
client.on('message', msg => {
|
||||
if (msg.content === 'ping') {
|
||||
msg.reply('pong');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('your token');
|
||||
client.login('token');
|
||||
```
|
||||
|
||||
## Links
|
||||
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
|
||||
* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
|
||||
* [Documentation](https://discord.js.org/#/docs)
|
||||
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API Discord server](https://discord.gg/discord-api)
|
||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||
* [GitHub](https://github.com/discordjs/discord.js)
|
||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||
|
||||
### Extensions
|
||||
* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](https://github.com/devsnek/discord-rpc))
|
||||
* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
|
||||
|
||||
## Contributing
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[documentation](https://discord.js.org/#/docs).
|
||||
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
|
||||
## Help
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Version 11.1.0
|
||||
v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages.
|
||||
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.1.0) for a full list of changes, including
|
||||
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.1.0) for a full list of changes, including
|
||||
information about deprecations.
|
||||
|
||||
# Version 11
|
||||
Version 11 contains loads of new and improved features, optimisations, and bug fixes.
|
||||
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.0.0) for a full list of changes.
|
||||
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.0.0) for a full list of changes.
|
||||
|
||||
## Significant additions
|
||||
* Message Reactions and Embeds (rich text)
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<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/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
<a href="https://travis-ci.org/discordjs/discord.js"><img src="https://travis-ci.org/discordjs/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
|
||||
@@ -17,7 +17,10 @@
|
||||
</div>
|
||||
|
||||
# Welcome!
|
||||
Welcome to the discord.js v12.0.0 documentation.
|
||||
Welcome to the discord.js v12 documentation.
|
||||
|
||||
v12 is still very much a work-in-progress, as we're aiming to make it the best it can possibly be before releasing.
|
||||
Only use it if you are fond of living life on the bleeding edge.
|
||||
|
||||
## About
|
||||
discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the
|
||||
@@ -32,9 +35,9 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
|
||||
**Node.js 8.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 --save`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||
Without voice support: `npm install discord.js`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
|
||||
|
||||
### Audio engines
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
@@ -42,12 +45,13 @@ Using opusscript is only recommended for development environments where node-opu
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
### Optional packages
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`)
|
||||
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack --save`)
|
||||
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`)
|
||||
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
|
||||
- One of the following packages can be installed for faster voice packet encryption and decryption:
|
||||
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium --save`)
|
||||
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`)
|
||||
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`)
|
||||
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
|
||||
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
|
||||
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws`)
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`)
|
||||
|
||||
## Example usage
|
||||
```js
|
||||
@@ -55,31 +59,34 @@ const Discord = require('discord.js');
|
||||
const client = new Discord.Client();
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
});
|
||||
|
||||
client.on('message', message => {
|
||||
if (message.content === 'ping') {
|
||||
message.reply('pong');
|
||||
client.on('message', msg => {
|
||||
if (msg.content === 'ping') {
|
||||
msg.reply('pong');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('your token');
|
||||
client.login('token');
|
||||
```
|
||||
|
||||
## Links
|
||||
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
|
||||
* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
|
||||
* [Documentation](https://discord.js.org/#/docs)
|
||||
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API Discord server](https://discord.gg/discord-api)
|
||||
* [GitHub](https://github.com/discordjs/discord.js)
|
||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||
* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
|
||||
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||
|
||||
### Extensions
|
||||
* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
|
||||
|
||||
## Contributing
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[documentation](https://discord.js.org/#/docs).
|
||||
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
|
||||
## Help
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
|
||||
@@ -6,8 +6,8 @@ In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `
|
||||
To get started, make sure you have:
|
||||
* ffmpeg - `npm install ffmpeg-binaries`
|
||||
* an opus encoder, choose one from below:
|
||||
* `npm install node-opus` (better performance)
|
||||
* `npm install opusscript`
|
||||
* `npm install node-opus`
|
||||
* a good network connection
|
||||
|
||||
## Joining a voice channel
|
||||
@@ -20,7 +20,7 @@ const client = new Discord.Client();
|
||||
|
||||
client.login('token here');
|
||||
|
||||
client.on('message', message => {
|
||||
client.on('message', async message => {
|
||||
// Voice only works in guilds, if the message does not come from a guild,
|
||||
// we ignore it
|
||||
if (!message.guild) return;
|
||||
@@ -28,11 +28,7 @@ client.on('message', message => {
|
||||
if (message.content === '/join') {
|
||||
// Only try to join the sender's voice channel if they are in one themselves
|
||||
if (message.member.voiceChannel) {
|
||||
message.member.voiceChannel.join()
|
||||
.then(connection => { // Connection is an instance of VoiceConnection
|
||||
message.reply('I have successfully connected to the channel!');
|
||||
})
|
||||
.catch(console.log);
|
||||
const connection = await message.member.voiceChannel.join();
|
||||
} else {
|
||||
message.reply('You need to join a voice channel first!');
|
||||
}
|
||||
@@ -42,73 +38,98 @@ client.on('message', message => {
|
||||
|
||||
## Streaming to a Voice Channel
|
||||
In the previous example, we looked at how to join a voice channel in order to obtain a `VoiceConnection`. Now that we
|
||||
have obtained a voice connection, we can start streaming audio to it. The following example shows how to stream an mp3
|
||||
file:
|
||||
have obtained a voice connection, we can start streaming audio to it.
|
||||
|
||||
**Playing a file:**
|
||||
### Introduction to playing on voice connections
|
||||
The most basic example of playing audio over a connection would be playing a local file:
|
||||
|
||||
```js
|
||||
// Use an absolute path
|
||||
const dispatcher = connection.playFile('C:/Users/Discord/Desktop/myfile.mp3');
|
||||
const dispatcher = connection.play('/home/discord/audio.mp3');
|
||||
```
|
||||
|
||||
```js
|
||||
// Or an dynamic path
|
||||
const dispatcher = connection.playFile('./myfile.mp3');
|
||||
```
|
||||
|
||||
Your file doesn't have to be just an mp3; ffmpeg can convert videos and audios of many formats.
|
||||
|
||||
The `dispatcher` variable is an instance of a `StreamDispatcher`, which manages streaming a specific resource to a voice
|
||||
channel. We can do many things with the dispatcher, such as finding out when the stream ends or changing the volume:
|
||||
The `dispatcher` in this case is a `StreamDispatcher` - here you can control the volume and playback of the stream:
|
||||
|
||||
```js
|
||||
dispatcher.on('end', () => {
|
||||
// The song has finished
|
||||
dispatcher.pause();
|
||||
dispatcher.resume();
|
||||
|
||||
dispatcher.setVolume(0.5); // half the volume
|
||||
|
||||
dispatcher.on('finish', () => {
|
||||
console.log('Finished playing!');
|
||||
});
|
||||
|
||||
dispatcher.on('error', e => {
|
||||
// Catch any errors that may arise
|
||||
console.log(e);
|
||||
dispatcher.destroy(); // end the stream
|
||||
```
|
||||
|
||||
We can also pass in options when we first play the stream:
|
||||
|
||||
```js
|
||||
const dispatcher = connection.play('/home/discord/audio.mp3', {
|
||||
volume: 0.5,
|
||||
passes: 3
|
||||
});
|
||||
```
|
||||
|
||||
These are just a subset of the options available (consult documentation for a full list). Most users may be interested in the `passes` option, however. As audio is sent over UDP, there is a chance packets may not arrive. Increasing the number of passes, e.g. to `3` gives you a better chance that your packets reach your recipients, at the cost of triple the bandwidth. We recommend not going over 5 passes.
|
||||
|
||||
### What can I play?
|
||||
|
||||
Discord.js allows you to play a lot of things:
|
||||
|
||||
```js
|
||||
// ReadableStreams, in this example YouTube audio
|
||||
const ytdl = require('ytdl-core');
|
||||
connection.play(ytdl(
|
||||
'https://www.youtube.com/watch?v=ZlAU_w7-Xp8',
|
||||
{ filter: 'audioonly' }));
|
||||
|
||||
// Files on the internet
|
||||
connection.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
|
||||
|
||||
// Local files
|
||||
connection.play('/home/discord/audio.mp3');
|
||||
```
|
||||
|
||||
New to v12 is the ability to play OggOpus and WebmOpus streams with much better performance by skipping out Ffmpeg. Note this comes at the cost of no longer having volume control over the stream:
|
||||
|
||||
```js
|
||||
connection.play(fs.createReadStream('./media.webm'), {
|
||||
type: 'webm/opus'
|
||||
});
|
||||
|
||||
dispatcher.setVolume(0.5); // Set the volume to 50%
|
||||
dispatcher.setVolume(1); // Set the volume back to 100%
|
||||
|
||||
console.log(dispatcher.time); // The time in milliseconds that the stream dispatcher has been playing for
|
||||
|
||||
dispatcher.pause(); // Pause the stream
|
||||
dispatcher.resume(); // Carry on playing
|
||||
|
||||
dispatcher.end(); // End the dispatcher, emits 'end' event
|
||||
connection.play(fs.createReadStream('./media.ogg'), {
|
||||
type: 'ogg/opus'
|
||||
});
|
||||
```
|
||||
|
||||
If you have an existing [ReadableStream](https://nodejs.org/api/stream.html#stream_readable_streams),
|
||||
this can also be used:
|
||||
Make sure to consult the documentation for a full list of what you can play - there's too much to cover here!
|
||||
|
||||
**Playing a ReadableStream:**
|
||||
```js
|
||||
connection.playStream(myReadableStream);
|
||||
## Voice Broadcasts
|
||||
|
||||
// You can use fs.createReadStream to create an ReadableStream
|
||||
|
||||
const fs = require('fs');
|
||||
const stream = fs.createReadStream('./test.mp3');
|
||||
connection.playStream(stream);
|
||||
```
|
||||
|
||||
It's important to note that creating a readable stream to a file is less efficient than simply using `connection.playFile()`.
|
||||
|
||||
**Playing anything else:**
|
||||
|
||||
For anything else, such as a URL to a file, you can use `connection.playArbitraryInput()`. You should consult the [ffmpeg protocol documentation](https://ffmpeg.org/ffmpeg-protocols.html) to see what you can use this for.
|
||||
A voice broadcast is very useful for "radio" bots, that play the same audio across multiple channels. It means audio is only transcoded once, and is much better on performance.
|
||||
|
||||
```js
|
||||
// Play an mp3 from a URL
|
||||
connection.playArbitraryInput('http://mysite.com/sound.mp3');
|
||||
const broadcast = client.createVoiceBroadcast();
|
||||
|
||||
broadcast.on('subscribe', dispatcher => {
|
||||
console.log('New broadcast subscriber!');
|
||||
});
|
||||
|
||||
broadcast.on('unsubscribe', dispatcher => {
|
||||
console.log('Channel unsubscribed from broadcast :(');
|
||||
})
|
||||
```
|
||||
|
||||
Again, playing a file from a URL like this is more performant than creating a ReadableStream to the file.
|
||||
`broadcast` is an instance of `VoiceBroadcast`, which has the same `play` method you are used to with regular VoiceConnections:
|
||||
|
||||
## Advanced Topics
|
||||
soon:tm:
|
||||
```js
|
||||
const dispatcher = broadcast.play('./audio.mp3');
|
||||
|
||||
connection.play(broadcast);
|
||||
```
|
||||
|
||||
It's important to note that the `dispatcher` stored above is a `BroadcastDispatcher` - it controls all the dispatcher subscribed to the broadcast, e.g. setting the volume of this dispatcher affects the volume of all subscribers.
|
||||
|
||||
## Voice Receive
|
||||
coming soon™
|
||||
|
||||
@@ -17,7 +17,7 @@ const Discord = require('discord.js/browser');
|
||||
```
|
||||
|
||||
### Webpack File
|
||||
You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/hydrabolt/discord.js/tree/webpack) of the GitHub repository.
|
||||
You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/discordjs/discord.js/tree/webpack) of the GitHub repository.
|
||||
There is a file for each branch and version of the library, and the ones ending in `.min.js` are minified to substantially reduce the size of the source code.
|
||||
|
||||
Include the file on the page just as you would any other JS library, like so:
|
||||
|
||||
47
package.json
47
package.json
@@ -6,8 +6,8 @@
|
||||
"types": "./typings/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "npm run lint && npm run docs:test",
|
||||
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json --jsdoc jsdoc.json",
|
||||
"docs:test": "docgen --source src --custom docs/index.yml --jsdoc jsdoc.json",
|
||||
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
|
||||
"docs:test": "docgen --source src --custom docs/index.yml",
|
||||
"lint": "eslint src *.js",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"build:browser": "webpack",
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hydrabolt/discord.js.git"
|
||||
"url": "git+https://github.com/discordjs/discord.js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"discord",
|
||||
@@ -28,36 +28,33 @@
|
||||
"author": "Amish Shah <amishshah.2k@gmail.com>",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/hydrabolt/discord.js/issues"
|
||||
"url": "https://github.com/discordjs/discord.js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/hydrabolt/discord.js#readme",
|
||||
"homepage": "https://github.com/discordjs/discord.js#readme",
|
||||
"runkitExampleFilename": "./docs/examples/ping.js",
|
||||
"unpkg": "./webpack/discord.min.js",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.0",
|
||||
"prism-media": "^0.0.2",
|
||||
"snekfetch": "^3.5.0",
|
||||
"prism-media": "hydrabolt/prism-media",
|
||||
"snekfetch": "^3.6.0",
|
||||
"tweetnacl": "^1.0.0",
|
||||
"ws": "^3.3.1"
|
||||
"ws": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^3.0.0",
|
||||
"erlpack": "discordapp/erlpack",
|
||||
"node-opus": "^0.2.0",
|
||||
"opusscript": "^0.0.4",
|
||||
"sodium": "^2.0.0",
|
||||
"libsodium-wrappers": "^0.7.0",
|
||||
"uws": "^8.14.0",
|
||||
"uws": "^9.14.0",
|
||||
"zlib-sync": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^8.0.0",
|
||||
"discord.js-docgen": "hydrabolt/discord.js-docgen",
|
||||
"eslint": "^4.11.0",
|
||||
"jsdoc-strip-async-await": "^0.1.0",
|
||||
"@types/node": "^9.4.6",
|
||||
"discord.js-docgen": "discordjs/docgen",
|
||||
"eslint": "^4.17.0",
|
||||
"json-filter-loader": "^1.0.0",
|
||||
"uglifyjs-webpack-plugin": "^1.0.0-beta.2",
|
||||
"webpack": "^3.8.0"
|
||||
"uglifyjs-webpack-plugin": "^1.1.8",
|
||||
"webpack": "^3.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
@@ -78,21 +75,13 @@
|
||||
"src/sharding/ShardingManager.js": false,
|
||||
"src/client/voice/ClientVoiceManager.js": false,
|
||||
"src/client/voice/VoiceConnection.js": false,
|
||||
"src/client/voice/VoiceUDPClient.js": false,
|
||||
"src/client/voice/VoiceWebSocket.js": false,
|
||||
"src/client/voice/networking/VoiceUDPClient.js": false,
|
||||
"src/client/voice/networking/VoiceWebSocket.js": false,
|
||||
"src/client/voice/dispatcher/StreamDispatcher.js": false,
|
||||
"src/client/voice/opus/BaseOpusEngine.js": false,
|
||||
"src/client/voice/opus/NodeOpusEngine.js": false,
|
||||
"src/client/voice/opus/OpusEngineList.js": false,
|
||||
"src/client/voice/opus/OpusScriptEngine.js": false,
|
||||
"src/client/voice/pcm/ConverterEngine.js": false,
|
||||
"src/client/voice/pcm/ConverterEngineList.js": false,
|
||||
"src/client/voice/pcm/FfmpegConverterEngine.js": false,
|
||||
"src/client/voice/player/AudioPlayer.js": false,
|
||||
"src/client/voice/receiver/VoiceReadable.js": false,
|
||||
"src/client/voice/receiver/VoiceReceiver.js": false,
|
||||
"src/client/voice/receiver/PacketHandler.js": false,
|
||||
"src/client/voice/receiver/Receiver.js": false,
|
||||
"src/client/voice/util/Secretbox.js": false,
|
||||
"src/client/voice/util/SecretKey.js": false,
|
||||
"src/client/voice/util/VolumeInterface.js": false,
|
||||
"src/client/voice/VoiceBroadcast.js": false
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const UserStore = require('../stores/UserStore');
|
||||
const ChannelStore = require('../stores/ChannelStore');
|
||||
const GuildStore = require('../stores/GuildStore');
|
||||
const ClientPresenceStore = require('../stores/ClientPresenceStore');
|
||||
const EmojiStore = require('../stores/EmojiStore');
|
||||
const GuildEmojiStore = require('../stores/GuildEmojiStore');
|
||||
const { Events, browser } = require('../util/Constants');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const { Error, TypeError, RangeError } = require('../errors');
|
||||
@@ -70,11 +70,10 @@ class Client extends BaseClient {
|
||||
this.voice = !browser ? new ClientVoiceManager(this) : null;
|
||||
|
||||
/**
|
||||
* The shard helpers for the client
|
||||
* (only if the process was spawned as a child, such as from a {@link ShardingManager})
|
||||
* Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
|
||||
* @type {?ShardClientUtil}
|
||||
*/
|
||||
this.shard = !browser && process.send ? ShardClientUtil.singleton(this) : null;
|
||||
this.shard = !browser && process.env.SHARDING_MANAGER ? ShardClientUtil.singleton(this) : null;
|
||||
|
||||
/**
|
||||
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
|
||||
@@ -180,7 +179,7 @@ class Client extends BaseClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* How long it has been since the client last entered the `READY` state
|
||||
* How long it has been since the client last entered the `READY` state in milliseconds
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
@@ -209,11 +208,11 @@ class Client extends BaseClient {
|
||||
|
||||
/**
|
||||
* All custom emojis that the client has access to, mapped by their IDs
|
||||
* @type {EmojiStore<Snowflake, Emoji>}
|
||||
* @type {GuildEmojiStore<Snowflake, GuildEmoji>}
|
||||
* @readonly
|
||||
*/
|
||||
get emojis() {
|
||||
const emojis = new EmojiStore({ client: this });
|
||||
const emojis = new GuildEmojiStore({ client: this });
|
||||
for (const guild of this.guilds.values()) {
|
||||
if (guild.available) for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji);
|
||||
}
|
||||
@@ -288,6 +287,11 @@ class Client extends BaseClient {
|
||||
* Obtains an invite from Discord.
|
||||
* @param {InviteResolvable} invite Invite code or URL
|
||||
* @returns {Promise<Invite>}
|
||||
* @example
|
||||
* client.fetchInvite('https://discord.gg/bRCvFy9')
|
||||
* .then(invite => {
|
||||
* console.log(`Obtained invite with code: ${invite.code}`);
|
||||
* }).catch(console.error);
|
||||
*/
|
||||
fetchInvite(invite) {
|
||||
const code = DataResolver.resolveInviteCode(invite);
|
||||
@@ -300,6 +304,11 @@ class Client extends BaseClient {
|
||||
* @param {Snowflake} id ID of the webhook
|
||||
* @param {string} [token] Token for the webhook
|
||||
* @returns {Promise<Webhook>}
|
||||
* @example
|
||||
* client.fetchWebhook('id', 'token')
|
||||
* .then(webhook => {
|
||||
* console.log(`Obtained webhook with name: ${webhook.name}`);
|
||||
* }).catch(console.error);
|
||||
*/
|
||||
fetchWebhook(id, token) {
|
||||
return this.api.webhooks(id, token).get().then(data => new Webhook(this, data));
|
||||
@@ -308,6 +317,11 @@ class Client extends BaseClient {
|
||||
/**
|
||||
* Obtains the available voice regions from Discord.
|
||||
* @returns {Collection<string, VoiceRegion>}
|
||||
* @example
|
||||
* client.fetchVoiceRegions()
|
||||
* .then(regions => {
|
||||
* console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`);
|
||||
* }).catch(console.error);
|
||||
*/
|
||||
fetchVoiceRegions() {
|
||||
return this.api.voice.regions.get().then(res => {
|
||||
@@ -324,6 +338,10 @@ class Client extends BaseClient {
|
||||
* will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime}
|
||||
* @returns {number} Amount of messages that were removed from the caches,
|
||||
* or -1 if the message cache lifetime is unlimited
|
||||
* @example
|
||||
* // Remove all messages older than 1800 seconds from the messages cache
|
||||
* const amount = client.sweepMessages(1800);
|
||||
* console.log(`Successfully removed ${amount} messages from the cache.`);
|
||||
*/
|
||||
sweepMessages(lifetime = this.options.messageCacheLifetime) {
|
||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) {
|
||||
@@ -360,6 +378,11 @@ class Client extends BaseClient {
|
||||
* Obtains the OAuth Application of the bot from Discord.
|
||||
* @param {Snowflake} [id='@me'] ID of application to fetch
|
||||
* @returns {Promise<ClientApplication>}
|
||||
* @example
|
||||
* client.fetchApplication('id')
|
||||
* .then(application => {
|
||||
* console.log(`Obtained application with name: ${application.name}`);
|
||||
* }).catch(console.error);
|
||||
*/
|
||||
fetchApplication(id = '@me') {
|
||||
return this.api.oauth2.applications(id).get()
|
||||
@@ -375,7 +398,7 @@ class Client extends BaseClient {
|
||||
* client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'])
|
||||
* .then(link => {
|
||||
* console.log(`Generated bot invite link: ${link}`);
|
||||
* });
|
||||
* }).catch(console.error);
|
||||
*/
|
||||
generateInvite(permissions) {
|
||||
if (permissions) {
|
||||
|
||||
@@ -39,7 +39,10 @@ class ClientManager {
|
||||
this.client.emit(Events.DEBUG, `Authenticated using token ${token}`);
|
||||
this.client.token = token;
|
||||
const timeout = this.client.setTimeout(() => reject(new Error('WS_CONNECTION_TIMEOUT')), 1000 * 300);
|
||||
this.client.api.gateway.get().then(res => {
|
||||
this.client.api.gateway.get().then(async res => {
|
||||
if (this.client.options.presence != null) { // eslint-disable-line eqeqeq
|
||||
this.client.options.ws.presence = await this.client.presences._parse(this.client.options.presence);
|
||||
}
|
||||
const gateway = `${res.url}/`;
|
||||
this.client.emit(Events.DEBUG, `Using gateway ${gateway}`);
|
||||
this.client.ws.connect(gateway);
|
||||
|
||||
@@ -5,7 +5,7 @@ class ChannelCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const existing = client.channels.has(data.id);
|
||||
const channel = client.channels.create(data);
|
||||
const channel = client.channels.add(data);
|
||||
if (!existing && channel) {
|
||||
client.emit(Events.CHANNEL_CREATE, channel);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ class GuildBanRemove extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
const user = client.users.create(data.user);
|
||||
const user = client.users.add(data.user);
|
||||
if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const { Events } = require('../../util/Constants');
|
||||
|
||||
class GuildEmojiCreateAction extends Action {
|
||||
handle(guild, createdEmoji) {
|
||||
const emoji = guild.emojis.create(createdEmoji);
|
||||
const emoji = guild.emojis.add(createdEmoji);
|
||||
this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
|
||||
return { emoji };
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class GuildEmojiCreateAction extends Action {
|
||||
/**
|
||||
* Emitted whenever a custom emoji is created in a guild.
|
||||
* @event Client#emojiCreate
|
||||
* @param {Emoji} emoji The emoji that was created
|
||||
* @param {GuildEmoji} emoji The emoji that was created
|
||||
*/
|
||||
|
||||
module.exports = GuildEmojiCreateAction;
|
||||
|
||||
@@ -10,9 +10,9 @@ class GuildEmojiDeleteAction extends Action {
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a custom guild emoji is deleted.
|
||||
* Emitted whenever a custom emoji is deleted in a guild.
|
||||
* @event Client#emojiDelete
|
||||
* @param {Emoji} emoji The emoji that was deleted
|
||||
* @param {GuildEmoji} emoji The emoji that was deleted
|
||||
*/
|
||||
|
||||
module.exports = GuildEmojiDeleteAction;
|
||||
|
||||
@@ -10,10 +10,10 @@ class GuildEmojiUpdateAction extends Action {
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a custom guild emoji is updated.
|
||||
* Emitted whenever a custom emoji is updated in a guild.
|
||||
* @event Client#emojiUpdate
|
||||
* @param {Emoji} oldEmoji The old emoji
|
||||
* @param {Emoji} newEmoji The new emoji
|
||||
* @param {GuildEmoji} oldEmoji The old emoji
|
||||
* @param {GuildEmoji} newEmoji The new emoji
|
||||
*/
|
||||
|
||||
module.exports = GuildEmojiUpdateAction;
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
const Action = require('./Action');
|
||||
|
||||
function mappify(iterable) {
|
||||
const map = new Map();
|
||||
for (const x of iterable) map.set(...x);
|
||||
return map;
|
||||
}
|
||||
|
||||
class GuildEmojisUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const guild = this.client.guilds.get(data.guild_id);
|
||||
if (!guild || !guild.emojis) return;
|
||||
|
||||
const deletions = mappify(guild.emojis.entries());
|
||||
const deletions = new Map(guild.emojis);
|
||||
|
||||
for (const emoji of data.emojis) {
|
||||
// Determine type of emoji event
|
||||
|
||||
@@ -8,8 +8,8 @@ class GuildMemberRemoveAction extends Action {
|
||||
let member = null;
|
||||
if (guild) {
|
||||
member = guild.members.get(data.user.id);
|
||||
guild.memberCount--;
|
||||
if (member) {
|
||||
guild.memberCount--;
|
||||
guild.members.remove(member.id);
|
||||
if (client.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ class GuildRoleCreate extends Action {
|
||||
let role;
|
||||
if (guild) {
|
||||
const already = guild.roles.has(data.role.id);
|
||||
role = guild.roles.create(data.role);
|
||||
role = guild.roles.add(data.role);
|
||||
if (!already) client.emit(Events.GUILD_ROLE_CREATE, role);
|
||||
}
|
||||
return { role };
|
||||
|
||||
@@ -7,7 +7,7 @@ class GuildSync extends Action {
|
||||
const guild = client.guilds.get(data.id);
|
||||
if (guild) {
|
||||
if (data.presences) {
|
||||
for (const presence of data.presences) guild.presences.create(presence);
|
||||
for (const presence of data.presences) guild.presences.add(presence);
|
||||
}
|
||||
|
||||
if (data.members) {
|
||||
@@ -16,7 +16,7 @@ class GuildSync extends Action {
|
||||
if (member) {
|
||||
member._patch(syncMember);
|
||||
} else {
|
||||
guild.members.create(syncMember, false);
|
||||
guild.members.add(syncMember, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ class MessageCreateAction extends Action {
|
||||
if (channel) {
|
||||
const existing = channel.messages.get(data.id);
|
||||
if (existing) return { message: existing };
|
||||
const message = channel.messages.create(data);
|
||||
const message = channel.messages.add(data);
|
||||
const user = message.author;
|
||||
const member = channel.guild ? channel.guild.member(user) : null;
|
||||
channel.lastMessageID = data.id;
|
||||
|
||||
@@ -19,7 +19,7 @@ class MessageReactionAdd extends Action {
|
||||
if (!message) return false;
|
||||
if (!data.emoji) return false;
|
||||
// Verify reaction
|
||||
const reaction = message.reactions.create({
|
||||
const reaction = message.reactions.add({
|
||||
emoji: data.emoji,
|
||||
count: 0,
|
||||
me: user.id === this.client.user.id,
|
||||
@@ -33,7 +33,7 @@ class MessageReactionAdd extends Action {
|
||||
* Emitted whenever a reaction is added to a message.
|
||||
* @event Client#messageReactionAdd
|
||||
* @param {MessageReaction} messageReaction The reaction object
|
||||
* @param {User} user The user that applied the emoji or reaction emoji
|
||||
* @param {User} user The user that applied the guild or reaction emoji
|
||||
*/
|
||||
|
||||
module.exports = MessageReactionAdd;
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
const VolumeInterface = require('./util/VolumeInterface');
|
||||
const Prism = require('prism-media');
|
||||
const OpusEncoders = require('./opus/OpusEngineList');
|
||||
const Collection = require('../../util/Collection');
|
||||
|
||||
const ffmpegArguments = [
|
||||
'-analyzeduration', '0',
|
||||
'-loglevel', '0',
|
||||
'-f', 's16le',
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
];
|
||||
const EventEmitter = require('events');
|
||||
const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer');
|
||||
const DispatcherSet = require('./util/DispatcherSet');
|
||||
const PlayInterface = require('./util/PlayInterface');
|
||||
|
||||
/**
|
||||
* A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency.
|
||||
@@ -17,15 +9,15 @@ const ffmpegArguments = [
|
||||
* Example usage:
|
||||
* ```js
|
||||
* const broadcast = client.createVoiceBroadcast();
|
||||
* broadcast.playFile('./music.mp3');
|
||||
* broadcast.play('./music.mp3');
|
||||
* // Play "music.mp3" in all voice connections that the client is in
|
||||
* for (const connection of client.voiceConnections.values()) {
|
||||
* connection.playBroadcast(broadcast);
|
||||
* connection.play(broadcast);
|
||||
* }
|
||||
* ```
|
||||
* @implements {VolumeInterface}
|
||||
* @implements {PlayInterface}
|
||||
*/
|
||||
class VoiceBroadcast extends VolumeInterface {
|
||||
class VoiceBroadcast extends EventEmitter {
|
||||
constructor(client) {
|
||||
super();
|
||||
/**
|
||||
@@ -33,339 +25,36 @@ class VoiceBroadcast extends VolumeInterface {
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this._dispatchers = new Collection();
|
||||
this._encoders = new Collection();
|
||||
/**
|
||||
* Whether playing is paused
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.paused = false;
|
||||
/**
|
||||
* The audio transcoder that this broadcast uses
|
||||
* @type {Prism}
|
||||
*/
|
||||
this.prism = new Prism();
|
||||
/**
|
||||
* The current audio transcoder that is being used
|
||||
* @type {Object}
|
||||
*/
|
||||
this.currentTranscoder = null;
|
||||
this.tickInterval = null;
|
||||
this._volume = 1;
|
||||
this.dispatchers = new DispatcherSet(this);
|
||||
this.player = new BroadcastAudioPlayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of subscribed dispatchers
|
||||
* @type {StreamDispatcher[]}
|
||||
* @readonly
|
||||
* The current master dispatcher, if any. This dispatcher controls all that is played by subscribed dispatchers.
|
||||
* @type {?BroadcastDispatcher}
|
||||
*/
|
||||
get dispatchers() {
|
||||
let d = [];
|
||||
for (const container of this._dispatchers.values()) {
|
||||
d = d.concat(Array.from(container.values()));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
get _playableStream() {
|
||||
const currentTranscoder = this.currentTranscoder;
|
||||
if (!currentTranscoder) return null;
|
||||
const transcoder = currentTranscoder.transcoder;
|
||||
const options = currentTranscoder.options;
|
||||
return (transcoder && transcoder.output) || options.stream;
|
||||
}
|
||||
|
||||
unregisterDispatcher(dispatcher, old) {
|
||||
const volume = old || dispatcher.volume;
|
||||
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher unsubscribes from the broadcast.
|
||||
* @event VoiceBroadcast#unsubscribe
|
||||
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
|
||||
*/
|
||||
this.emit('unsubscribe', dispatcher);
|
||||
for (const container of this._dispatchers.values()) {
|
||||
container.delete(dispatcher);
|
||||
|
||||
if (!container.size) {
|
||||
this._encoders.get(volume).destroy();
|
||||
this._dispatchers.delete(volume);
|
||||
this._encoders.delete(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerDispatcher(dispatcher) {
|
||||
if (!this._dispatchers.has(dispatcher.volume)) {
|
||||
this._dispatchers.set(dispatcher.volume, new Set());
|
||||
this._encoders.set(dispatcher.volume, OpusEncoders.fetch());
|
||||
}
|
||||
const container = this._dispatchers.get(dispatcher.volume);
|
||||
if (!container.has(dispatcher)) {
|
||||
container.add(dispatcher);
|
||||
dispatcher.once('end', () => this.unregisterDispatcher(dispatcher));
|
||||
dispatcher.on('volumeChange', (o, n) => {
|
||||
this.unregisterDispatcher(dispatcher, o);
|
||||
if (!this._dispatchers.has(n)) {
|
||||
this._dispatchers.set(n, new Set());
|
||||
this._encoders.set(n, OpusEncoders.fetch());
|
||||
}
|
||||
this._dispatchers.get(n).add(dispatcher);
|
||||
});
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher subscribes to the broadcast.
|
||||
* @event VoiceBroadcast#subscribe
|
||||
* @param {StreamDispatcher} dispatcher The subscribed dispatcher
|
||||
*/
|
||||
this.emit('subscribe', dispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
killCurrentTranscoder() {
|
||||
if (this.currentTranscoder) {
|
||||
if (this.currentTranscoder.transcoder) this.currentTranscoder.transcoder.kill();
|
||||
this.currentTranscoder = null;
|
||||
this.emit('end');
|
||||
}
|
||||
get dispatcher() {
|
||||
return this.player.dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays any audio stream across the broadcast.
|
||||
* @param {ReadableStream} stream The audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {VoiceBroadcast}
|
||||
* Play an audio resource.
|
||||
* @param {ReadableStream|string} resource The resource to play.
|
||||
* @param {StreamOptions} [options] The options to play.
|
||||
* @example
|
||||
* // Play streams using ytdl-core
|
||||
* const ytdl = require('ytdl-core');
|
||||
* const streamOptions = { seek: 0, volume: 1 };
|
||||
* const broadcast = client.createVoiceBroadcast();
|
||||
*
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
|
||||
* broadcast.playStream(stream);
|
||||
* const dispatcher = connection.playBroadcast(broadcast);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
*/
|
||||
playStream(stream, options = {}) {
|
||||
this.setVolume(options.volume || 1);
|
||||
return this._playTranscodable(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the given file in the voice connection.
|
||||
* @param {string} file The absolute path to the file
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* // Play a local audio file
|
||||
* broadcast.play('/home/hydrabolt/audio.mp3', { volume: 0.5 });
|
||||
* @example
|
||||
* // Play files natively
|
||||
* const broadcast = client.createVoiceBroadcast();
|
||||
*
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* broadcast.playFile('C:/Users/Discord/Desktop/music.mp3');
|
||||
* const dispatcher = connection.playBroadcast(broadcast);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
* // Play a ReadableStream
|
||||
* broadcast.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' }));
|
||||
* @example
|
||||
* // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html
|
||||
* broadcast.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
|
||||
* @returns {BroadcastDispatcher}
|
||||
*/
|
||||
playFile(file, options = {}) {
|
||||
this.setVolume(options.volume || 1);
|
||||
return this._playTranscodable(`file:${file}`, options);
|
||||
}
|
||||
|
||||
_playTranscodable(media, options) {
|
||||
this.killCurrentTranscoder();
|
||||
const transcoder = this.prism.transcode({
|
||||
type: 'ffmpeg',
|
||||
media,
|
||||
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
|
||||
});
|
||||
/**
|
||||
* Emitted whenever an error occurs.
|
||||
* @event VoiceBroadcast#error
|
||||
* @param {Error} error The error that occurred
|
||||
*/
|
||||
transcoder.once('error', e => {
|
||||
if (this.listenerCount('error') > 0) this.emit('error', e);
|
||||
/**
|
||||
* Emitted whenever the VoiceBroadcast has any warnings.
|
||||
* @event VoiceBroadcast#warn
|
||||
* @param {string|Error} warning The warning that was raised
|
||||
*/
|
||||
else this.emit('warn', e);
|
||||
});
|
||||
/**
|
||||
* Emitted once the broadcast (the audio stream) ends.
|
||||
* @event VoiceBroadcast#end
|
||||
*/
|
||||
transcoder.once('end', () => this.killCurrentTranscoder());
|
||||
this.currentTranscoder = {
|
||||
transcoder,
|
||||
options,
|
||||
};
|
||||
transcoder.output.once('readable', () => this._startPlaying());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a stream of 16-bit signed stereo PCM.
|
||||
* @param {ReadableStream} stream The audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {VoiceBroadcast}
|
||||
*/
|
||||
playConvertedStream(stream, options = {}) {
|
||||
this.killCurrentTranscoder();
|
||||
this.setVolume(options.volume || 1);
|
||||
this.currentTranscoder = { options: { stream } };
|
||||
stream.once('readable', () => this._startPlaying());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays an Opus encoded stream.
|
||||
* <warn>Note that inline volume is not compatible with this method.</warn>
|
||||
* @param {ReadableStream} stream The Opus audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playOpusStream(stream) {
|
||||
this.currentTranscoder = { options: { stream }, opus: true };
|
||||
stream.once('readable', () => this._startPlaying());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description).
|
||||
* @param {string} input The arbitrary input
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {VoiceBroadcast}
|
||||
*/
|
||||
playArbitraryInput(input, options = {}) {
|
||||
this.setVolume(options.volume || 1);
|
||||
options.input = input;
|
||||
return this._playTranscodable(input, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the entire broadcast - all dispatchers are also paused.
|
||||
*/
|
||||
pause() {
|
||||
this.paused = true;
|
||||
for (const container of this._dispatchers.values()) {
|
||||
for (const dispatcher of container.values()) {
|
||||
dispatcher.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the entire broadcast - all dispatchers are also resumed.
|
||||
*/
|
||||
resume() {
|
||||
this.paused = false;
|
||||
for (const container of this._dispatchers.values()) {
|
||||
for (const dispatcher of container.values()) {
|
||||
dispatcher.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_startPlaying() {
|
||||
if (this.tickInterval) clearInterval(this.tickInterval);
|
||||
// Old code?
|
||||
// this.tickInterval = this.client.setInterval(this.tick.bind(this), 20);
|
||||
this._startTime = Date.now();
|
||||
this._count = 0;
|
||||
this._pausedTime = 0;
|
||||
this._missed = 0;
|
||||
this.tick();
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this._playableStream) return;
|
||||
if (this.paused) {
|
||||
this._pausedTime += 20;
|
||||
setTimeout(() => this.tick(), 20);
|
||||
return;
|
||||
}
|
||||
|
||||
const opus = this.currentTranscoder.opus;
|
||||
const buffer = this.readStreamBuffer();
|
||||
|
||||
if (!buffer) {
|
||||
this._missed++;
|
||||
if (this._missed < 5) {
|
||||
this._pausedTime += 200;
|
||||
setTimeout(() => this.tick(), 200);
|
||||
} else {
|
||||
this.killCurrentTranscoder();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._missed = 0;
|
||||
|
||||
let packetMatrix = {};
|
||||
|
||||
const getOpusPacket = volume => {
|
||||
if (packetMatrix[volume]) return packetMatrix[volume];
|
||||
|
||||
const opusEncoder = this._encoders.get(volume);
|
||||
const opusPacket = opusEncoder.encode(this.applyVolume(buffer, this._volume * volume));
|
||||
packetMatrix[volume] = opusPacket;
|
||||
return opusPacket;
|
||||
};
|
||||
|
||||
for (const dispatcher of this.dispatchers) {
|
||||
if (opus) {
|
||||
dispatcher.processPacket(buffer);
|
||||
continue;
|
||||
}
|
||||
|
||||
const volume = dispatcher.volume;
|
||||
dispatcher.processPacket(getOpusPacket(volume));
|
||||
}
|
||||
|
||||
const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now());
|
||||
this._count++;
|
||||
setTimeout(() => this.tick(), next);
|
||||
}
|
||||
|
||||
readStreamBuffer() {
|
||||
const opus = this.currentTranscoder.opus;
|
||||
const bufferLength = (opus ? 80 : 1920) * 2;
|
||||
let buffer = this._playableStream.read(bufferLength);
|
||||
if (opus) return buffer;
|
||||
if (!buffer) return null;
|
||||
|
||||
if (buffer.length !== bufferLength) {
|
||||
const newBuffer = Buffer.alloc(bufferLength).fill(0);
|
||||
buffer.copy(newBuffer);
|
||||
buffer = newBuffer;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current stream from playing without unsubscribing dispatchers.
|
||||
*/
|
||||
end() {
|
||||
this.killCurrentTranscoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the current broadcast, all subscribed dispatchers will also end.
|
||||
*/
|
||||
destroy() {
|
||||
this.end();
|
||||
for (const container of this._dispatchers.values()) {
|
||||
for (const dispatcher of container.values()) {
|
||||
dispatcher.destroy('end', 'broadcast ended');
|
||||
}
|
||||
}
|
||||
}
|
||||
play() { return null; }
|
||||
}
|
||||
|
||||
PlayInterface.applyToClass(VoiceBroadcast);
|
||||
|
||||
module.exports = VoiceBroadcast;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const VoiceWebSocket = require('./VoiceWebSocket');
|
||||
const VoiceUDP = require('./VoiceUDPClient');
|
||||
const VoiceWebSocket = require('./networking/VoiceWebSocket');
|
||||
const VoiceUDP = require('./networking/VoiceUDPClient');
|
||||
const Util = require('../../util/Util');
|
||||
const { OPCodes, VoiceOPCodes, VoiceStatus } = require('../../util/Constants');
|
||||
const AudioPlayer = require('./player/AudioPlayer');
|
||||
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
||||
const VoiceReceiver = require('./receiver/Receiver');
|
||||
const EventEmitter = require('events');
|
||||
const Prism = require('prism-media');
|
||||
const { Error } = require('../../errors');
|
||||
const PlayInterface = require('./util/PlayInterface');
|
||||
|
||||
/**
|
||||
* Represents a connection to a guild's voice server.
|
||||
@@ -18,6 +18,7 @@ const { Error } = require('../../errors');
|
||||
* });
|
||||
* ```
|
||||
* @extends {EventEmitter}
|
||||
* @implements {PlayInterface}
|
||||
*/
|
||||
class VoiceConnection extends EventEmitter {
|
||||
constructor(voiceManager, channel) {
|
||||
@@ -35,17 +36,6 @@ class VoiceConnection extends EventEmitter {
|
||||
*/
|
||||
this.client = voiceManager.client;
|
||||
|
||||
/**
|
||||
* @external Prism
|
||||
* @see {@link https://github.com/hydrabolt/prism-media}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The audio transcoder for this connection
|
||||
* @type {Prism}
|
||||
*/
|
||||
this.prism = new Prism();
|
||||
|
||||
/**
|
||||
* The voice channel this connection is currently serving
|
||||
* @type {VoiceChannel}
|
||||
@@ -101,6 +91,8 @@ class VoiceConnection extends EventEmitter {
|
||||
this.emit('warn', e);
|
||||
});
|
||||
|
||||
this.once('closing', () => this.player.destroy());
|
||||
|
||||
/**
|
||||
* Map SSRC to speaking values
|
||||
* @type {Map<number, boolean>}
|
||||
@@ -425,11 +417,6 @@ class VoiceConnection extends EventEmitter {
|
||||
const guild = this.channel.guild;
|
||||
const user = this.client.users.get(user_id);
|
||||
this.ssrcMap.set(+ssrc, user);
|
||||
if (!speaking) {
|
||||
for (const receiver of this.receivers) {
|
||||
receiver.stoppedSpeaking(user);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Emitted whenever a user starts/stops speaking.
|
||||
* @event VoiceConnection#speaking
|
||||
@@ -440,100 +427,6 @@ class VoiceConnection extends EventEmitter {
|
||||
guild._memberSpeakUpdate(user_id, speaking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be passed to stream-playing methods:
|
||||
* @typedef {Object} StreamOptions
|
||||
* @property {number} [seek=0] The time to seek to
|
||||
* @property {number} [volume=1] The volume to play at
|
||||
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
|
||||
* @property {number|string} [bitrate=48000] The bitrate (quality) of the audio.
|
||||
* If set to 'auto', the voice channel's bitrate will be used
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plays the given file in the voice connection.
|
||||
* @param {string} file The absolute path to the file
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* @example
|
||||
* // Play files natively
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
|
||||
* })
|
||||
* .catch(console.error);
|
||||
*/
|
||||
playFile(file, options) {
|
||||
return this.player.playUnknownStream(`file:${file}`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
|
||||
* @param {string} input the arbitrary input
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playArbitraryInput(input, options) {
|
||||
return this.player.playUnknownStream(input, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays and converts an audio stream in the voice connection.
|
||||
* @param {ReadableStream} stream The audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* @example
|
||||
* // Play streams using ytdl-core
|
||||
* const ytdl = require('ytdl-core');
|
||||
* const streamOptions = { seek: 0, volume: 1 };
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
|
||||
* const dispatcher = connection.playStream(stream, streamOptions);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
*/
|
||||
playStream(stream, options) {
|
||||
return this.player.playUnknownStream(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a stream of 16-bit signed stereo PCM.
|
||||
* @param {ReadableStream} stream The audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playConvertedStream(stream, options) {
|
||||
return this.player.playPCMStream(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays an Opus encoded stream.
|
||||
* <warn>Note that inline volume is not compatible with this method.</warn>
|
||||
* @param {ReadableStream} stream The Opus audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playOpusStream(stream, options) {
|
||||
return this.player.playOpusStream(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a voice broadcast.
|
||||
* @param {VoiceBroadcast} broadcast The broadcast to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* @example
|
||||
* // Play a broadcast
|
||||
* const broadcast = client
|
||||
* .createVoiceBroadcast()
|
||||
* .playFile('./test.mp3');
|
||||
* const dispatcher = voiceConnection.playBroadcast(broadcast);
|
||||
*/
|
||||
playBroadcast(broadcast, options) {
|
||||
return this.player.playBroadcast(broadcast, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a VoiceReceiver so you can start listening to voice data.
|
||||
* It's recommended to only create one of these.
|
||||
@@ -544,6 +437,10 @@ class VoiceConnection extends EventEmitter {
|
||||
this.receivers.push(receiver);
|
||||
return receiver;
|
||||
}
|
||||
|
||||
play() {} // eslint-disable-line no-empty-function
|
||||
}
|
||||
|
||||
PlayInterface.applyToClass(VoiceConnection);
|
||||
|
||||
module.exports = VoiceConnection;
|
||||
|
||||
38
src/client/voice/dispatcher/BroadcastDispatcher.js
Normal file
38
src/client/voice/dispatcher/BroadcastDispatcher.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const StreamDispatcher = require('./StreamDispatcher');
|
||||
|
||||
/**
|
||||
* The class that sends voice packet data to the voice connection.
|
||||
* @implements {VolumeInterface}
|
||||
* @extends {StreamDispatcher}
|
||||
*/
|
||||
class BroadcastDispatcher extends StreamDispatcher {
|
||||
constructor(player, options, streams) {
|
||||
super(player, options, streams);
|
||||
this.broadcast = player.broadcast;
|
||||
}
|
||||
|
||||
_write(chunk, enc, done) {
|
||||
if (!this.startTime) this.startTime = Date.now();
|
||||
for (const dispatcher of this.broadcast.dispatchers) {
|
||||
dispatcher._write(chunk, enc);
|
||||
}
|
||||
this._step(done);
|
||||
}
|
||||
|
||||
_destroy(err, cb) {
|
||||
if (this.player.dispatcher === this) this.player.dispatcher = null;
|
||||
const { streams } = this;
|
||||
if (streams.opus) streams.opus.unpipe(this);
|
||||
if (streams.ffmpeg) streams.ffmpeg.destroy();
|
||||
super._destroy(err, cb);
|
||||
}
|
||||
|
||||
setBitrate(value) {
|
||||
if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false;
|
||||
const bitrate = value === 'auto' ? 48 : value;
|
||||
this.streams.opus.setBitrate(bitrate * 1000);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BroadcastDispatcher;
|
||||
@@ -1,158 +1,221 @@
|
||||
const VolumeInterface = require('../util/VolumeInterface');
|
||||
const VoiceBroadcast = require('../VoiceBroadcast');
|
||||
const { VoiceStatus } = require('../../../util/Constants');
|
||||
const VolumeInterface = require('../util/VolumeInterface');
|
||||
const { Writable } = require('stream');
|
||||
|
||||
const secretbox = require('../util/Secretbox');
|
||||
|
||||
const FRAME_LENGTH = 20;
|
||||
const CHANNELS = 2;
|
||||
const TIMESTAMP_INC = (48000 / 100) * CHANNELS;
|
||||
|
||||
const nonce = Buffer.alloc(24);
|
||||
nonce.fill(0);
|
||||
|
||||
/**
|
||||
* @external WritableStream
|
||||
* @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The class that sends voice packet data to the voice connection.
|
||||
* ```js
|
||||
* // Obtained using:
|
||||
* voiceChannel.join().then(connection => {
|
||||
* // You can play a file or a stream here:
|
||||
* const dispatcher = connection.playFile('./file.mp3');
|
||||
* const dispatcher = connection.play('/home/hydrabolt/audio.mp3');
|
||||
* });
|
||||
* ```
|
||||
* @implements {VolumeInterface}
|
||||
* @extends {WritableStream}
|
||||
*/
|
||||
class StreamDispatcher extends VolumeInterface {
|
||||
constructor(player, stream, streamOptions) {
|
||||
class StreamDispatcher extends Writable {
|
||||
constructor(
|
||||
player,
|
||||
{ seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {},
|
||||
streams) {
|
||||
const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark };
|
||||
super(streamOptions);
|
||||
/**
|
||||
* The Audio Player that controls this dispatcher
|
||||
* @type {AudioPlayer}
|
||||
*/
|
||||
this.player = player;
|
||||
/**
|
||||
* The stream that the dispatcher plays
|
||||
* @type {ReadableStream|VoiceBroadcast}
|
||||
*/
|
||||
this.stream = stream;
|
||||
if (!(this.stream instanceof VoiceBroadcast)) this.startStreaming();
|
||||
this.streamOptions = streamOptions;
|
||||
|
||||
const data = this.streamingData;
|
||||
data.length = 20;
|
||||
data.missed = 0;
|
||||
this.streams = streams;
|
||||
|
||||
/**
|
||||
* Whether playing is paused
|
||||
* @type {boolean}
|
||||
* The time that the stream was paused at (null if not paused)
|
||||
* @type {?number}
|
||||
*/
|
||||
this.paused = false;
|
||||
this.pausedSince = null;
|
||||
this._writeCallback = null;
|
||||
|
||||
/**
|
||||
* Whether this dispatcher has been destroyed
|
||||
* @type {boolean}
|
||||
* The broadcast controlling this dispatcher, if any
|
||||
* @type {?VoiceBroadcast}
|
||||
*/
|
||||
this.destroyed = false;
|
||||
this.broadcast = this.streams.broadcast;
|
||||
|
||||
this._opus = streamOptions.opus;
|
||||
this._pausedTime = 0;
|
||||
this.count = 0;
|
||||
|
||||
this.on('finish', () => {
|
||||
// Still emitting end for backwards compatibility, probably remove it in the future!
|
||||
this.emit('end');
|
||||
});
|
||||
|
||||
if (typeof volume !== 'undefined') this.setVolume(volume);
|
||||
if (typeof fec !== 'undefined') this.setFEC(fec);
|
||||
if (typeof plp !== 'undefined') this.setPLP(plp);
|
||||
if (typeof bitrate !== 'undefined') this.setBitrate(bitrate);
|
||||
|
||||
const streamError = (type, err) => {
|
||||
/**
|
||||
* Emitted when the dispatcher encounters an error.
|
||||
* @event StreamDispatcher#error
|
||||
*/
|
||||
if (type && err) {
|
||||
err.message = `${type} stream: ${err.message}`;
|
||||
this.emit(this.player.dispatcher === this ? 'error' : 'debug', err);
|
||||
}
|
||||
this.destroy();
|
||||
};
|
||||
|
||||
this.on('error', () => streamError());
|
||||
if (this.streams.input) this.streams.input.on('error', err => streamError('input', err));
|
||||
if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', err => streamError('ffmpeg', err));
|
||||
if (this.streams.opus) this.streams.opus.on('error', err => streamError('opus', err));
|
||||
if (this.streams.volume) this.streams.volume.on('error', err => streamError('volume', err));
|
||||
}
|
||||
|
||||
/**
|
||||
* How many passes the dispatcher should take when sending packets to reduce packet loss. Values over 5
|
||||
* aren't recommended, as it means you are using 5x more bandwidth. You _can_ edit this at runtime
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get passes() {
|
||||
return this.streamOptions.passes || 1;
|
||||
}
|
||||
|
||||
set passes(n) {
|
||||
this.streamOptions.passes = n;
|
||||
}
|
||||
|
||||
get streamingData() {
|
||||
get _sdata() {
|
||||
return this.player.streamingData;
|
||||
}
|
||||
|
||||
/**
|
||||
* How long the stream dispatcher has been "speaking" for
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get time() {
|
||||
return this.streamingData.count * (this.streamingData.length || 0);
|
||||
_write(chunk, enc, done) {
|
||||
if (!this.startTime) {
|
||||
/**
|
||||
* Emitted once the stream has started to play.
|
||||
* @event StreamDispatcher#start
|
||||
*/
|
||||
this.emit('start');
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
this._playChunk(chunk);
|
||||
this._step(done);
|
||||
}
|
||||
|
||||
_destroy(err, cb) {
|
||||
if (this.player.dispatcher === this) this.player.dispatcher = null;
|
||||
const { streams } = this;
|
||||
if (streams.broadcast) streams.broadcast.dispatchers.delete(this);
|
||||
if (streams.opus) streams.opus.unpipe(this);
|
||||
if (streams.ffmpeg) streams.ffmpeg.destroy();
|
||||
super._destroy(err, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* The total time, taking into account pauses and skips, that the dispatcher has been streaming for
|
||||
* Pauses playback
|
||||
*/
|
||||
pause() {
|
||||
this.pausedSince = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not playback is paused
|
||||
* @type {boolean}
|
||||
*/
|
||||
get paused() { return Boolean(this.pausedSince); }
|
||||
|
||||
/**
|
||||
* Total time that this dispatcher has been paused
|
||||
* @type {number}
|
||||
*/
|
||||
get pausedTime() { return this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0); }
|
||||
|
||||
/**
|
||||
* Resumes playback
|
||||
*/
|
||||
resume() {
|
||||
this._pausedTime += Date.now() - this.pausedSince;
|
||||
this.pausedSince = null;
|
||||
if (this._writeCallback) this._writeCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) that the dispatcher has actually been playing audio for
|
||||
* @type {number}
|
||||
*/
|
||||
get streamTime() {
|
||||
return this.count * FRAME_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) that the dispatcher has been playing audio for, taking into account skips and pauses
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get totalStreamTime() {
|
||||
return this.time + this.streamingData.pausedTime;
|
||||
return Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops sending voice packets to the voice connection (stream may still progress however).
|
||||
*/
|
||||
pause() { this.setPaused(true); }
|
||||
|
||||
/**
|
||||
* Resumes sending voice packets to the voice connection (may be further on in the stream than when paused).
|
||||
*/
|
||||
resume() { this.setPaused(false); }
|
||||
|
||||
|
||||
/**
|
||||
* Stops the current stream permanently and emits an `end` event.
|
||||
* @param {string} [reason='user'] An optional reason for stopping the dispatcher
|
||||
*/
|
||||
end(reason = 'user') {
|
||||
this.destroy('end', reason);
|
||||
}
|
||||
|
||||
setSpeaking(value) {
|
||||
if (this.speaking === value) return;
|
||||
if (this.player.voiceConnection.status !== VoiceStatus.CONNECTED) return;
|
||||
this.speaking = value;
|
||||
/**
|
||||
* Emitted when the dispatcher starts/stops speaking.
|
||||
* @event StreamDispatcher#speaking
|
||||
* @param {boolean} value Whether or not the dispatcher is speaking
|
||||
*/
|
||||
this.emit('speaking', value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the bitrate of the current Opus encoder.
|
||||
* @param {number} bitrate New bitrate, in kbps.
|
||||
* Set the bitrate of the current Opus encoder if using a compatible Opus stream.
|
||||
* @param {number} value New bitrate, in kbps
|
||||
* If set to 'auto', the voice channel's bitrate will be used
|
||||
* @returns {boolean} true if the bitrate has been successfully changed.
|
||||
*/
|
||||
setBitrate(bitrate) {
|
||||
this.player.setBitrate(bitrate);
|
||||
setBitrate(value) {
|
||||
if (!value || !this.bitrateEditable) return false;
|
||||
const bitrate = value === 'auto' ? this.player.voiceConnection.channel.bitrate : value;
|
||||
this.streams.opus.setBitrate(bitrate * 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
sendBuffer(buffer, sequence, timestamp, opusPacket) {
|
||||
opusPacket = opusPacket || this.player.opusEncoder.encode(buffer);
|
||||
const packet = this.createPacket(sequence, timestamp, opusPacket);
|
||||
this.sendPacket(packet);
|
||||
/**
|
||||
* Sets the expected packet loss percentage if using a compatible Opus stream.
|
||||
* @param {number} value between 0 and 1
|
||||
* @returns {boolean} Returns true if it was successfully set.
|
||||
*/
|
||||
setPLP(value) {
|
||||
if (!this.bitrateEditable) return false;
|
||||
this.streams.opus.setPLP(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
sendPacket(packet) {
|
||||
let repeats = this.passes;
|
||||
/**
|
||||
* Emitted whenever the dispatcher has debug information.
|
||||
* @event StreamDispatcher#debug
|
||||
* @param {string} info The debug info
|
||||
*/
|
||||
this.setSpeaking(true);
|
||||
while (repeats--) {
|
||||
this.player.voiceConnection.sockets.udp.send(packet)
|
||||
.catch(e => {
|
||||
this.setSpeaking(false);
|
||||
this.emit('debug', `Failed to send a packet ${e}`);
|
||||
});
|
||||
/**
|
||||
* Enables or disables forward error correction if using a compatible Opus stream.
|
||||
* @param {boolean} enabled true to enable
|
||||
* @returns {boolean} Returns true if it was successfully set.
|
||||
*/
|
||||
setFEC(enabled) {
|
||||
if (!this.bitrateEditable) return false;
|
||||
this.streams.opus.setFEC(enabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
_step(done) {
|
||||
if (this.pausedSince) {
|
||||
this._writeCallback = done;
|
||||
return;
|
||||
}
|
||||
if (!this.streams.broadcast) {
|
||||
const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime);
|
||||
setTimeout(done.bind(this), next);
|
||||
}
|
||||
this._sdata.sequence++;
|
||||
this._sdata.timestamp += TIMESTAMP_INC;
|
||||
if (this._sdata.sequence >= 2 ** 16) this._sdata.sequence = 0;
|
||||
if (this._sdata.timestamp >= 2 ** 32) this._sdata.timestamp = 0;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
createPacket(sequence, timestamp, buffer) {
|
||||
_playChunk(chunk) {
|
||||
if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secretKey) return;
|
||||
this._setSpeaking(true);
|
||||
this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk));
|
||||
}
|
||||
|
||||
_createPacket(sequence, timestamp, buffer) {
|
||||
const packetBuffer = Buffer.alloc(buffer.length + 28);
|
||||
packetBuffer.fill(0);
|
||||
packetBuffer[0] = 0x80;
|
||||
@@ -163,169 +226,69 @@ class StreamDispatcher extends VolumeInterface {
|
||||
packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4);
|
||||
|
||||
packetBuffer.copy(nonce, 0, 0, 12);
|
||||
buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key);
|
||||
buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey);
|
||||
for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
|
||||
|
||||
return packetBuffer;
|
||||
}
|
||||
|
||||
processPacket(packet) {
|
||||
try {
|
||||
if (this.destroyed) {
|
||||
this.setSpeaking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.streamingData;
|
||||
|
||||
if (this.paused) {
|
||||
this.setSpeaking(false);
|
||||
data.pausedTime = data.length * 10;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!packet) {
|
||||
data.missed++;
|
||||
data.pausedTime += data.length * 10;
|
||||
return;
|
||||
}
|
||||
|
||||
this.started();
|
||||
this.missed = 0;
|
||||
|
||||
this.stepStreamingData();
|
||||
this.sendBuffer(null, data.sequence, data.timestamp, packet);
|
||||
} catch (e) {
|
||||
this.destroy('error', e);
|
||||
}
|
||||
}
|
||||
|
||||
process() {
|
||||
try {
|
||||
if (this.destroyed) {
|
||||
this.setSpeaking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.streamingData;
|
||||
|
||||
if (data.missed >= 5) {
|
||||
this.destroy('end', 'Stream is not generating quickly enough.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.paused) {
|
||||
this.setSpeaking(false);
|
||||
// Old code?
|
||||
// data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
|
||||
data.pausedTime += data.length * 10;
|
||||
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
|
||||
return;
|
||||
}
|
||||
|
||||
this.started();
|
||||
|
||||
const buffer = this.readStreamBuffer();
|
||||
if (!buffer) {
|
||||
data.missed++;
|
||||
data.pausedTime += data.length * 10;
|
||||
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
|
||||
return;
|
||||
}
|
||||
|
||||
data.missed = 0;
|
||||
|
||||
this.stepStreamingData();
|
||||
|
||||
if (this._opus) {
|
||||
this.sendBuffer(null, data.sequence, data.timestamp, buffer);
|
||||
} else {
|
||||
this.sendBuffer(buffer, data.sequence, data.timestamp);
|
||||
}
|
||||
|
||||
const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now());
|
||||
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), nextTime);
|
||||
} catch (e) {
|
||||
this.destroy('error', e);
|
||||
}
|
||||
}
|
||||
|
||||
readStreamBuffer() {
|
||||
const data = this.streamingData;
|
||||
const bufferLength = (this._opus ? 80 : 1920) * data.channels;
|
||||
let buffer = this.stream.read(bufferLength);
|
||||
if (this._opus) return buffer;
|
||||
if (!buffer) return null;
|
||||
|
||||
if (buffer.length !== bufferLength) {
|
||||
const newBuffer = Buffer.alloc(bufferLength).fill(0);
|
||||
buffer.copy(newBuffer);
|
||||
buffer = newBuffer;
|
||||
}
|
||||
|
||||
buffer = this.applyVolume(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
started() {
|
||||
const data = this.streamingData;
|
||||
|
||||
if (!data.startTime) {
|
||||
/**
|
||||
* Emitted once the dispatcher starts streaming.
|
||||
* @event StreamDispatcher#start
|
||||
*/
|
||||
this.emit('start');
|
||||
data.startTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
stepStreamingData() {
|
||||
const data = this.streamingData;
|
||||
data.count++;
|
||||
data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0;
|
||||
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
|
||||
}
|
||||
|
||||
destroy(type, reason) {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
this.setSpeaking(false);
|
||||
this.emit(type, reason);
|
||||
_sendPacket(packet) {
|
||||
let repeats = this.streamOptions.passes;
|
||||
/**
|
||||
* Emitted once the dispatcher ends.
|
||||
* @param {string} [reason] The reason the dispatcher ended
|
||||
* @event StreamDispatcher#end
|
||||
* Emitted whenever the dispatcher has debug information.
|
||||
* @event StreamDispatcher#debug
|
||||
* @param {string} info The debug info
|
||||
*/
|
||||
if (type !== 'end') this.emit('end', `destroyed due to ${type} - ${reason}`);
|
||||
}
|
||||
|
||||
startStreaming() {
|
||||
if (!this.stream) {
|
||||
/**
|
||||
* Emitted if the dispatcher encounters an error.
|
||||
* @event StreamDispatcher#error
|
||||
* @param {string} error The error message
|
||||
*/
|
||||
this.emit('error', 'No stream');
|
||||
return;
|
||||
this._setSpeaking(true);
|
||||
while (repeats--) {
|
||||
this.player.voiceConnection.sockets.udp.send(packet)
|
||||
.catch(e => {
|
||||
this._setSpeaking(false);
|
||||
this.emit('debug', `Failed to send a packet ${e}`);
|
||||
});
|
||||
}
|
||||
|
||||
this.stream.on('end', err => this.destroy('end', err || 'stream'));
|
||||
this.stream.on('error', err => this.destroy('error', err));
|
||||
|
||||
const data = this.streamingData;
|
||||
data.length = 20;
|
||||
data.missed = 0;
|
||||
|
||||
this.stream.once('readable', () => {
|
||||
data.startTime = null;
|
||||
data.count = 0;
|
||||
this.process();
|
||||
});
|
||||
}
|
||||
|
||||
setPaused(paused) { this.setSpeaking(!(this.paused = paused)); }
|
||||
_setSpeaking(value) {
|
||||
if (this.speaking === value) return;
|
||||
if (this.player.voiceConnection.status !== VoiceStatus.CONNECTED) return;
|
||||
this.speaking = value;
|
||||
this.player.voiceConnection.setSpeaking(value);
|
||||
/**
|
||||
* Emitted when the dispatcher starts/stops speaking.
|
||||
* @event StreamDispatcher#speaking
|
||||
* @param {boolean} value Whether or not the dispatcher is speaking
|
||||
*/
|
||||
this.emit('speaking', value);
|
||||
}
|
||||
|
||||
get volumeEditable() { return Boolean(this.streams.volume); }
|
||||
|
||||
/**
|
||||
* Whether or not the Opus bitrate of this stream is editable
|
||||
* @type {boolean}
|
||||
*/
|
||||
get bitrateEditable() { return this.streams.opus && this.streams.opus.setBitrate; }
|
||||
|
||||
// Volume
|
||||
get volume() {
|
||||
return this.streams.volume ? this.streams.volume.volume : 1;
|
||||
}
|
||||
|
||||
setVolume(value) {
|
||||
if (!this.streams.volume) return false;
|
||||
this.streams.volume.setVolume(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Volume stubs for docs
|
||||
/* eslint-disable no-empty-function*/
|
||||
get volumeDecibels() {}
|
||||
get volumeLogarithmic() {}
|
||||
setVolumeDecibels() {}
|
||||
setVolumeLogarithmic() {}
|
||||
}
|
||||
|
||||
VolumeInterface.applyToClass(StreamDispatcher);
|
||||
|
||||
module.exports = StreamDispatcher;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const udp = require('dgram');
|
||||
const dns = require('dns');
|
||||
const { VoiceOPCodes } = require('../../util/Constants');
|
||||
const { VoiceOPCodes } = require('../../../util/Constants');
|
||||
const EventEmitter = require('events');
|
||||
const { Error } = require('../../errors');
|
||||
const { Error } = require('../../../errors');
|
||||
|
||||
/**
|
||||
* Represents a UDP client for a Voice Connection.
|
||||
@@ -1,8 +1,7 @@
|
||||
const { OPCodes, VoiceOPCodes } = require('../../util/Constants');
|
||||
const SecretKey = require('./util/SecretKey');
|
||||
const { OPCodes, VoiceOPCodes } = require('../../../util/Constants');
|
||||
const EventEmitter = require('events');
|
||||
const { Error } = require('../../errors');
|
||||
const WebSocket = require('../../WebSocket');
|
||||
const { Error } = require('../../../errors');
|
||||
const WebSocket = require('../../../WebSocket');
|
||||
|
||||
/**
|
||||
* Represents a Voice Connection's WebSocket.
|
||||
@@ -156,7 +155,8 @@ class VoiceWebSocket extends EventEmitter {
|
||||
onPacket(packet) {
|
||||
switch (packet.op) {
|
||||
case VoiceOPCodes.READY:
|
||||
this.setHeartbeat(packet.d.heartbeat_interval);
|
||||
// *.75 to correct for discord devs taking longer to fix things than i do to release versions
|
||||
this.setHeartbeat(packet.d.heartbeat_interval * 0.75);
|
||||
/**
|
||||
* Emitted once the voice WebSocket receives the ready packet.
|
||||
* @param {Object} packet The received packet
|
||||
@@ -164,14 +164,17 @@ class VoiceWebSocket extends EventEmitter {
|
||||
*/
|
||||
this.emit('ready', packet.d);
|
||||
break;
|
||||
/* eslint-disable no-case-declarations */
|
||||
case VoiceOPCodes.SESSION_DESCRIPTION:
|
||||
const key = new Uint8Array(new ArrayBuffer(packet.d.secret_key.length));
|
||||
for (const i in packet.d.secret_key) key[i] = packet.d.secret_key[i];
|
||||
/**
|
||||
* Emitted once the Voice Websocket receives a description of this voice session.
|
||||
* @param {string} encryptionMode The type of encryption being used
|
||||
* @param {SecretKey} secretKey The secret key used for encryption
|
||||
* @param {Uint8Array} secretKey The secret key used for encryption
|
||||
* @event VoiceWebSocket#sessionDescription
|
||||
*/
|
||||
this.emit('sessionDescription', packet.d.mode, new SecretKey(packet.d.secret_key));
|
||||
this.emit('sessionDescription', packet.d.mode, key);
|
||||
break;
|
||||
case VoiceOPCodes.SPEAKING:
|
||||
/**
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* The base opus encoding engine.
|
||||
* @private
|
||||
*/
|
||||
class BaseOpus {
|
||||
/**
|
||||
* @param {Object} [options] The options to apply to the Opus engine
|
||||
* @param {number} [options.bitrate=48] The desired bitrate (kbps)
|
||||
* @param {boolean} [options.fec=false] Whether to enable forward error correction
|
||||
* @param {number} [options.plp=0] The expected packet loss percentage
|
||||
*/
|
||||
constructor({ bitrate = 48, fec = false, plp = 0 } = {}) {
|
||||
this.ctl = {
|
||||
BITRATE: 4002,
|
||||
FEC: 4012,
|
||||
PLP: 4014,
|
||||
};
|
||||
|
||||
this.samplingRate = 48000;
|
||||
this.channels = 2;
|
||||
|
||||
/**
|
||||
* The desired bitrate (kbps)
|
||||
* @type {number}
|
||||
*/
|
||||
this.bitrate = bitrate;
|
||||
|
||||
/**
|
||||
* Miscellaneous Opus options
|
||||
* @type {Object}
|
||||
*/
|
||||
this.options = { fec, plp };
|
||||
}
|
||||
|
||||
init() {
|
||||
try {
|
||||
this.setBitrate(this.bitrate);
|
||||
|
||||
// Set FEC (forward error correction)
|
||||
if (this.options.fec) this.setFEC(this.options.fec);
|
||||
|
||||
// Set PLP (expected packet loss percentage)
|
||||
if (this.options.plp) this.setPLP(this.options.plp);
|
||||
} catch (err) {
|
||||
// Opus engine likely has no support for libopus CTL
|
||||
}
|
||||
}
|
||||
|
||||
encode(buffer) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
decode(buffer) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
destroy() {} // eslint-disable-line no-empty-function
|
||||
}
|
||||
|
||||
module.exports = BaseOpus;
|
||||
@@ -1,40 +0,0 @@
|
||||
const OpusEngine = require('./BaseOpusEngine');
|
||||
|
||||
let opus;
|
||||
|
||||
class NodeOpusEngine extends OpusEngine {
|
||||
constructor(player) {
|
||||
super(player);
|
||||
try {
|
||||
opus = require('node-opus');
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
|
||||
super.init();
|
||||
}
|
||||
|
||||
setBitrate(bitrate) {
|
||||
this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
|
||||
}
|
||||
|
||||
setFEC(enabled) {
|
||||
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
|
||||
}
|
||||
|
||||
setPLP(percent) {
|
||||
this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100)));
|
||||
}
|
||||
|
||||
encode(buffer) {
|
||||
super.encode(buffer);
|
||||
return this.encoder.encode(buffer, 1920);
|
||||
}
|
||||
|
||||
decode(buffer) {
|
||||
super.decode(buffer);
|
||||
return this.encoder.decode(buffer, 1920);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NodeOpusEngine;
|
||||
@@ -1,30 +0,0 @@
|
||||
const { Error } = require('../../../errors');
|
||||
|
||||
const list = [
|
||||
require('./NodeOpusEngine'),
|
||||
require('./OpusScriptEngine'),
|
||||
];
|
||||
|
||||
function fetch(Encoder, engineOptions) {
|
||||
try {
|
||||
return new Encoder(engineOptions);
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') return null;
|
||||
|
||||
// The Opus engine exists, but another error occurred.
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
exports.add = encoder => {
|
||||
list.push(encoder);
|
||||
};
|
||||
|
||||
exports.fetch = engineOptions => {
|
||||
for (const encoder of list) {
|
||||
const fetched = fetch(encoder, engineOptions);
|
||||
if (fetched) return fetched;
|
||||
}
|
||||
|
||||
throw new Error('OPUS_ENGINE_MISSING');
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
const OpusEngine = require('./BaseOpusEngine');
|
||||
|
||||
let OpusScript;
|
||||
|
||||
class OpusScriptEngine extends OpusEngine {
|
||||
constructor(player) {
|
||||
super(player);
|
||||
try {
|
||||
OpusScript = require('opusscript');
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
this.encoder = new OpusScript(this.samplingRate, this.channels);
|
||||
super.init();
|
||||
}
|
||||
|
||||
setBitrate(bitrate) {
|
||||
this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
|
||||
}
|
||||
|
||||
setFEC(enabled) {
|
||||
this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0);
|
||||
}
|
||||
|
||||
setPLP(percent) {
|
||||
this.encoder.encoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100)));
|
||||
}
|
||||
|
||||
encode(buffer) {
|
||||
super.encode(buffer);
|
||||
return this.encoder.encode(buffer, 960);
|
||||
}
|
||||
|
||||
decode(buffer) {
|
||||
super.decode(buffer);
|
||||
return this.encoder.decode(buffer);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.encoder.delete();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OpusScriptEngine;
|
||||
@@ -1,23 +1,11 @@
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const Prism = require('prism-media');
|
||||
const StreamDispatcher = require('../dispatcher/StreamDispatcher');
|
||||
const Collection = require('../../../util/Collection');
|
||||
const OpusEncoders = require('../opus/OpusEngineList');
|
||||
|
||||
const ffmpegArguments = [
|
||||
'-analyzeduration', '0',
|
||||
'-loglevel', '0',
|
||||
'-f', 's16le',
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
];
|
||||
const BasePlayer = require('./BasePlayer');
|
||||
|
||||
/**
|
||||
* An Audio Player for a Voice Connection.
|
||||
* @private
|
||||
* @extends {EventEmitter}
|
||||
* @extends {BasePlayer}
|
||||
*/
|
||||
class AudioPlayer extends EventEmitter {
|
||||
class AudioPlayer extends BasePlayer {
|
||||
constructor(voiceConnection) {
|
||||
super();
|
||||
/**
|
||||
@@ -25,145 +13,11 @@ class AudioPlayer extends EventEmitter {
|
||||
* @type {VoiceConnection}
|
||||
*/
|
||||
this.voiceConnection = voiceConnection;
|
||||
/**
|
||||
* The prism transcoder that the player uses
|
||||
* @type {Prism}
|
||||
*/
|
||||
this.prism = new Prism();
|
||||
this.streams = new Collection();
|
||||
this.currentStream = {};
|
||||
this.streamingData = {
|
||||
channels: 2,
|
||||
count: 0,
|
||||
sequence: 0,
|
||||
timestamp: 0,
|
||||
pausedTime: 0,
|
||||
};
|
||||
this.voiceConnection.once('closing', () => this.destroyCurrentStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* The current transcoder
|
||||
* @type {?Object}
|
||||
* @readonly
|
||||
*/
|
||||
get transcoder() {
|
||||
return this.currentStream.transcoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current dispatcher
|
||||
* @type {?StreamDispatcher}
|
||||
* @readonly
|
||||
*/
|
||||
get dispatcher() {
|
||||
return this.currentStream.dispatcher;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.opusEncoder) this.opusEncoder.destroy();
|
||||
this.opusEncoder = null;
|
||||
}
|
||||
|
||||
destroyCurrentStream() {
|
||||
const transcoder = this.transcoder;
|
||||
const dispatcher = this.dispatcher;
|
||||
if (transcoder) transcoder.kill();
|
||||
if (dispatcher) {
|
||||
const end = dispatcher.listeners('end')[0];
|
||||
const error = dispatcher.listeners('error')[0];
|
||||
if (end) dispatcher.removeListener('end', end);
|
||||
if (error) dispatcher.removeListener('error', error);
|
||||
dispatcher.destroy('end');
|
||||
}
|
||||
this.currentStream = {};
|
||||
this.streamingData.pausedTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bitrate of the current Opus encoder.
|
||||
* @param {number} value New bitrate, in kbps
|
||||
* If set to 'auto', the voice channel's bitrate will be used
|
||||
*/
|
||||
setBitrate(value) {
|
||||
if (!value) return;
|
||||
if (!this.opusEncoder) return;
|
||||
const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value;
|
||||
this.opusEncoder.setBitrate(bitrate);
|
||||
}
|
||||
|
||||
playUnknownStream(stream, options = {}) {
|
||||
this.destroy();
|
||||
this.opusEncoder = OpusEncoders.fetch(options);
|
||||
const transcoder = this.prism.transcode({
|
||||
type: 'ffmpeg',
|
||||
media: stream,
|
||||
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
|
||||
});
|
||||
this.destroyCurrentStream();
|
||||
this.currentStream = {
|
||||
transcoder: transcoder,
|
||||
output: transcoder.output,
|
||||
input: stream,
|
||||
};
|
||||
transcoder.on('error', e => {
|
||||
this.destroyCurrentStream();
|
||||
if (this.listenerCount('error') > 0) this.emit('error', e);
|
||||
this.emit('warn', `prism transcoder error - ${e}`);
|
||||
});
|
||||
return this.playPCMStream(transcoder.output, options, true);
|
||||
}
|
||||
|
||||
playPCMStream(stream, options = {}, fromUnknown = false) {
|
||||
this.destroy();
|
||||
this.opusEncoder = OpusEncoders.fetch(options);
|
||||
this.setBitrate(options.bitrate);
|
||||
const dispatcher = this.createDispatcher(stream, options);
|
||||
if (fromUnknown) {
|
||||
this.currentStream.dispatcher = dispatcher;
|
||||
} else {
|
||||
this.destroyCurrentStream();
|
||||
this.currentStream = {
|
||||
dispatcher,
|
||||
input: stream,
|
||||
output: stream,
|
||||
};
|
||||
}
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
playOpusStream(stream, options = {}) {
|
||||
options.opus = true;
|
||||
this.destroyCurrentStream();
|
||||
const dispatcher = this.createDispatcher(stream, options);
|
||||
this.currentStream = {
|
||||
dispatcher,
|
||||
input: stream,
|
||||
output: stream,
|
||||
};
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
playBroadcast(broadcast, options) {
|
||||
this.destroyCurrentStream();
|
||||
const dispatcher = this.createDispatcher(broadcast, options);
|
||||
this.currentStream = {
|
||||
dispatcher,
|
||||
broadcast,
|
||||
input: broadcast,
|
||||
output: broadcast,
|
||||
};
|
||||
broadcast.registerDispatcher(dispatcher);
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
createDispatcher(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||
const options = { seek, volume, passes };
|
||||
|
||||
const dispatcher = new StreamDispatcher(this, stream, options);
|
||||
dispatcher.on('end', () => this.destroyCurrentStream());
|
||||
dispatcher.on('error', () => this.destroyCurrentStream());
|
||||
dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value));
|
||||
const dispatcher = this.createDispatcher(options, { broadcast });
|
||||
broadcast.dispatchers.add(dispatcher);
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
|
||||
84
src/client/voice/player/BasePlayer.js
Normal file
84
src/client/voice/player/BasePlayer.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const { Readable: ReadableStream } = require('stream');
|
||||
const prism = require('prism-media');
|
||||
const StreamDispatcher = require('../dispatcher/StreamDispatcher');
|
||||
|
||||
const FFMPEG_ARGUMENTS = [
|
||||
'-analyzeduration', '0',
|
||||
'-loglevel', '0',
|
||||
'-f', 's16le',
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
];
|
||||
|
||||
/**
|
||||
* An Audio Player for a Voice Connection.
|
||||
* @private
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class BasePlayer extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.dispatcher = null;
|
||||
|
||||
this.streamingData = {
|
||||
channels: 2,
|
||||
sequence: 0,
|
||||
timestamp: 0,
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyDispatcher();
|
||||
}
|
||||
|
||||
destroyDispatcher() {
|
||||
if (this.dispatcher) {
|
||||
this.dispatcher.destroy();
|
||||
this.dispatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
playUnknown(input, options) {
|
||||
this.destroyDispatcher();
|
||||
|
||||
const isStream = input instanceof ReadableStream;
|
||||
const args = isStream ? FFMPEG_ARGUMENTS : ['-i', input, ...FFMPEG_ARGUMENTS];
|
||||
const ffmpeg = new prism.FFmpeg({ args });
|
||||
const streams = { ffmpeg };
|
||||
if (isStream) {
|
||||
streams.input = input;
|
||||
input.pipe(ffmpeg);
|
||||
}
|
||||
return this.playPCMStream(ffmpeg, options, streams);
|
||||
}
|
||||
|
||||
playPCMStream(stream, options, streams = {}) {
|
||||
this.destroyDispatcher();
|
||||
const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 1920 });
|
||||
if (options && options.volume === false) {
|
||||
stream.pipe(opus);
|
||||
return this.playOpusStream(opus, options, streams);
|
||||
}
|
||||
const volume = streams.volume = new prism.VolumeTransformer16LE(null, { volume: options ? options.volume : 1 });
|
||||
stream.pipe(volume).pipe(opus);
|
||||
return this.playOpusStream(opus, options, streams);
|
||||
}
|
||||
|
||||
playOpusStream(stream, options, streams = {}) {
|
||||
this.destroyDispatcher();
|
||||
streams.opus = stream;
|
||||
const dispatcher = this.createDispatcher(options, streams);
|
||||
stream.pipe(dispatcher);
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
createDispatcher(options, streams, broadcast) {
|
||||
this.destroyDispatcher();
|
||||
const dispatcher = this.dispatcher = new StreamDispatcher(this, options, streams, broadcast);
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BasePlayer;
|
||||
26
src/client/voice/player/BroadcastAudioPlayer.js
Normal file
26
src/client/voice/player/BroadcastAudioPlayer.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher');
|
||||
const BasePlayer = require('./BasePlayer');
|
||||
|
||||
/**
|
||||
* An Audio Player for a Voice Connection.
|
||||
* @private
|
||||
* @extends {BasePlayer}
|
||||
*/
|
||||
class AudioPlayer extends BasePlayer {
|
||||
constructor(broadcast) {
|
||||
super();
|
||||
/**
|
||||
* The broadcast that the player serves
|
||||
* @type {VoiceBroadcast}
|
||||
*/
|
||||
this.broadcast = broadcast;
|
||||
}
|
||||
|
||||
createDispatcher(options, streams) {
|
||||
this.destroyDispatcher();
|
||||
const dispatcher = this.dispatcher = new BroadcastDispatcher(this, options, streams);
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AudioPlayer;
|
||||
63
src/client/voice/receiver/PacketHandler.js
Normal file
63
src/client/voice/receiver/PacketHandler.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const nonce = Buffer.alloc(24);
|
||||
const secretbox = require('../util/Secretbox');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Readable extends require('stream').Readable { _read() {} } // eslint-disable-line no-empty-function
|
||||
|
||||
class PacketHandler extends EventEmitter {
|
||||
constructor(receiver) {
|
||||
super();
|
||||
this.receiver = receiver;
|
||||
this.streams = new Map();
|
||||
}
|
||||
|
||||
makeStream(user) {
|
||||
if (this.streams.has(user)) return this.streams.get(user);
|
||||
const stream = new Readable();
|
||||
this.streams.set(user, stream);
|
||||
return stream;
|
||||
}
|
||||
|
||||
parseBuffer(buffer) {
|
||||
// Reuse nonce buffer
|
||||
buffer.copy(nonce, 0, 0, 12);
|
||||
|
||||
let packet = secretbox.methods.open(buffer.slice(12), nonce, this.receiver.connection.authentication.secretKey);
|
||||
if (!packet) return new Error('Failed to decrypt voice packet');
|
||||
packet = Buffer.from(packet);
|
||||
|
||||
// Strip RTP Header Extensions (one-byte only)
|
||||
if (packet[0] === 0xBE && packet[1] === 0xDE && packet.length > 4) {
|
||||
const headerExtensionLength = packet.readUInt16BE(2);
|
||||
let offset = 4;
|
||||
for (let i = 0; i < headerExtensionLength; i++) {
|
||||
const byte = packet[offset];
|
||||
offset++;
|
||||
if (byte === 0) continue;
|
||||
offset += 1 + (0b1111 & (byte >> 4));
|
||||
}
|
||||
while (packet[offset] === 0) offset++;
|
||||
packet = packet.slice(offset);
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
userFromSSRC(ssrc) { return this.receiver.connection.ssrcMap.get(ssrc); }
|
||||
|
||||
push(buffer) {
|
||||
const ssrc = buffer.readUInt32BE(8);
|
||||
const user = this.userFromSSRC(ssrc);
|
||||
if (!user) return;
|
||||
const stream = this.streams.get(user.id);
|
||||
if (!stream) return;
|
||||
const opusPacket = this.parseBuffer(buffer);
|
||||
if (opusPacket instanceof Error) {
|
||||
this.emit('error', opusPacket);
|
||||
return;
|
||||
}
|
||||
stream.push(opusPacket);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PacketHandler;
|
||||
55
src/client/voice/receiver/Receiver.js
Normal file
55
src/client/voice/receiver/Receiver.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const EventEmitter = require('events');
|
||||
const prism = require('prism-media');
|
||||
const PacketHandler = require('./PacketHandler');
|
||||
const { Error } = require('../../../errors');
|
||||
|
||||
/**
|
||||
* Receives audio packets from a voice connection.
|
||||
* @example
|
||||
* const receiver = connection.createReceiver();
|
||||
* // opusStream is a ReadableStream - that means you could play it back to a voice channel if you wanted to!
|
||||
* const opusStream = receiver.createStream(user);
|
||||
*/
|
||||
class VoiceReceiver extends EventEmitter {
|
||||
constructor(connection) {
|
||||
super();
|
||||
this.connection = connection;
|
||||
this.packets = new PacketHandler(this);
|
||||
/**
|
||||
* Emitted whenever there is a warning
|
||||
* @event VoiceReceiver#debug
|
||||
* @param {Error|string} error The error or message to debug
|
||||
*/
|
||||
this.packets.on('error', err => this.emit('debug', err));
|
||||
this.connection.sockets.udp.socket.on('message', buffer => this.packets.push(buffer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Options passed to `VoiceReceiver#createStream`.
|
||||
* @typedef {Object} ReceiveStreamOptions
|
||||
* @property {string} [mode='opus'] The mode for audio output. This defaults to opus, meaning discord.js won't decode
|
||||
* the packets for you. You can set this to 'pcm' so that the stream's output will be 16-bit little-endian stereo
|
||||
* audio
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new audio receiving stream. If a stream already exists for a user, then that stream will be returned
|
||||
* rather than generating a new one.
|
||||
* @param {UserResolvable} user The user to start listening to.
|
||||
* @param {ReceiveStreamOptions} options Options.
|
||||
* @returns {ReadableStream}
|
||||
*/
|
||||
createStream(user, { mode = 'opus' } = {}) {
|
||||
user = this.connection.client.users.resolve(user);
|
||||
if (!user) throw new Error('VOICE_USER_MISSING');
|
||||
const stream = this.packets.makeStream(user.id);
|
||||
if (mode === 'pcm') {
|
||||
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 1920 });
|
||||
stream.pipe(decoder);
|
||||
return decoder;
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VoiceReceiver;
|
||||
@@ -1,17 +0,0 @@
|
||||
const { Readable } = require('stream');
|
||||
|
||||
class VoiceReadable extends Readable {
|
||||
constructor() {
|
||||
super();
|
||||
this._packets = [];
|
||||
this.open = true;
|
||||
}
|
||||
|
||||
_read() {} // eslint-disable-line no-empty-function
|
||||
|
||||
_push(d) {
|
||||
if (this.open) this.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VoiceReadable;
|
||||
@@ -1,220 +0,0 @@
|
||||
const EventEmitter = require('events');
|
||||
const secretbox = require('../util/Secretbox');
|
||||
const Readable = require('./VoiceReadable');
|
||||
const OpusEncoders = require('../opus/OpusEngineList');
|
||||
const { Error } = require('../../../errors');
|
||||
|
||||
const nonce = Buffer.alloc(24);
|
||||
nonce.fill(0);
|
||||
|
||||
/**
|
||||
* Receives voice data from a voice connection.
|
||||
* ```js
|
||||
* // Obtained using:
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* const receiver = connection.createReceiver();
|
||||
* });
|
||||
* ```
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class VoiceReceiver extends EventEmitter {
|
||||
constructor(connection) {
|
||||
super();
|
||||
/*
|
||||
Need a queue because we don't get the ssrc of the user speaking until after the first few packets,
|
||||
so we queue up unknown SSRCs until they become known, then empty the queue
|
||||
*/
|
||||
this.queues = new Map();
|
||||
this.pcmStreams = new Map();
|
||||
this.opusStreams = new Map();
|
||||
this.opusEncoders = new Map();
|
||||
|
||||
/**
|
||||
* Whether or not this receiver has been destroyed
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.destroyed = false;
|
||||
|
||||
/**
|
||||
* The VoiceConnection that instantiated this
|
||||
* @type {VoiceConnection}
|
||||
*/
|
||||
this.voiceConnection = connection;
|
||||
|
||||
this._listener = msg => {
|
||||
const ssrc = +msg.readUInt32BE(8).toString(10);
|
||||
const user = this.voiceConnection.ssrcMap.get(ssrc);
|
||||
if (!user) {
|
||||
if (!this.queues.has(ssrc)) this.queues.set(ssrc, []);
|
||||
this.queues.get(ssrc).push(msg);
|
||||
} else {
|
||||
if (this.queues.get(ssrc)) {
|
||||
this.queues.get(ssrc).push(msg);
|
||||
this.queues.get(ssrc).map(m => this.handlePacket(m, user));
|
||||
this.queues.delete(ssrc);
|
||||
return;
|
||||
}
|
||||
this.handlePacket(msg, user);
|
||||
}
|
||||
};
|
||||
this.voiceConnection.sockets.udp.socket.on('message', this._listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this VoiceReceiver has been destroyed, running `recreate()` will recreate the listener.
|
||||
* This avoids you having to create a new receiver.
|
||||
* <info>Any streams that you had prior to destroying the receiver will not be recreated.</info>
|
||||
*/
|
||||
recreate() {
|
||||
if (!this.destroyed) return;
|
||||
this.voiceConnection.sockets.udp.socket.on('message', this._listener);
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this VoiceReceiver, also ending any streams that it may be controlling.
|
||||
*/
|
||||
destroy() {
|
||||
this.voiceConnection.sockets.udp.socket.removeListener('message', this._listener);
|
||||
for (const [id, stream] of this.pcmStreams) {
|
||||
stream._push(null);
|
||||
this.pcmStreams.delete(id);
|
||||
}
|
||||
for (const [id, stream] of this.opusStreams) {
|
||||
stream._push(null);
|
||||
this.opusStreams.delete(id);
|
||||
}
|
||||
for (const [id, encoder] of this.opusEncoders) {
|
||||
encoder.destroy();
|
||||
this.opusEncoders.delete(id);
|
||||
}
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a user stops speaking.
|
||||
* @param {User} user The user that stopped speaking
|
||||
* @private
|
||||
*/
|
||||
stoppedSpeaking(user) {
|
||||
const opusStream = this.opusStreams.get(user.id);
|
||||
const pcmStream = this.pcmStreams.get(user.id);
|
||||
const opusEncoder = this.opusEncoders.get(user.id);
|
||||
if (opusStream) {
|
||||
opusStream.push(null);
|
||||
opusStream.open = false;
|
||||
this.opusStreams.delete(user.id);
|
||||
}
|
||||
if (pcmStream) {
|
||||
pcmStream.push(null);
|
||||
pcmStream.open = false;
|
||||
this.pcmStreams.delete(user.id);
|
||||
}
|
||||
if (opusEncoder) {
|
||||
opusEncoder.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a readable stream for a user that provides opus data while the user is speaking. When the user
|
||||
* stops speaking, the stream is destroyed.
|
||||
* @param {UserResolvable} user The user to create the stream for
|
||||
* @returns {ReadableStream}
|
||||
*/
|
||||
createOpusStream(user) {
|
||||
user = this.voiceConnection.voiceManager.client.users.resolve(user);
|
||||
if (!user) throw new Error('VOICE_USER_MISSING');
|
||||
if (this.opusStreams.get(user.id)) throw new Error('VOICE_STREAM_EXISTS');
|
||||
const stream = new Readable();
|
||||
this.opusStreams.set(user.id, stream);
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a readable stream for a user that provides PCM data while the user is speaking. When the user
|
||||
* stops speaking, the stream is destroyed. The stream is 32-bit signed stereo PCM at 48KHz.
|
||||
* @param {UserResolvable} user The user to create the stream for
|
||||
* @returns {ReadableStream}
|
||||
*/
|
||||
createPCMStream(user) {
|
||||
user = this.voiceConnection.voiceManager.client.users.resolve(user);
|
||||
if (!user) throw new Error('VOICE_USER_MISSING');
|
||||
if (this.pcmStreams.get(user.id)) throw new Error('VOICE_STREAM_EXISTS');
|
||||
const stream = new Readable();
|
||||
this.pcmStreams.set(user.id, stream);
|
||||
return stream;
|
||||
}
|
||||
|
||||
handlePacket(msg, user) {
|
||||
msg.copy(nonce, 0, 0, 12);
|
||||
let data = secretbox.methods.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key);
|
||||
if (!data) {
|
||||
/**
|
||||
* Emitted whenever a voice packet experiences a problem.
|
||||
* @event VoiceReceiver#warn
|
||||
* @param {string} reason The reason for the warning. If it happened because the voice packet could not be
|
||||
* decrypted, this would be `decrypt`. If it happened because the voice packet could not be decoded into
|
||||
* PCM, this would be `decode`
|
||||
* @param {string} message The warning message
|
||||
*/
|
||||
this.emit('warn', 'decrypt', 'Failed to decrypt voice packet');
|
||||
return;
|
||||
}
|
||||
data = Buffer.from(data);
|
||||
|
||||
// Strip RTP Header Extensions (one-byte only)
|
||||
if (data[0] === 0xBE && data[1] === 0xDE && data.length > 4) {
|
||||
const headerExtensionLength = data.readUInt16BE(2);
|
||||
let offset = 4;
|
||||
for (let i = 0; i < headerExtensionLength; i++) {
|
||||
const byte = data[offset];
|
||||
offset++;
|
||||
if (byte === 0) {
|
||||
continue;
|
||||
}
|
||||
offset += 1 + (0b1111 & (byte >> 4));
|
||||
}
|
||||
while (data[offset] === 0) {
|
||||
offset++;
|
||||
}
|
||||
data = data.slice(offset);
|
||||
}
|
||||
|
||||
if (this.opusStreams.get(user.id)) this.opusStreams.get(user.id)._push(data);
|
||||
/**
|
||||
* Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM).
|
||||
* @event VoiceReceiver#opus
|
||||
* @param {User} user The user that is sending the buffer (is speaking)
|
||||
* @param {Buffer} buffer The opus buffer
|
||||
*/
|
||||
this.emit('opus', user, data);
|
||||
if (this.listenerCount('pcm') > 0 || this.pcmStreams.size > 0) {
|
||||
if (!this.opusEncoders.get(user.id)) this.opusEncoders.set(user.id, OpusEncoders.fetch());
|
||||
const { pcm, error } = VoiceReceiver._tryDecode(this.opusEncoders.get(user.id), data);
|
||||
if (error) {
|
||||
this.emit('warn', 'decode', `Failed to decode packet voice to PCM because: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm);
|
||||
/**
|
||||
* Emits decoded voice data when it's received. For performance reasons, the decoding will only
|
||||
* happen if there is at least one `pcm` listener on this receiver.
|
||||
* @event VoiceReceiver#pcm
|
||||
* @param {User} user The user that is sending the buffer (is speaking)
|
||||
* @param {Buffer} buffer The decoded buffer
|
||||
*/
|
||||
this.emit('pcm', user, pcm);
|
||||
}
|
||||
}
|
||||
|
||||
static _tryDecode(encoder, data) {
|
||||
try {
|
||||
return { pcm: encoder.decode(data) };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VoiceReceiver;
|
||||
40
src/client/voice/util/DispatcherSet.js
Normal file
40
src/client/voice/util/DispatcherSet.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
/**
|
||||
* A "store" for handling broadcast dispatcher (un)subscription
|
||||
* @private
|
||||
*/
|
||||
class DispatcherSet extends Set {
|
||||
constructor(broadcast) {
|
||||
super();
|
||||
/**
|
||||
* The broadcast that this set belongs to
|
||||
* @type {VoiceBroadcast}
|
||||
*/
|
||||
this.broadcast = broadcast;
|
||||
}
|
||||
|
||||
add(dispatcher) {
|
||||
super.add(dispatcher);
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher subscribes to the broadcast.
|
||||
* @event VoiceBroadcast#subscribe
|
||||
* @param {StreamDispatcher} dispatcher The subscribed dispatcher
|
||||
*/
|
||||
this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(dispatcher) {
|
||||
const ret = super.delete(dispatcher);
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher unsubscribes to the broadcast.
|
||||
* @event VoiceBroadcast#unsubscribe
|
||||
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
|
||||
*/
|
||||
if (ret) this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DispatcherSet;
|
||||
93
src/client/voice/util/PlayInterface.js
Normal file
93
src/client/voice/util/PlayInterface.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { Readable } = require('stream');
|
||||
const prism = require('prism-media');
|
||||
const { Error } = require('../../../errors');
|
||||
|
||||
/**
|
||||
* Options that can be passed to stream-playing methods:
|
||||
* @typedef {Object} StreamOptions
|
||||
* @property {StreamType} [type='unknown'] The type of stream.
|
||||
* @property {number} [seek=0] The time to seek to
|
||||
* @property {number|boolean} [volume=1] The volume to play at. Set this to false to disable volume transforms for
|
||||
* this stream to improve performance.
|
||||
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
|
||||
* @property {number} [plp] Expected packet loss percentage
|
||||
* @property {boolean} [fec] Enabled forward error correction
|
||||
* @property {number|string} [bitrate=96] The bitrate (quality) of the audio in kbps.
|
||||
* If set to 'auto', the voice channel's bitrate will be used
|
||||
* @property {number} [highWaterMark=12] The maximum number of opus packets to make and store before they are
|
||||
* actually needed. See https://nodejs.org/en/docs/guides/backpressuring-in-streams/. Setting this value to
|
||||
* 1 means that changes in volume will be more instant.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An option passed as part of `StreamOptions` specifying the type of the stream.
|
||||
* * `unknown`: The default type, streams/input will be passed through to ffmpeg before encoding.
|
||||
* Will play most streams.
|
||||
* * `converted`: Play a stream of 16bit signed stereo PCM data, skipping ffmpeg.
|
||||
* * `opus`: Play a stream of opus packets, skipping ffmpeg. You lose the ability to alter volume.
|
||||
* * `ogg/opus`: Play an ogg file with the opus encoding, skipping ffmpeg. You lose the ability to alter volume.
|
||||
* * `webm/opus`: Play a webm file with opus audio, skipping ffmpeg. You lose the ability to alter volume.
|
||||
* @typedef {string} StreamType
|
||||
*/
|
||||
|
||||
/**
|
||||
* An interface class to allow you to play audio over VoiceConnections and VoiceBroadcasts.
|
||||
*/
|
||||
class PlayInterface {
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play an audio resource.
|
||||
* @param {VoiceBroadcast|ReadableStream|string} resource The resource to play.
|
||||
* @param {StreamOptions} [options] The options to play.
|
||||
* @example
|
||||
* // Play a local audio file
|
||||
* connection.play('/home/hydrabolt/audio.mp3', { volume: 0.5 });
|
||||
* @example
|
||||
* // Play a ReadableStream
|
||||
* connection.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' }));
|
||||
* @example
|
||||
* // Play a voice broadcast
|
||||
* const broadcast = client.createVoiceBroadcast();
|
||||
* broadcast.play('/home/hydrabolt/audio.mp3');
|
||||
* connection.play(broadcast);
|
||||
* @example
|
||||
* // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html
|
||||
* connection.play('http://www.sample-videos.com/audio/mp3/wave.mp3');
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
play(resource, options = {}) {
|
||||
if (resource instanceof Broadcast) {
|
||||
if (!this.player.playBroadcast) throw new Error('VOICE_PLAY_INTERFACE_NO_BROADCAST');
|
||||
return this.player.playBroadcast(resource, options);
|
||||
}
|
||||
const type = options.type || 'unknown';
|
||||
if (type === 'unknown') {
|
||||
return this.player.playUnknown(resource, options);
|
||||
} else if (type === 'converted') {
|
||||
return this.player.playPCMStream(resource, options);
|
||||
} else if (type === 'opus') {
|
||||
return this.player.playOpusStream(resource, options);
|
||||
} else if (type === 'ogg/opus') {
|
||||
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
|
||||
return this.player.playOpusStream(resource.pipe(new prism.OggOpusDemuxer()));
|
||||
} else if (type === 'webm/opus') {
|
||||
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
|
||||
return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer()));
|
||||
}
|
||||
throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE');
|
||||
}
|
||||
|
||||
static applyToClass(structure) {
|
||||
for (const prop of ['play']) {
|
||||
Object.defineProperty(structure.prototype, prop,
|
||||
Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlayInterface;
|
||||
|
||||
const Broadcast = require('../VoiceBroadcast');
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Represents a Secret Key used in encryption over voice.
|
||||
* @private
|
||||
*/
|
||||
class SecretKey {
|
||||
constructor(key) {
|
||||
/**
|
||||
* The key used for encryption
|
||||
* @type {Uint8Array}
|
||||
*/
|
||||
this.key = new Uint8Array(new ArrayBuffer(key.length));
|
||||
for (const index in key) this.key[index] = key[index];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SecretKey;
|
||||
@@ -5,13 +5,21 @@ const EventEmitter = require('events');
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class VolumeInterface extends EventEmitter {
|
||||
constructor({ volume = 0 } = {}) {
|
||||
constructor({ volume = 1 } = {}) {
|
||||
super();
|
||||
this.setVolume(volume || 1);
|
||||
this.setVolume(volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current volume of the broadcast
|
||||
* Whether or not the volume of this stream is editable
|
||||
* @type {boolean}
|
||||
*/
|
||||
get volumeEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current volume of the stream
|
||||
* @readonly
|
||||
* @type {number}
|
||||
*/
|
||||
@@ -20,21 +28,21 @@ class VolumeInterface extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The current volume of the broadcast in decibels
|
||||
* The current volume of the stream in decibels
|
||||
* @readonly
|
||||
* @type {number}
|
||||
*/
|
||||
get volumeDecibels() {
|
||||
return Math.log10(this._volume) * 20;
|
||||
return Math.log10(this.volume) * 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current volume of the broadcast from a logarithmic scale
|
||||
* The current volume of the stream from a logarithmic scale
|
||||
* @readonly
|
||||
* @type {number}
|
||||
*/
|
||||
get volumeLogarithmic() {
|
||||
return Math.pow(this._volume, 1 / 1.660964);
|
||||
return Math.pow(this.volume, 1 / 1.660964);
|
||||
}
|
||||
|
||||
applyVolume(buffer, volume) {
|
||||
@@ -83,4 +91,19 @@ class VolumeInterface extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VolumeInterface;
|
||||
const props = [
|
||||
'volumeDecibels',
|
||||
'volumeLogarithmic',
|
||||
'setVolumeDecibels',
|
||||
'setVolumeLogarithmic',
|
||||
];
|
||||
|
||||
exports.applyToClass = function applyToClass(structure) {
|
||||
for (const prop of props) {
|
||||
Object.defineProperty(
|
||||
structure.prototype,
|
||||
prop,
|
||||
Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -269,13 +269,15 @@ class WebSocketConnection extends EventEmitter {
|
||||
|
||||
this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
|
||||
if (!flush) return;
|
||||
let packet;
|
||||
try {
|
||||
const packet = WebSocket.unpack(this.inflate.result);
|
||||
this.onPacket(packet);
|
||||
if (this.client.listenerCount('raw')) this.client.emit('raw', packet);
|
||||
packet = WebSocket.unpack(this.inflate.result);
|
||||
} catch (err) {
|
||||
this.client.emit('debug', err);
|
||||
return;
|
||||
}
|
||||
this.onPacket(packet);
|
||||
if (this.client.listenerCount('raw')) this.client.emit('raw', packet);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ class GuildCreateHandler extends AbstractHandler {
|
||||
}
|
||||
} else {
|
||||
// A new guild
|
||||
guild = client.guilds.create(data);
|
||||
guild = client.guilds.add(data);
|
||||
const emitEvent = client.ws.connection.status === Status.READY;
|
||||
if (emitEvent) {
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ class GuildMemberAddHandler extends AbstractHandler {
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
if (guild) {
|
||||
guild.memberCount++;
|
||||
const member = guild.members.create(data);
|
||||
const member = guild.members.add(data);
|
||||
if (client.ws.connection.status === Status.READY) {
|
||||
client.emit(Events.GUILD_MEMBER_ADD, member);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ class GuildMembersChunkHandler extends AbstractHandler {
|
||||
if (!guild) return;
|
||||
const members = new Collection();
|
||||
|
||||
for (const member of data.members) members.set(member.user.id, guild.members.create(member));
|
||||
for (const member of data.members) members.set(member.user.id, guild.members.add(member));
|
||||
|
||||
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class PresenceUpdateHandler extends AbstractHandler {
|
||||
// Step 1
|
||||
if (!user) {
|
||||
if (data.user.username) {
|
||||
user = client.users.create(data.user);
|
||||
user = client.users.add(data.user);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class PresenceUpdateHandler extends AbstractHandler {
|
||||
if (guild) {
|
||||
let member = guild.members.get(user.id);
|
||||
if (!member && data.status !== 'offline') {
|
||||
member = guild.members.create({
|
||||
member = guild.members.add({
|
||||
user,
|
||||
roles: data.roles,
|
||||
deaf: false,
|
||||
@@ -35,17 +35,17 @@ class PresenceUpdateHandler extends AbstractHandler {
|
||||
}
|
||||
if (member) {
|
||||
if (client.listenerCount(Events.PRESENCE_UPDATE) === 0) {
|
||||
guild.presences.create(data);
|
||||
guild.presences.add(data);
|
||||
return;
|
||||
}
|
||||
const oldMember = member._clone();
|
||||
if (member.presence) {
|
||||
oldMember.frozenPresence = member.presence._clone();
|
||||
}
|
||||
guild.presences.create(data);
|
||||
guild.presences.add(data);
|
||||
client.emit(Events.PRESENCE_UPDATE, oldMember, member);
|
||||
} else {
|
||||
guild.presences.create(data);
|
||||
guild.presences.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ class ReadyHandler extends AbstractHandler {
|
||||
client.readyAt = new Date();
|
||||
client.users.set(clientUser.id, clientUser);
|
||||
|
||||
for (const guild of data.guilds) client.guilds.create(guild);
|
||||
for (const privateDM of data.private_channels) client.channels.create(privateDM);
|
||||
for (const guild of data.guilds) client.guilds.add(guild);
|
||||
for (const privateDM of data.private_channels) client.channels.add(privateDM);
|
||||
|
||||
for (const relation of data.relationships) {
|
||||
const user = client.users.create(relation.user);
|
||||
const user = client.users.add(relation.user);
|
||||
if (relation.type === 1) {
|
||||
client.user.friends.set(user.id, user);
|
||||
} else if (relation.type === 2) {
|
||||
@@ -30,7 +30,7 @@ class ReadyHandler extends AbstractHandler {
|
||||
}
|
||||
}
|
||||
|
||||
for (const presence of data.presences || []) client.presences.create(presence);
|
||||
for (const presence of data.presences || []) client.presences.add(presence);
|
||||
|
||||
if (data.notes) {
|
||||
for (const user in data.notes) {
|
||||
@@ -42,7 +42,7 @@ class ReadyHandler extends AbstractHandler {
|
||||
}
|
||||
|
||||
if (!client.users.has('1')) {
|
||||
client.users.create({
|
||||
client.users.add({
|
||||
id: '1',
|
||||
username: 'Clyde',
|
||||
discriminator: '0000',
|
||||
|
||||
@@ -32,11 +32,8 @@ const Messages = {
|
||||
COLOR_CONVERT: 'Unable to convert color to a number.',
|
||||
|
||||
EMBED_FIELD_COUNT: 'MessageEmbeds may not exceed 25 fields.',
|
||||
EMBED_FIELD_NAME: 'MessageEmbed field names may not exceed 256 characters or be empty.',
|
||||
EMBED_FIELD_VALUE: 'MessageEmbed field values may not exceed 1024 characters or be empty.',
|
||||
EMBED_DESCRIPTION: 'MessageEmbed descriptions may not exceed 2048 characters.',
|
||||
EMBED_FOOTER_TEXT: 'MessageEmbed footer text may not exceed 2048 characters.',
|
||||
EMBED_TITLE: 'MessageEmbed titles may not exceed 256 characters.',
|
||||
EMBED_FIELD_NAME: 'MessageEmbed field names may not be empty.',
|
||||
EMBED_FIELD_VALUE: 'MessageEmbed field values may not be empty.',
|
||||
|
||||
FILE_NOT_FOUND: file => `File could not be found: ${file}`,
|
||||
|
||||
@@ -54,6 +51,9 @@ const Messages = {
|
||||
VOICE_NO_BROWSER: 'Voice connections are not available in browsers.',
|
||||
VOICE_CONNECTION_ATTEMPTS_EXCEEDED: attempts => `Too many connection attempts (${attempts}).`,
|
||||
VOICE_JOIN_SOCKET_CLOSED: 'Tried to send join packet, but the WebSocket is not open.',
|
||||
VOICE_PLAY_INTERFACE_NO_BROADCAST: 'A broadcast cannot be played in this context.',
|
||||
VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type',
|
||||
VOICE_PRISM_DEMUXERS_NEED_STREAM: 'To play a webm/ogg stream, you need to pass a ReadableStream.',
|
||||
|
||||
OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.',
|
||||
|
||||
@@ -93,7 +93,7 @@ const Messages = {
|
||||
|
||||
WEBHOOK_MESSAGE: 'The message was not sent by a webhook.',
|
||||
|
||||
EMOJI_TYPE: 'Emoji must be a string or Emoji/ReactionEmoji',
|
||||
EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji',
|
||||
|
||||
REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.',
|
||||
};
|
||||
|
||||
@@ -26,9 +26,11 @@ module.exports = {
|
||||
// Stores
|
||||
ChannelStore: require('./stores/ChannelStore'),
|
||||
ClientPresenceStore: require('./stores/ClientPresenceStore'),
|
||||
EmojiStore: require('./stores/EmojiStore'),
|
||||
GuildChannelStore: require('./stores/GuildChannelStore'),
|
||||
GuildEmojiStore: require('./stores/GuildEmojiStore'),
|
||||
GuildEmojiRoleStore: require('./stores/GuildEmojiRoleStore'),
|
||||
GuildMemberStore: require('./stores/GuildMemberStore'),
|
||||
GuildMemberRoleStore: require('./stores/GuildMemberRoleStore'),
|
||||
GuildStore: require('./stores/GuildStore'),
|
||||
ReactionUserStore: require('./stores/ReactionUserStore'),
|
||||
MessageStore: require('./stores/MessageStore'),
|
||||
@@ -37,8 +39,11 @@ module.exports = {
|
||||
UserStore: require('./stores/UserStore'),
|
||||
|
||||
// Shortcuts to Util methods
|
||||
discordSort: Util.discordSort,
|
||||
escapeMarkdown: Util.escapeMarkdown,
|
||||
fetchRecommendedShards: Util.fetchRecommendedShards,
|
||||
resolveColor: Util.resolveColor,
|
||||
resolveString: Util.resolveString,
|
||||
splitMessage: Util.splitMessage,
|
||||
|
||||
// Structures
|
||||
@@ -61,6 +66,7 @@ module.exports = {
|
||||
Guild: require('./structures/Guild'),
|
||||
GuildAuditLogs: require('./structures/GuildAuditLogs'),
|
||||
GuildChannel: require('./structures/GuildChannel'),
|
||||
GuildEmoji: require('./structures/GuildEmoji'),
|
||||
GuildMember: require('./structures/GuildMember'),
|
||||
Invite: require('./structures/Invite'),
|
||||
Message: require('./structures/Message'),
|
||||
|
||||
@@ -10,20 +10,17 @@ class APIRequest {
|
||||
this.rest = rest;
|
||||
this.client = rest.client;
|
||||
this.method = method;
|
||||
this.path = path.toString();
|
||||
this.route = options.route;
|
||||
this.options = options;
|
||||
|
||||
const queryString = (querystring.stringify(options.query).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
|
||||
this.path = `${path}${queryString ? `?${queryString}` : ''}`;
|
||||
}
|
||||
|
||||
gen() {
|
||||
const API = this.options.versioned === false ? this.client.options.http.api :
|
||||
`${this.client.options.http.api}/v${this.client.options.http.version}`;
|
||||
|
||||
if (this.options.query) {
|
||||
const queryString = (querystring.stringify(this.options.query).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
|
||||
this.path += `?${queryString}`;
|
||||
}
|
||||
|
||||
const request = snekfetch[this.method](`${API}${this.path}`, { agent });
|
||||
|
||||
if (this.options.auth !== false) request.set('Authorization', this.rest.getAuth());
|
||||
|
||||
@@ -42,6 +42,7 @@ class Shard extends EventEmitter {
|
||||
* @type {Object}
|
||||
*/
|
||||
this.env = Object.assign({}, process.env, {
|
||||
SHARDING_MANAGER: true,
|
||||
SHARD_ID: this.id,
|
||||
SHARD_COUNT: this.manager.totalShards,
|
||||
CLIENT_TOKEN: this.manager.token,
|
||||
@@ -174,8 +175,8 @@ class Shard extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a script on the shard, in the context of the {@link Client}.
|
||||
* @param {string} script JavaScript to run on the shard
|
||||
* Evaluates a script or function on the shard, in the context of the {@link Client}.
|
||||
* @param {string|Function} script JavaScript to run on the shard
|
||||
* @returns {Promise<*>} Result of the script execution
|
||||
*/
|
||||
eval(script) {
|
||||
@@ -190,7 +191,8 @@ class Shard extends EventEmitter {
|
||||
};
|
||||
this.process.on('message', listener);
|
||||
|
||||
this.send({ _eval: script }).catch(err => {
|
||||
const _eval = typeof script === 'function' ? `(${script})(this)` : script;
|
||||
this.send({ _eval }).catch(err => {
|
||||
this.process.removeListener('message', listener);
|
||||
this._evals.delete(script);
|
||||
reject(err);
|
||||
|
||||
@@ -86,6 +86,7 @@ class ShardClientUtil {
|
||||
*/
|
||||
broadcastEval(script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
script = typeof script === 'function' ? `(${script})(this)` : script;
|
||||
const listener = message => {
|
||||
if (!message || message._sEval !== script) return;
|
||||
process.removeListener('message', listener);
|
||||
@@ -118,7 +119,7 @@ class ShardClientUtil {
|
||||
* @param {*} message Message received
|
||||
* @private
|
||||
*/
|
||||
_handleMessage(message) {
|
||||
async _handleMessage(message) {
|
||||
if (!message) return;
|
||||
if (message._fetchProp) {
|
||||
const props = message._fetchProp.split('.');
|
||||
@@ -127,7 +128,7 @@ class ShardClientUtil {
|
||||
this._respond('fetchProp', { _fetchProp: message._fetchProp, _result: value });
|
||||
} else if (message._eval) {
|
||||
try {
|
||||
this._respond('eval', { _eval: message._eval, _result: this.client._eval(message._eval) });
|
||||
this._respond('eval', { _eval: message._eval, _result: await this.client._eval(message._eval) });
|
||||
} catch (err) {
|
||||
this._respond('eval', { _eval: message._eval, _error: Util.makePlainError(err) });
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ const lruable = ['group', 'dm'];
|
||||
|
||||
/**
|
||||
* Stores channels.
|
||||
* @private
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class ChannelStore extends DataStore {
|
||||
@@ -51,7 +50,7 @@ class ChannelStore extends DataStore {
|
||||
return super.delete(key);
|
||||
}
|
||||
|
||||
create(data, guild, cache = true) {
|
||||
add(data, guild, cache = true) {
|
||||
const existing = this.get(data.id);
|
||||
if (existing) return existing;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ const { TypeError } = require('../errors');
|
||||
/**
|
||||
* Stores the client presence and other presences.
|
||||
* @extends {PresenceStore}
|
||||
* @private
|
||||
*/
|
||||
class ClientPresenceStore extends PresenceStore {
|
||||
constructor(...args) {
|
||||
@@ -20,7 +19,14 @@ class ClientPresenceStore extends PresenceStore {
|
||||
});
|
||||
}
|
||||
|
||||
async setClientPresence({ status, since, afk, activity }) { // eslint-disable-line complexity
|
||||
async setClientPresence(presence) {
|
||||
const packet = await this._parse(presence);
|
||||
this.clientPresence.patch(packet);
|
||||
this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet });
|
||||
return this.clientPresence;
|
||||
}
|
||||
|
||||
async _parse({ status, since, afk, activity }) { // eslint-disable-line complexity
|
||||
const applicationID = activity && (activity.application ? activity.application.id || activity.application : null);
|
||||
let assets = new Collection();
|
||||
if (activity) {
|
||||
@@ -39,7 +45,7 @@ class ClientPresenceStore extends PresenceStore {
|
||||
since: since != null ? since : null, // eslint-disable-line eqeqeq
|
||||
status: status || this.clientPresence.status,
|
||||
game: activity ? {
|
||||
type: typeof activity.type === 'number' ? activity.type : ActivityTypes.indexOf(activity.type),
|
||||
type: activity.type,
|
||||
name: activity.name,
|
||||
url: activity.url,
|
||||
details: activity.details || undefined,
|
||||
@@ -58,9 +64,16 @@ class ClientPresenceStore extends PresenceStore {
|
||||
} : null,
|
||||
};
|
||||
|
||||
this.clientPresence.patch(packet);
|
||||
this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet });
|
||||
return this.clientPresence;
|
||||
if ((status || afk || since) && !activity) {
|
||||
packet.game = this.clientPresence.activity;
|
||||
}
|
||||
|
||||
if (packet.game) {
|
||||
packet.game.type = typeof packet.game.type === 'number' ?
|
||||
packet.game.type : ActivityTypes.indexOf(packet.game.type);
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ class DataStore extends Collection {
|
||||
if (!Structures) Structures = require('../util/Structures');
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) || holds });
|
||||
if (iterable) for (const item of iterable) this.create(item);
|
||||
if (iterable) for (const item of iterable) this.add(item);
|
||||
}
|
||||
|
||||
create(data, cache = true, { id, extras = [] } = {}) {
|
||||
add(data, cache = true, { id, extras = [] } = {}) {
|
||||
const existing = this.get(id || data.id);
|
||||
if (existing) return existing;
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
const DataStore = require('./DataStore');
|
||||
const Emoji = require('../structures/Emoji');
|
||||
const ReactionEmoji = require('../structures/ReactionEmoji');
|
||||
|
||||
/**
|
||||
* Stores emojis.
|
||||
* @private
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class EmojiStore extends DataStore {
|
||||
constructor(guild, iterable) {
|
||||
super(guild.client, iterable, Emoji);
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
create(data, cache) {
|
||||
return super.create(data, cache, { extras: [this.guild] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved into an Emoji object. This can be:
|
||||
* * A custom emoji ID
|
||||
* * An Emoji object
|
||||
* * A ReactionEmoji object
|
||||
* @typedef {Snowflake|Emoji|ReactionEmoji} EmojiResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a EmojiResolvable to a Emoji object.
|
||||
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
|
||||
* @returns {?Emoji}
|
||||
*/
|
||||
resolve(emoji) {
|
||||
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
|
||||
return super.resolve(emoji);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a EmojiResolvable to a 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 EmojiResolveable
|
||||
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves an EmojiResolvable to an emoji identifier.
|
||||
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
|
||||
* @returns {?string}
|
||||
*/
|
||||
resolveIdentifier(emoji) {
|
||||
const emojiResolveable = this.resolve(emoji);
|
||||
if (emojiResolveable) return emojiResolveable.identifier;
|
||||
if (typeof emoji === 'string') {
|
||||
if (!emoji.includes('%')) return encodeURIComponent(emoji);
|
||||
else return emoji;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EmojiStore;
|
||||
@@ -1,10 +1,12 @@
|
||||
const DataStore = require('./DataStore');
|
||||
const Collection = require('../util/Collection');
|
||||
const Channel = require('../structures/Channel');
|
||||
const { ChannelTypes } = require('../util/Constants');
|
||||
const DataStore = require('./DataStore');
|
||||
const GuildChannel = require('../structures/GuildChannel');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* Stores guild channels.
|
||||
* @private
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class GuildChannelStore extends DataStore {
|
||||
@@ -13,13 +15,80 @@ class GuildChannelStore extends DataStore {
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
create(data) {
|
||||
add(data) {
|
||||
const existing = this.get(data.id);
|
||||
if (existing) return existing;
|
||||
|
||||
return Channel.create(this.client, data, this.guild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to overwrite permissions when creating a channel.
|
||||
* @typedef {Object} ChannelCreationOverwrites
|
||||
* @property {PermissionResolvable[]|number} [allow] The permissions to allow
|
||||
* @property {PermissionResolvable[]|number} [deny] The permissions to deny
|
||||
* @property {RoleResolvable|UserResolvable} id ID of the role or member this overwrite is for
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new channel in the guild.
|
||||
* @param {string} name The name of the new channel
|
||||
* @param {Object} [options] Options
|
||||
* @param {string} [options.type='text'] The type of the new channel, either `text`, `voice`, or `category`
|
||||
* @param {boolean} [options.nsfw] Whether the new channel is nsfw
|
||||
* @param {number} [options.bitrate] Bitrate of the new channel in bits (only voice)
|
||||
* @param {number} [options.userLimit] Maximum amount of users allowed in the new channel (only voice)
|
||||
* @param {ChannelResolvable} [options.parent] Parent of the new channel
|
||||
* @param {Array<PermissionOverwrites|ChannelCreationOverwrites>} [options.overwrites] Permission overwrites
|
||||
* @param {string} [options.reason] Reason for creating the channel
|
||||
* @returns {Promise<GuildChannel>}
|
||||
* @example
|
||||
* // Create a new text channel
|
||||
* guild.channels.create('new-general', { reason: 'Needed a cool new channel' })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
create(name, { type, nsfw, bitrate, userLimit, parent, overwrites, reason } = {}) {
|
||||
if (overwrites instanceof Collection || overwrites instanceof Array) {
|
||||
overwrites = overwrites.map(overwrite => {
|
||||
let allow = overwrite.allow || (overwrite.allowed ? overwrite.allowed.bitfield : 0);
|
||||
let deny = overwrite.deny || (overwrite.denied ? overwrite.denied.bitfield : 0);
|
||||
if (allow instanceof Array) allow = Permissions.resolve(allow);
|
||||
if (deny instanceof Array) deny = Permissions.resolve(deny);
|
||||
|
||||
const role = this.guild.roles.resolve(overwrite.id);
|
||||
if (role) {
|
||||
overwrite.id = role.id;
|
||||
overwrite.type = 'role';
|
||||
} else {
|
||||
overwrite.id = this.client.users.resolveID(overwrite.id);
|
||||
overwrite.type = 'member';
|
||||
}
|
||||
|
||||
return {
|
||||
allow,
|
||||
deny,
|
||||
type: overwrite.type,
|
||||
id: overwrite.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (parent) parent = this.client.channels.resolveID(parent);
|
||||
return this.client.api.guilds(this.guild.id).channels.post({
|
||||
data: {
|
||||
name,
|
||||
type: type ? ChannelTypes[type.toUpperCase()] : 'text',
|
||||
nsfw,
|
||||
bitrate,
|
||||
user_limit: userLimit,
|
||||
parent_id: parent,
|
||||
permission_overwrites: overwrites,
|
||||
},
|
||||
reason,
|
||||
}).then(data => this.client.actions.ChannelCreate.handle(data).channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a Guild Channel object. This can be:
|
||||
* * A GuildChannel object
|
||||
|
||||
110
src/stores/GuildEmojiRoleStore.js
Normal file
110
src/stores/GuildEmojiRoleStore.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const DataStore = require('./DataStore');
|
||||
const Collection = require('../util/Collection');
|
||||
const { TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Stores emoji roles
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class GuildEmojiRoleStore extends DataStore {
|
||||
constructor(emoji) {
|
||||
super(emoji.client, null, require('../structures/GuildEmoji'));
|
||||
this.emoji = emoji;
|
||||
this.guild = emoji.guild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role (or multiple roles) to the list of roles that can use this emoji.
|
||||
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add
|
||||
* @returns {Promise<GuildEmoji>}
|
||||
*/
|
||||
add(roleOrRoles) {
|
||||
if (roleOrRoles instanceof Collection) return this.add(roleOrRoles.keyArray());
|
||||
if (!(roleOrRoles instanceof Array)) return this.add([roleOrRoles]);
|
||||
|
||||
roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r));
|
||||
|
||||
if (roleOrRoles.includes(null)) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
} else {
|
||||
for (const role of roleOrRoles) super.set(role.id, role);
|
||||
}
|
||||
|
||||
return this.set(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role (or multiple roles) from the list of roles that can use this emoji.
|
||||
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove
|
||||
* @returns {Promise<GuildEmoji>}
|
||||
*/
|
||||
remove(roleOrRoles) {
|
||||
if (roleOrRoles instanceof Collection) return this.remove(roleOrRoles.keyArray());
|
||||
if (!(roleOrRoles instanceof Array)) return this.remove([roleOrRoles]);
|
||||
|
||||
roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolveID(r));
|
||||
|
||||
if (roleOrRoles.includes(null)) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
} else {
|
||||
for (const role of roleOrRoles) super.remove(role);
|
||||
}
|
||||
|
||||
return this.set(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the role(s) that can use this emoji.
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to apply
|
||||
* @returns {Promise<GuildEmoji>}
|
||||
* @example
|
||||
* // Set the emoji's roles to a single role
|
||||
* guildEmoji.roles.set(['391156570408615936'])
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Remove all roles from an emoji
|
||||
* guildEmoji.roles.set([])
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
set(roles) {
|
||||
return this.emoji.edit({ roles });
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the roles for this store
|
||||
* @param {Snowflake[]} roles The new roles
|
||||
* @private
|
||||
*/
|
||||
_patch(roles) {
|
||||
this.clear();
|
||||
|
||||
for (let role of roles) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (role) super.set(role.id, role);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a RoleResolvable to a Role object.
|
||||
* @method resolve
|
||||
* @memberof GuildEmojiRoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Role}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a RoleResolvable to a role ID string.
|
||||
* @method resolveID
|
||||
* @memberof GuildEmojiRoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = GuildEmojiRoleStore;
|
||||
114
src/stores/GuildEmojiStore.js
Normal file
114
src/stores/GuildEmojiStore.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const Collection = require('../util/Collection');
|
||||
const DataStore = require('./DataStore');
|
||||
const GuildEmoji = require('../structures/GuildEmoji');
|
||||
const ReactionEmoji = require('../structures/ReactionEmoji');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const { TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Stores guild emojis.
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class GuildEmojiStore extends DataStore {
|
||||
constructor(guild, iterable) {
|
||||
super(guild.client, iterable, GuildEmoji);
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
add(data, cache) {
|
||||
return super.add(data, cache, { extras: [this.guild] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new custom emoji in the guild.
|
||||
* @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji
|
||||
* @param {string} name The name for the emoji
|
||||
* @param {Object} [options] Options
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} [options.roles] Roles to limit the emoji to
|
||||
* @param {string} [options.reason] Reason for creating the emoji
|
||||
* @returns {Promise<Emoji>} The created emoji
|
||||
* @example
|
||||
* // Create a new emoji from a url
|
||||
* guild.emojis.create('https://i.imgur.com/w3duR07.png', 'rip')
|
||||
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Create a new emoji from a file on your computer
|
||||
* guild.emojis.create('./memes/banana.png', 'banana')
|
||||
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
create(attachment, name, { roles, reason } = {}) {
|
||||
if (typeof attachment === 'string' && attachment.startsWith('data:')) {
|
||||
const data = { image: attachment, name };
|
||||
if (roles) {
|
||||
data.roles = [];
|
||||
for (let role of roles instanceof Collection ? roles.values() : roles) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (!role) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
}
|
||||
data.roles.push(role.id);
|
||||
}
|
||||
}
|
||||
|
||||
return this.client.api.guilds(this.guild.id).emojis.post({ data, reason })
|
||||
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji);
|
||||
}
|
||||
|
||||
return DataResolver.resolveImage(attachment).then(image => this.create(image, name, { roles, reason }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 EmojiResolveable
|
||||
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves an EmojiResolvable to an emoji identifier.
|
||||
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
|
||||
* @returns {?string}
|
||||
*/
|
||||
resolveIdentifier(emoji) {
|
||||
const emojiResolveable = this.resolve(emoji);
|
||||
if (emojiResolveable) return emojiResolveable.identifier;
|
||||
if (typeof emoji === 'string') {
|
||||
if (!emoji.includes('%')) return encodeURIComponent(emoji);
|
||||
else return emoji;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildEmojiStore;
|
||||
156
src/stores/GuildMemberRoleStore.js
Normal file
156
src/stores/GuildMemberRoleStore.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const DataStore = require('./DataStore');
|
||||
const Role = require('../structures/Role');
|
||||
const Collection = require('../util/Collection');
|
||||
const { TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Stores member roles
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class GuildMemberRoleStore extends DataStore {
|
||||
constructor(member) {
|
||||
super(member.client, null, Role);
|
||||
this.member = member;
|
||||
this.guild = member.guild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role (or multiple roles) to the member.
|
||||
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add
|
||||
* @param {string} [reason] Reason for adding the role(s)
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
add(roleOrRoles, reason) {
|
||||
if (roleOrRoles instanceof Collection) return this.add(roleOrRoles.keyArray(), reason);
|
||||
if (!(roleOrRoles instanceof Array)) return this.add([roleOrRoles], reason);
|
||||
|
||||
roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r));
|
||||
|
||||
if (roleOrRoles.includes(null)) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
} else {
|
||||
for (const role of roleOrRoles) super.set(role.id, role);
|
||||
}
|
||||
|
||||
return this.set(this, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the roles applied to the member.
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to apply
|
||||
* @param {string} [reason] Reason for applying the roles
|
||||
* @returns {Promise<GuildMember>}
|
||||
* @example
|
||||
* // Set the member's roles to a single role
|
||||
* guildMember.roles.set(['391156570408615936'])
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Remove all the roles from a member
|
||||
* guildMember.roles.set([])
|
||||
* .then(member => console.log(`Member roles is now of ${member.roles.size} size`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
set(roles, reason) {
|
||||
return this.member.edit({ roles }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role (or multiple roles) from the member.
|
||||
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove
|
||||
* @param {string} [reason] Reason for removing the role(s)
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
remove(roleOrRoles, reason) {
|
||||
if (roleOrRoles instanceof Collection) return this.remove(roleOrRoles.keyArray(), reason);
|
||||
if (!(roleOrRoles instanceof Array)) return this.remove([roleOrRoles], reason);
|
||||
|
||||
roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolveID(r));
|
||||
|
||||
if (roleOrRoles.includes(null)) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
} else {
|
||||
for (const role of roleOrRoles) super.remove(role);
|
||||
}
|
||||
|
||||
return this.set(this, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the member used to hoist them in a separate category in the users list
|
||||
* @type {?Role}
|
||||
* @readonly
|
||||
*/
|
||||
get hoist() {
|
||||
const hoistedRoles = this.filter(role => role.hoist);
|
||||
if (!hoistedRoles.size) return null;
|
||||
return hoistedRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the member used to set their color
|
||||
* @type {?Role}
|
||||
* @readonly
|
||||
*/
|
||||
get color() {
|
||||
const coloredRoles = this.filter(role => role.color);
|
||||
if (!coloredRoles.size) return null;
|
||||
return coloredRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the member with the highest position
|
||||
* @type {Role}
|
||||
* @readonly
|
||||
*/
|
||||
get highest() {
|
||||
return this.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the roles for this store
|
||||
* @param {Snowflake[]} roles The new roles
|
||||
* @private
|
||||
*/
|
||||
_patch(roles) {
|
||||
this.clear();
|
||||
|
||||
const everyoneRole = this.guild.roles.get(this.guild.id);
|
||||
if (everyoneRole) super.set(everyoneRole.id, everyoneRole);
|
||||
|
||||
if (roles) {
|
||||
for (const roleID of roles) {
|
||||
const role = this.guild.roles.resolve(roleID);
|
||||
if (role) super.set(role.id, role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
const clone = new this.constructor(this.member);
|
||||
clone._patch(this.keyArray());
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a RoleResolvable to a Role object.
|
||||
* @method resolve
|
||||
* @memberof GuildMemberRoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Role}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a RoleResolvable to a role ID string.
|
||||
* @method resolveID
|
||||
* @memberof GuildMemberRoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = GuildMemberRoleStore;
|
||||
@@ -2,7 +2,7 @@ const DataStore = require('./DataStore');
|
||||
const GuildMember = require('../structures/GuildMember');
|
||||
const { Events, OPCodes } = require('../util/Constants');
|
||||
const Collection = require('../util/Collection');
|
||||
const { Error } = require('../errors');
|
||||
const { Error, TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Stores guild members.
|
||||
@@ -14,8 +14,8 @@ class GuildMemberStore extends DataStore {
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
create(data, cache) {
|
||||
return super.create(data, cache, { extras: [this.guild] });
|
||||
add(data, cache) {
|
||||
return super.add(data, cache, { extras: [this.guild] });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,21 +72,22 @@ class GuildMemberStore extends DataStore {
|
||||
* @returns {Promise<GuildMember>|Promise<Collection<Snowflake, GuildMember>>}
|
||||
* @example
|
||||
* // Fetch all members from a guild
|
||||
* guild.members.fetch();
|
||||
* guild.members.fetch()
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Fetch a single member
|
||||
* guild.members.fetch('66564597481480192');
|
||||
* guild.members.fetch(user);
|
||||
* guild.members.fetch({ user, cache: false }); // Fetch and don't cache
|
||||
* guild.members.fetch('66564597481480192')
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* guild.members.fetch({ user, cache: false }) // Fetch and don't cache
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Fetch by query
|
||||
* guild.members.fetch({
|
||||
* query: 'hydra',
|
||||
* });
|
||||
* guild.members.fetch({
|
||||
* query: 'hydra',
|
||||
* limit: 10,
|
||||
* });
|
||||
* guild.members.fetch({ query: 'hydra' })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
fetch(options) {
|
||||
if (!options) return this._fetchMany();
|
||||
@@ -99,11 +100,85 @@ class GuildMemberStore extends DataStore {
|
||||
return this._fetchMany(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes members from the guild based on how long they have been inactive.
|
||||
* @param {Object} [options] Prune options
|
||||
* @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 {string} [options.reason] Reason for this prune
|
||||
* @returns {Promise<number>} The number of members that were/will be kicked
|
||||
* @example
|
||||
* // See how many members will be pruned
|
||||
* guild.members.prune({ dry: true })
|
||||
* .then(pruned => console.log(`This will prune ${pruned} people!`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Actually prune the members
|
||||
* guild.members.prune({ days: 1, reason: 'too many people!' })
|
||||
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
prune({ days = 7, dry = false, reason } = {}) {
|
||||
if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
|
||||
return this.client.api.guilds(this.guild.id).prune[dry ? 'get' : 'post']({ query: { days }, reason })
|
||||
.then(data => data.pruned);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
* be resolved, the user ID will be the result.
|
||||
* @example
|
||||
* // Ban a user by ID (or with a user/guild member object)
|
||||
* guild.members.ban('84484653687267328')
|
||||
* .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
ban(user, options = { days: 0 }) {
|
||||
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({ query: options })
|
||||
.then(() => {
|
||||
if (user instanceof GuildMember) return user;
|
||||
const _user = this.client.users.resolve(id);
|
||||
if (_user) {
|
||||
const member = this.resolve(_user);
|
||||
return member || _user;
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbans a user from the guild.
|
||||
* @param {UserResolvable} user The user to unban
|
||||
* @param {string} [reason] Reason for unbanning user
|
||||
* @returns {Promise<User>}
|
||||
* @example
|
||||
* // Unban a user by ID (or with a user/guild member object)
|
||||
* guild.members.unban('84484653687267328')
|
||||
* .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
unban(user, reason) {
|
||||
const id = this.client.users.resolveID(user);
|
||||
if (!id) throw new Error('BAN_RESOLVE_ID');
|
||||
return this.client.api.guilds(this.guild.id).bans[id].delete({ reason })
|
||||
.then(() => user);
|
||||
}
|
||||
|
||||
|
||||
_fetchSingle({ user, cache }) {
|
||||
const existing = this.get(user);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
return this.client.api.guilds(this.guild.id).members(user).get()
|
||||
.then(data => this.create(data, cache));
|
||||
.then(data => this.add(data, cache));
|
||||
}
|
||||
|
||||
_fetchMany({ query = '', limit = 0 } = {}) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const DataStore = require('./DataStore');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const { Events } = require('../util/Constants');
|
||||
const Guild = require('../structures/Guild');
|
||||
|
||||
/**
|
||||
* Stores guilds.
|
||||
* @private
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class GuildStore extends DataStore {
|
||||
@@ -35,6 +36,44 @@ class GuildStore extends DataStore {
|
||||
* @param {GuildResolvable} guild The guild resolvable to identify
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a guild.
|
||||
* <warn>This is only available when using a user account.</warn>
|
||||
* @param {string} name The name of the guild
|
||||
* @param {Object} [options] Options for the creating
|
||||
* @param {string} [options.region] The region for the server, defaults to the closest one available
|
||||
* @param {BufferResolvable|Base64Resolvable} [options.icon=null] The icon for the guild
|
||||
* @returns {Promise<Guild>} The guild that was created
|
||||
*/
|
||||
create(name, { region, icon = null } = {}) {
|
||||
if (!icon || (typeof icon === 'string' && icon.startsWith('data:'))) {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.client.api.guilds.post({ data: { name, region, icon } })
|
||||
.then(data => {
|
||||
if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id));
|
||||
|
||||
const handleGuild = guild => {
|
||||
if (guild.id === data.id) {
|
||||
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
|
||||
this.client.clearTimeout(timeout);
|
||||
resolve(guild);
|
||||
}
|
||||
};
|
||||
this.client.on(Events.GUILD_CREATE, handleGuild);
|
||||
|
||||
const timeout = this.client.setTimeout(() => {
|
||||
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
|
||||
resolve(this.client.guilds.add(data));
|
||||
}, 10000);
|
||||
return undefined;
|
||||
}, reject)
|
||||
);
|
||||
}
|
||||
|
||||
return DataResolver.resolveImage(icon)
|
||||
.then(data => this.create(name, { region, icon: data || null }));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildStore;
|
||||
|
||||
@@ -13,8 +13,8 @@ class MessageStore extends DataStore {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
create(data, cache) {
|
||||
return super.create(data, cache, { extras: [this.channel] });
|
||||
add(data, cache) {
|
||||
return super.add(data, cache, { extras: [this.channel] });
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
@@ -62,7 +62,7 @@ class MessageStore extends DataStore {
|
||||
fetchPinned() {
|
||||
return this.client.api.channels[this.channel.id].pins.get().then(data => {
|
||||
const messages = new Collection();
|
||||
for (const message of data) messages.set(message.id, this.create(message));
|
||||
for (const message of data) messages.set(message.id, this.add(message));
|
||||
return messages;
|
||||
});
|
||||
}
|
||||
@@ -77,14 +77,14 @@ class MessageStore extends DataStore {
|
||||
});
|
||||
}
|
||||
return this.client.api.channels[this.channel.id].messages[messageID].get()
|
||||
.then(data => this.create(data));
|
||||
.then(data => this.add(data));
|
||||
}
|
||||
|
||||
_fetchMany(options = {}) {
|
||||
return this.client.api.channels[this.channel.id].messages.get({ query: options })
|
||||
.then(data => {
|
||||
const messages = new Collection();
|
||||
for (const message of data) messages.set(message.id, this.create(message));
|
||||
for (const message of data) messages.set(message.id, this.add(message));
|
||||
return messages;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ const { Presence } = require('../structures/Presence');
|
||||
|
||||
/**
|
||||
* Stores presences.
|
||||
* @private
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class PresenceStore extends DataStore {
|
||||
@@ -11,9 +10,9 @@ class PresenceStore extends DataStore {
|
||||
super(client, iterable, Presence);
|
||||
}
|
||||
|
||||
create(data, cache) {
|
||||
add(data, cache) {
|
||||
const existing = this.get(data.user.id);
|
||||
return existing ? existing.patch(data) : super.create(data, cache, { id: data.user.id });
|
||||
return existing ? existing.patch(data) : super.add(data, cache, { id: data.user.id });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ const MessageReaction = require('../structures/MessageReaction');
|
||||
|
||||
/**
|
||||
* Stores reactions.
|
||||
* @private
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class ReactionStore extends DataStore {
|
||||
@@ -12,8 +11,8 @@ class ReactionStore extends DataStore {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
create(data, cache) {
|
||||
return super.create(data, cache, { id: data.emoji.id || data.emoji.name, extras: [this.message] });
|
||||
add(data, cache) {
|
||||
return super.add(data, cache, { id: data.emoji.id || data.emoji.name, extras: [this.message] });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +39,15 @@ class ReactionStore extends DataStore {
|
||||
* @param {MessageReactionResolvable} role The role resolvable to resolve
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Removes all reactions from a message.
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
removeAll() {
|
||||
return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete()
|
||||
.then(() => this.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReactionStore;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const DataStore = require('./DataStore');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
* A data store to store User models who reacted to a MessageReaction.
|
||||
* @extends {DataStore}
|
||||
@@ -23,11 +25,33 @@ class ReactionUserStore extends DataStore {
|
||||
.reactions[this.reaction.emoji.identifier]
|
||||
.get({ query: { limit, before, after } });
|
||||
for (const rawUser of users) {
|
||||
const user = this.client.users.create(rawUser);
|
||||
const user = this.client.users.add(rawUser);
|
||||
this.set(user.id, user);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a user from this reaction.
|
||||
* @param {UserResolvable} [user=this.reaction.message.client.user] The user to remove the reaction of
|
||||
* @returns {Promise<MessageReaction>}
|
||||
*/
|
||||
remove(user = this.reaction.message.client.user) {
|
||||
const message = this.reaction.message;
|
||||
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(() =>
|
||||
message.client.actions.MessageReactionRemove.handle({
|
||||
user_id: userID,
|
||||
message_id: message.id,
|
||||
emoji: this.reaction.emoji,
|
||||
channel_id: message.channel.id,
|
||||
}).reaction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReactionUserStore;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const DataStore = require('./DataStore');
|
||||
const Role = require('../structures/Role');
|
||||
const { resolveColor } = require('../util/Util');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* Stores roles.
|
||||
* @private
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class RoleStore extends DataStore {
|
||||
@@ -12,8 +13,46 @@ class RoleStore extends DataStore {
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
create(data, cache) {
|
||||
return super.create(data, cache, { extras: [this.guild] });
|
||||
add(data, cache) {
|
||||
return super.add(data, cache, { extras: [this.guild] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new role in the guild with given information.
|
||||
* <warn>The position will silently reset to 1 if an invalid one is provided, or none.</warn>
|
||||
* @param {Object} [options] Options
|
||||
* @param {RoleData} [options.data] The data to update the role with
|
||||
* @param {string} [options.reason] Reason for creating this role
|
||||
* @returns {Promise<Role>}
|
||||
* @example
|
||||
* // Create a new role
|
||||
* guild.roles.create()
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Create a new role with data and a reason
|
||||
* guild.roles.create({
|
||||
* data: {
|
||||
* name: 'Super Cool People',
|
||||
* color: 'BLUE',
|
||||
* },
|
||||
* reason: 'we needed a role for Super Cool People',
|
||||
* })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
create({ data = {}, reason } = {}) {
|
||||
if (data.color) data.color = resolveColor(data.color);
|
||||
if (data.permissions) data.permissions = Permissions.resolve(data.permissions);
|
||||
|
||||
return this.guild.client.api.guilds(this.guild.id).roles.post({ data, reason }).then(r => {
|
||||
const { role } = this.client.actions.GuildRoleCreate.handle({
|
||||
guild_id: this.guild.id,
|
||||
role: r,
|
||||
});
|
||||
if (data.position) return role.setPosition(data.position, reason);
|
||||
return role;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,22 +63,22 @@ class RoleStore extends DataStore {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a RoleResolvable to a Role object.
|
||||
* @method resolve
|
||||
* @memberof RoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Role}
|
||||
*/
|
||||
* Resolves a RoleResolvable to a Role object.
|
||||
* @method resolve
|
||||
* @memberof RoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Role}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a RoleResolvable to a role ID string.
|
||||
* @method resolveID
|
||||
* @memberof RoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
* Resolves a RoleResolvable to a role ID string.
|
||||
* @method resolveID
|
||||
* @memberof RoleStore
|
||||
* @instance
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = RoleStore;
|
||||
|
||||
@@ -54,7 +54,7 @@ class UserStore extends DataStore {
|
||||
const existing = this.get(id);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
|
||||
return this.client.api.users(id).get().then(data => this.create(data, cache));
|
||||
return this.client.api.users(id).get().then(data => this.add(data, cache));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,26 @@ const GuildChannel = require('./GuildChannel');
|
||||
*/
|
||||
class CategoryChannel extends GuildChannel {
|
||||
/**
|
||||
* Channels that are part of this category
|
||||
* Channels that are a part of this category
|
||||
* @type {?Collection<Snowflake, GuildChannel>}
|
||||
* @readonly
|
||||
*/
|
||||
get children() {
|
||||
return this.guild.channels.filter(c => c.parentID === this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the category parent of this channel.
|
||||
* <warn>It is not currently possible to set the parent of a CategoryChannel.</warn>
|
||||
* @method setParent
|
||||
* @memberof CategoryChannel
|
||||
* @instance
|
||||
* @param {?GuildChannel|Snowflake} channel Parent channel
|
||||
* @param {Object} [options={}] Options to pass
|
||||
* @param {boolean} [options.lockPermissions=true] Lock the permissions to what the parent's permissions are
|
||||
* @param {string} [options.reason] Reason for modifying the parent of this channel
|
||||
* @returns {Promise<GuildChannel>}
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = CategoryChannel;
|
||||
|
||||
@@ -52,14 +52,25 @@ class Channel extends Base {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically returns the channel's mention instead of the Channel object.
|
||||
* @returns {string}
|
||||
* @example
|
||||
* // Logs: Hello from <#123456789012345678>!
|
||||
* console.log(`Hello from ${channel}!`);
|
||||
*/
|
||||
toString() {
|
||||
return `<#${this.id}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this channel.
|
||||
* @returns {Promise<Channel>}
|
||||
* @example
|
||||
* // Delete the channel
|
||||
* channel.delete()
|
||||
* .then() // Success
|
||||
* .catch(console.error); // Log error
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
delete() {
|
||||
return this.client.api.channels(this.id).delete().then(() => this);
|
||||
|
||||
@@ -97,7 +97,7 @@ class ClientApplication extends Base {
|
||||
* The owner of this OAuth application
|
||||
* @type {?User}
|
||||
*/
|
||||
this.owner = this.client.users.create(data.owner);
|
||||
this.owner = this.client.users.add(data.owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,13 +166,12 @@ class ClientApplication extends Base {
|
||||
* @param {string} type Type of the asset. `big`, or `small`
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createAsset(name, data, type) {
|
||||
return DataResolver.resolveBase64(data).then(b64 =>
|
||||
this.client.api.oauth2.applications(this.id).assets.post({ data: {
|
||||
name,
|
||||
data: b64,
|
||||
type: ClientApplicationAssetTypes[type.toUpperCase()],
|
||||
} }));
|
||||
async createAsset(name, data, type) {
|
||||
return this.client.api.oauth2.applications(this.id).assets.post({ data: {
|
||||
name,
|
||||
type: ClientApplicationAssetTypes[type.toUpperCase()],
|
||||
image: await DataResolver.resolveImage(data),
|
||||
} });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@ const Structures = require('../util/Structures');
|
||||
const Collection = require('../util/Collection');
|
||||
const ClientUserSettings = require('./ClientUserSettings');
|
||||
const ClientUserGuildSettings = require('./ClientUserGuildSettings');
|
||||
const { Events } = require('../util/Constants');
|
||||
const Util = require('../util/Util');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const Guild = require('./Guild');
|
||||
@@ -23,7 +22,8 @@ class ClientUser extends Structures.get('User') {
|
||||
|
||||
/**
|
||||
* The email of this account
|
||||
* @type {string}
|
||||
* <warn>This is only filled when using a user account.</warn>
|
||||
* @type {?string}
|
||||
*/
|
||||
this.email = data.email;
|
||||
this._typing = new Map();
|
||||
@@ -188,7 +188,9 @@ class ClientUser extends Structures.get('User') {
|
||||
* @typedef {Object} PresenceData
|
||||
* @property {PresenceStatus} [status] Status of the user
|
||||
* @property {boolean} [afk] Whether the user is AFK
|
||||
* @property {Object} [activity] activity the user is playing
|
||||
* @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] Stream url
|
||||
@@ -260,45 +262,7 @@ class ClientUser extends Structures.get('User') {
|
||||
Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options);
|
||||
|
||||
return this.client.api.users('@me').mentions.get({ query: options })
|
||||
.then(data => data.map(m => this.client.channels.get(m.channel_id).messages.create(m, false)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a guild.
|
||||
* <warn>This is only available when using a user account.</warn>
|
||||
* @param {string} name The name of the guild
|
||||
* @param {Object} [options] Options for the creating
|
||||
* @param {string} [options.region] The region for the server, defaults to the closest one available
|
||||
* @param {BufferResolvable|Base64Resolvable} [options.icon=null] The icon for the guild
|
||||
* @returns {Promise<Guild>} The guild that was created
|
||||
*/
|
||||
createGuild(name, { region, icon = null } = {}) {
|
||||
if (!icon || (typeof icon === 'string' && icon.startsWith('data:'))) {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.client.api.guilds.post({ data: { name, region, icon } })
|
||||
.then(data => {
|
||||
if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id));
|
||||
|
||||
const handleGuild = guild => {
|
||||
if (guild.id === data.id) {
|
||||
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
|
||||
this.client.clearTimeout(timeout);
|
||||
resolve(guild);
|
||||
}
|
||||
};
|
||||
this.client.on(Events.GUILD_CREATE, handleGuild);
|
||||
|
||||
const timeout = this.client.setTimeout(() => {
|
||||
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
|
||||
resolve(this.client.guilds.create(data));
|
||||
}, 10000);
|
||||
return undefined;
|
||||
}, reject)
|
||||
);
|
||||
}
|
||||
|
||||
return DataResolver.resolveImage(icon)
|
||||
.then(data => this.createGuild(name, { region, icon: data || null }));
|
||||
.then(data => data.map(m => this.client.channels.get(m.channel_id).messages.add(m, false)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,7 +290,7 @@ class ClientUser extends Structures.get('User') {
|
||||
}, {}),
|
||||
} : { recipients: recipients.map(u => this.client.users.resolveID(u.user || u.id)) };
|
||||
return this.client.api.users('@me').channels.post({ data })
|
||||
.then(res => this.client.channels.create(res));
|
||||
.then(res => this.client.channels.add(res));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class DMChannel extends Channel {
|
||||
* The recipient on the other end of the DM
|
||||
* @type {User}
|
||||
*/
|
||||
this.recipient = this.client.users.create(data.recipients[0]);
|
||||
this.recipient = this.client.users.add(data.recipients[0]);
|
||||
|
||||
this.lastMessageID = data.last_message_id;
|
||||
}
|
||||
|
||||
@@ -1,91 +1,29 @@
|
||||
const Collection = require('../util/Collection');
|
||||
const Snowflake = require('../util/Snowflake');
|
||||
const Base = require('./Base');
|
||||
const { TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents a custom emoji.
|
||||
* Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Emoji extends Base {
|
||||
constructor(client, data, guild) {
|
||||
constructor(client, emoji) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The guild this emoji is part of
|
||||
* @type {Guild}
|
||||
* Whether this emoji is animated
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The ID of the emoji
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
this.animated = emoji.animated;
|
||||
|
||||
/**
|
||||
* The name of the emoji
|
||||
* The name of this emoji
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
this.name = emoji.name;
|
||||
|
||||
/**
|
||||
* Whether or not this emoji requires colons surrounding it
|
||||
* @type {boolean}
|
||||
* The ID of this emoji
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.requiresColons = data.require_colons;
|
||||
|
||||
/**
|
||||
* Whether this emoji is managed by an external service
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.managed = data.managed;
|
||||
|
||||
this._roles = data.roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the emoji was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return Snowflake.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the emoji was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of roles this emoji is active for (empty if all), mapped by role ID
|
||||
* @type {Collection<Snowflake, Role>}
|
||||
* @readonly
|
||||
*/
|
||||
get roles() {
|
||||
const roles = new Collection();
|
||||
for (const role of this._roles) {
|
||||
if (this.guild.roles.has(role)) roles.set(role, this.guild.roles.get(role));
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to the emoji file
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
get url() {
|
||||
return this.client.rest.cdn.Emoji(this.id);
|
||||
this.id = emoji.id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,144 +32,34 @@ class Emoji extends Base {
|
||||
* @readonly
|
||||
*/
|
||||
get identifier() {
|
||||
if (this.id) return `${this.name}:${this.id}`;
|
||||
if (this.id) return `${this.animated ? 'a:' : ''}${this.name}:${this.id}`;
|
||||
return encodeURIComponent(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for editing an emoji.
|
||||
* @typedef {Object} EmojiEditData
|
||||
* @property {string} [name] The name of the emoji
|
||||
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] Roles to restrict emoji to
|
||||
* The URL to the emoji file if its a custom emoji
|
||||
* @type {?string}
|
||||
* @readonly
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits the emoji.
|
||||
* @param {EmojiEditData} data The new data for the emoji
|
||||
* @param {string} [reason] Reason for editing this emoji
|
||||
* @returns {Promise<Emoji>}
|
||||
* @example
|
||||
* // Edit an emoji
|
||||
* emoji.edit({name: 'newemoji'})
|
||||
* .then(e => console.log(`Edited emoji ${e}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(data, reason) {
|
||||
return this.client.api.guilds(this.guild.id).emojis(this.id)
|
||||
.patch({ data: {
|
||||
name: data.name,
|
||||
roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : undefined,
|
||||
}, reason })
|
||||
.then(() => this);
|
||||
get url() {
|
||||
if (!this.id) return null;
|
||||
return this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the emoji.
|
||||
* @param {string} name The new name for the emoji
|
||||
* @param {string} [reason] Reason for changing the emoji's name
|
||||
* @returns {Promise<Emoji>}
|
||||
*/
|
||||
setName(name, reason) {
|
||||
return this.edit({ name }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role to the list of roles that can use this emoji.
|
||||
* @param {Role} role The role to add
|
||||
* @returns {Promise<Emoji>}
|
||||
*/
|
||||
addRestrictedRole(role) {
|
||||
return this.addRestrictedRoles([role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple roles to the list of roles that can use this emoji.
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles Roles to add
|
||||
* @returns {Promise<Emoji>}
|
||||
*/
|
||||
addRestrictedRoles(roles) {
|
||||
const newRoles = new Collection(this.roles);
|
||||
for (let role of roles instanceof Collection ? roles.values() : roles) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (!role) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
}
|
||||
newRoles.set(role.id, role);
|
||||
}
|
||||
return this.edit({ roles: newRoles });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role from the list of roles that can use this emoji.
|
||||
* @param {Role} role The role to remove
|
||||
* @returns {Promise<Emoji>}
|
||||
*/
|
||||
removeRestrictedRole(role) {
|
||||
return this.removeRestrictedRoles([role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple roles from the list of roles that can use this emoji.
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles Roles to remove
|
||||
* @returns {Promise<Emoji>}
|
||||
*/
|
||||
removeRestrictedRoles(roles) {
|
||||
const newRoles = new Collection(this.roles);
|
||||
for (let role of roles instanceof Collection ? roles.values() : roles) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (!role) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
}
|
||||
if (newRoles.has(role.id)) newRoles.delete(role.id);
|
||||
}
|
||||
return this.edit({ roles: newRoles });
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically concatenates the emoji's mention instead of the Emoji object.
|
||||
* When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord
|
||||
* instead of the Emoji object.
|
||||
* @returns {string}
|
||||
* @example
|
||||
* // Send an emoji:
|
||||
* // Send a custom emoji from a guild:
|
||||
* const emoji = guild.emojis.first();
|
||||
* 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}`);
|
||||
*/
|
||||
toString() {
|
||||
return this.requiresColons ? `<:${this.name}:${this.id}>` : this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the emoji.
|
||||
* @param {string} [reason] Reason for deleting the emoji
|
||||
* @returns {Promise<Emoji>}
|
||||
*/
|
||||
delete(reason) {
|
||||
return this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason })
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this emoji is the same as another one.
|
||||
* @param {Emoji|Object} other The emoji to compare it to
|
||||
* @returns {boolean} Whether the emoji is equal to the given emoji or not
|
||||
*/
|
||||
equals(other) {
|
||||
if (other instanceof Emoji) {
|
||||
return (
|
||||
other.id === this.id &&
|
||||
other.name === this.name &&
|
||||
other.managed === this.managed &&
|
||||
other.requiresColons === this.requiresColons &&
|
||||
other._roles === this._roles
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
other.id === this.id &&
|
||||
other.name === this.name &&
|
||||
other._roles === this._roles
|
||||
);
|
||||
}
|
||||
return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class GroupDMChannel extends Channel {
|
||||
|
||||
if (data.recipients) {
|
||||
for (const recipient of data.recipients) {
|
||||
const user = this.client.users.create(recipient);
|
||||
const user = this.client.users.add(recipient);
|
||||
this.recipients.set(user.id, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
const Invite = require('./Invite');
|
||||
const GuildAuditLogs = require('./GuildAuditLogs');
|
||||
const Webhook = require('./Webhook');
|
||||
const GuildMember = require('./GuildMember');
|
||||
const VoiceRegion = require('./VoiceRegion');
|
||||
const { ChannelTypes, Events, browser } = require('../util/Constants');
|
||||
const Collection = require('../util/Collection');
|
||||
const Util = require('../util/Util');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const Snowflake = require('../util/Snowflake');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const Shared = require('./shared');
|
||||
const GuildMemberStore = require('../stores/GuildMemberStore');
|
||||
const RoleStore = require('../stores/RoleStore');
|
||||
const EmojiStore = require('../stores/EmojiStore');
|
||||
const GuildEmojiStore = require('../stores/GuildEmojiStore');
|
||||
const GuildChannelStore = require('../stores/GuildChannelStore');
|
||||
const PresenceStore = require('../stores/PresenceStore');
|
||||
const Base = require('./Base');
|
||||
@@ -42,7 +40,7 @@ class Guild extends Base {
|
||||
|
||||
/**
|
||||
* A collection of roles that are in this guild. The key is the role's ID, the value is the role
|
||||
* @type {Collection<Snowflake, Role>}
|
||||
* @type {RoleStore<Snowflake, Role>}
|
||||
*/
|
||||
this.roles = new RoleStore(this);
|
||||
|
||||
@@ -181,9 +179,21 @@ class Guild extends Base {
|
||||
this.available = !data.unavailable;
|
||||
this.features = data.features || this.features || [];
|
||||
|
||||
if (data.channels) {
|
||||
this.channels.clear();
|
||||
for (const rawChannel of data.channels) {
|
||||
this.client.channels.add(rawChannel, this);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.roles) {
|
||||
this.roles.clear();
|
||||
for (const role of data.roles) this.roles.add(role);
|
||||
}
|
||||
|
||||
if (data.members) {
|
||||
this.members.clear();
|
||||
for (const guildUser of data.members) this.members.create(guildUser);
|
||||
for (const guildUser of data.members) this.members.add(guildUser);
|
||||
}
|
||||
|
||||
if (data.owner_id) {
|
||||
@@ -194,21 +204,9 @@ class Guild extends Base {
|
||||
this.ownerID = data.owner_id;
|
||||
}
|
||||
|
||||
if (data.channels) {
|
||||
this.channels.clear();
|
||||
for (const rawChannel of data.channels) {
|
||||
this.client.channels.create(rawChannel, this);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.roles) {
|
||||
this.roles.clear();
|
||||
for (const role of data.roles) this.roles.create(role);
|
||||
}
|
||||
|
||||
if (data.presences) {
|
||||
for (const presence of data.presences) {
|
||||
this.presences.create(presence);
|
||||
this.presences.add(presence);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,10 +218,10 @@ class Guild extends Base {
|
||||
if (!this.emojis) {
|
||||
/**
|
||||
* A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji.
|
||||
* @type {EmojiStore<Snowflake, Emoji>}
|
||||
* @type {GuildEmojiStore<Snowflake, GuildEmoji>}
|
||||
*/
|
||||
this.emojis = new EmojiStore(this);
|
||||
if (data.emojis) for (const emoji of data.emojis) this.emojis.create(emoji);
|
||||
this.emojis = new GuildEmojiStore(this);
|
||||
if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji);
|
||||
} else {
|
||||
this.client.actions.GuildEmojisUpdate.handle({
|
||||
guild_id: this.id,
|
||||
@@ -457,7 +455,7 @@ class Guild extends Base {
|
||||
bans.reduce((collection, ban) => {
|
||||
collection.set(ban.user.id, {
|
||||
reason: ban.reason,
|
||||
user: this.client.users.create(ban.user),
|
||||
user: this.client.users.add(ban.user),
|
||||
});
|
||||
return collection;
|
||||
}, new Collection())
|
||||
@@ -514,6 +512,11 @@ class Guild extends Base {
|
||||
* @param {UserResolvable} [options.user] Only show entries involving this user
|
||||
* @param {AuditLogAction|number} [options.type] Only show entries involving this action type
|
||||
* @returns {Promise<GuildAuditLogs>}
|
||||
* @example
|
||||
* // Output audit log entries
|
||||
* guild.fetchAuditLogs()
|
||||
* .then(audit => console.log(audit.entries))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
fetchAuditLogs(options = {}) {
|
||||
if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id;
|
||||
@@ -560,7 +563,7 @@ class Guild extends Base {
|
||||
}
|
||||
}
|
||||
return this.client.api.guilds(this.id).members(user).put({ data: options })
|
||||
.then(data => this.members.create(data));
|
||||
.then(data => this.members.add(data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -818,79 +821,6 @@ class Guild extends Base {
|
||||
else return settings.addRestrictedGuild(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
* be resolved, the user ID will be the result.
|
||||
* @example
|
||||
* // Ban a user by ID (or with a user/guild member object)
|
||||
* guild.ban('some user ID')
|
||||
* .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
ban(user, options = { days: 0 }) {
|
||||
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.id).bans[id].put({ query: options })
|
||||
.then(() => {
|
||||
if (user instanceof GuildMember) return user;
|
||||
const _user = this.client.users.resolve(id);
|
||||
if (_user) {
|
||||
const member = this.members.resolve(_user);
|
||||
return member || _user;
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbans a user from the guild.
|
||||
* @param {UserResolvable} user The user to unban
|
||||
* @param {string} [reason] Reason for unbanning user
|
||||
* @returns {Promise<User>}
|
||||
* @example
|
||||
* // Unban a user by ID (or with a user/guild member object)
|
||||
* guild.unban('some user ID')
|
||||
* .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
unban(user, reason) {
|
||||
const id = this.client.users.resolveID(user);
|
||||
if (!id) throw new Error('BAN_RESOLVE_ID');
|
||||
return this.client.api.guilds(this.id).bans[id].delete({ reason })
|
||||
.then(() => user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes members from the guild based on how long they have been inactive.
|
||||
* @param {Object} [options] Prune options
|
||||
* @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 {string} [options.reason] Reason for this prune
|
||||
* @returns {Promise<number>} The number of members that were/will be kicked
|
||||
* @example
|
||||
* // See how many members will be pruned
|
||||
* guild.pruneMembers({ dry: true })
|
||||
* .then(pruned => console.log(`This will prune ${pruned} people!`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Actually prune the members
|
||||
* guild.pruneMembers({ days: 1, reason: 'too many people!' })
|
||||
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
pruneMembers({ days = 7, dry = false, reason } = {}) {
|
||||
if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
|
||||
return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason })
|
||||
.then(data => data.pruned);
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs this guild (already done automatically every 30 seconds).
|
||||
* <warn>This is only available when using a user account.</warn>
|
||||
@@ -899,73 +829,6 @@ class Guild extends Base {
|
||||
if (!this.client.user.bot) this.client.syncGuilds([this]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to overwrite permissions when creating a channel.
|
||||
* @typedef {Object} ChannelCreationOverwrites
|
||||
* @property {PermissionResolvable[]|number} [allow] The permissions to allow
|
||||
* @property {PermissionResolvable[]|number} [deny] The permissions to deny
|
||||
* @property {RoleResolvable|UserResolvable} id ID of the role or member this overwrite is for
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new channel in the guild.
|
||||
* @param {string} name The name of the new channel
|
||||
* @param {string} type The type of the new channel, either `text`, `voice`, or `category`
|
||||
* @param {Object} [options] Options
|
||||
* @param {boolean} [options.nsfw] Whether the new channel is nsfw
|
||||
* @param {number} [options.bitrate] Bitrate of the new channel in bits (only voice)
|
||||
* @param {number} [options.userLimit] Maximum amount of users allowed in the new channel (only voice)
|
||||
* @param {ChannelResolvable} [options.parent] Parent of the new channel
|
||||
* @param {Array<PermissionOverwrites|ChannelCreationOverwrites>} [options.overwrites] Permission overwrites
|
||||
* @param {string} [options.reason] Reason for creating the channel
|
||||
* @returns {Promise<GuildChannel>}
|
||||
* @example
|
||||
* // Create a new text channel
|
||||
* guild.createChannel('new-general', 'text')
|
||||
* .then(channel => console.log(`Created new channel ${channel}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createChannel(name, type, { nsfw, bitrate, userLimit, parent, overwrites, reason } = {}) {
|
||||
if (overwrites instanceof Collection || overwrites instanceof Array) {
|
||||
overwrites = overwrites.map(overwrite => {
|
||||
let allow = overwrite.allow || (overwrite.allowed ? overwrite.allowed.bitfield : 0);
|
||||
let deny = overwrite.deny || (overwrite.denied ? overwrite.denied.bitfield : 0);
|
||||
if (allow instanceof Array) allow = Permissions.resolve(allow);
|
||||
if (deny instanceof Array) deny = Permissions.resolve(deny);
|
||||
|
||||
const role = this.roles.resolve(overwrite.id);
|
||||
if (role) {
|
||||
overwrite.id = role.id;
|
||||
overwrite.type = 'role';
|
||||
} else {
|
||||
overwrite.id = this.client.users.resolveID(overwrite.id);
|
||||
overwrite.type = 'member';
|
||||
}
|
||||
|
||||
return {
|
||||
allow,
|
||||
deny,
|
||||
type: overwrite.type,
|
||||
id: overwrite.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (parent) parent = this.client.channels.resolveID(parent);
|
||||
return this.client.api.guilds(this.id).channels.post({
|
||||
data: {
|
||||
name,
|
||||
type: ChannelTypes[type.toUpperCase()],
|
||||
nsfw,
|
||||
bitrate,
|
||||
user_limit: userLimit,
|
||||
parent_id: parent,
|
||||
permission_overwrites: overwrites,
|
||||
},
|
||||
reason,
|
||||
}).then(data => this.client.actions.ChannelCreate.handle(data).channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* The data needed for updating a channel's position.
|
||||
* @typedef {Object} ChannelPosition
|
||||
@@ -996,85 +859,6 @@ class Guild extends Base {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new role in the guild with given information.
|
||||
* <warn>The position will silently reset to 1 if an invalid one is provided, or none.</warn>
|
||||
* @param {Object} [options] Options
|
||||
* @param {RoleData} [options.data] The data to update the role with
|
||||
* @param {string} [options.reason] Reason for creating this role
|
||||
* @returns {Promise<Role>}
|
||||
* @example
|
||||
* // Create a new role
|
||||
* guild.createRole()
|
||||
* .then(role => console.log(`Created role ${role}`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Create a new role with data and a reason
|
||||
* guild.createRole({
|
||||
* data: {
|
||||
* name: 'Super Cool People',
|
||||
* color: 'BLUE',
|
||||
* },
|
||||
* reason: 'we needed a role for Super Cool People',
|
||||
* })
|
||||
* .then(role => console.log(`Created role ${role}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createRole({ data = {}, reason } = {}) {
|
||||
if (data.color) data.color = Util.resolveColor(data.color);
|
||||
if (data.permissions) data.permissions = Permissions.resolve(data.permissions);
|
||||
|
||||
return this.client.api.guilds(this.id).roles.post({ data, reason }).then(r => {
|
||||
const { role } = this.client.actions.GuildRoleCreate.handle({
|
||||
guild_id: this.id,
|
||||
role: r,
|
||||
});
|
||||
if (data.position) return role.setPosition(data.position, reason);
|
||||
return role;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new custom emoji in the guild.
|
||||
* @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji
|
||||
* @param {string} name The name for the emoji
|
||||
* @param {Object} [options] Options
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} [options.roles] Roles to limit the emoji to
|
||||
* @param {string} [options.reason] Reason for creating the emoji
|
||||
* @returns {Promise<Emoji>} The created emoji
|
||||
* @example
|
||||
* // Create a new emoji from a url
|
||||
* guild.createEmoji('https://i.imgur.com/w3duR07.png', 'rip')
|
||||
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Create a new emoji from a file on your computer
|
||||
* guild.createEmoji('./memes/banana.png', 'banana')
|
||||
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createEmoji(attachment, name, { roles, reason } = {}) {
|
||||
if (typeof attachment === 'string' && attachment.startsWith('data:')) {
|
||||
const data = { image: attachment, name };
|
||||
if (roles) {
|
||||
data.roles = [];
|
||||
for (let role of roles instanceof Collection ? roles.values() : roles) {
|
||||
role = this.roles.resolve(role);
|
||||
if (!role) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
}
|
||||
data.roles.push(role.id);
|
||||
}
|
||||
}
|
||||
|
||||
return this.client.api.guilds(this.id).emojis.post({ data, reason })
|
||||
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji);
|
||||
}
|
||||
|
||||
return DataResolver.resolveImage(attachment).then(image => this.createEmoji(image, name, { roles, reason }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves the guild.
|
||||
* @returns {Promise<Guild>}
|
||||
|
||||
@@ -106,7 +106,7 @@ const Actions = {
|
||||
*/
|
||||
class GuildAuditLogs {
|
||||
constructor(guild, data) {
|
||||
if (data.users) for (const user of data.users) guild.client.users.create(user);
|
||||
if (data.users) for (const user of data.users) guild.client.users.add(user);
|
||||
/**
|
||||
* Cached webhooks
|
||||
* @type {Collection<Snowflake, Webhook>}
|
||||
@@ -148,7 +148,7 @@ class GuildAuditLogs {
|
||||
* * An invite
|
||||
* * A webhook
|
||||
* * An object where the keys represent either the new value or the old value
|
||||
* @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook} AuditLogEntryTarget
|
||||
* @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook} AuditLogEntryTarget
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ const { MessageNotificationTypes } = require('../util/Constants');
|
||||
const { Error, TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents a guild channel (e.g. text channels and voice channels).
|
||||
* Represents a guild channel (i.g. a {@link TextChannel}, {@link VoiceChannel} or {@link CategoryChannel}).
|
||||
* @extends {Channel}
|
||||
*/
|
||||
class GuildChannel extends Channel {
|
||||
@@ -92,31 +92,16 @@ class GuildChannel extends Channel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the overall set of permissions for a user in this channel, taking into account roles and permission
|
||||
* overwrites.
|
||||
* @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for
|
||||
* Gets the overall set of permissions for a member or role in this channel, taking into account channel overwrites.
|
||||
* @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for
|
||||
* @returns {?Permissions}
|
||||
*/
|
||||
permissionsFor(member) {
|
||||
member = this.guild.members.resolve(member);
|
||||
if (!member) return null;
|
||||
if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze();
|
||||
|
||||
const roles = member.roles;
|
||||
const permissions = new Permissions(roles.map(role => role.permissions));
|
||||
|
||||
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze();
|
||||
|
||||
const overwrites = this.overwritesFor(member, true, roles);
|
||||
|
||||
return permissions
|
||||
.remove(overwrites.everyone ? overwrites.everyone.denied : 0)
|
||||
.add(overwrites.everyone ? overwrites.everyone.allowed : 0)
|
||||
.remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.denied) : 0)
|
||||
.add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allowed) : 0)
|
||||
.remove(overwrites.member ? overwrites.member.denied : 0)
|
||||
.add(overwrites.member ? overwrites.member.allowed : 0)
|
||||
.freeze();
|
||||
permissionsFor(memberOrRole) {
|
||||
const member = this.guild.members.resolve(memberOrRole);
|
||||
if (member) return this.memberPermissions(member);
|
||||
const role = this.guild.roles.resolve(memberOrRole);
|
||||
if (role) return this.rolePermissions(role);
|
||||
return null;
|
||||
}
|
||||
|
||||
overwritesFor(member, verified = false, roles = null) {
|
||||
@@ -145,6 +130,52 @@ class GuildChannel extends Channel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the overall set of permissions for a member in this channel, taking into account channel overwrites.
|
||||
* @param {GuildMember} member The member to obtain the overall permissions for
|
||||
* @returns {Permissions}
|
||||
* @private
|
||||
*/
|
||||
memberPermissions(member) {
|
||||
if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze();
|
||||
|
||||
const roles = member.roles;
|
||||
const permissions = new Permissions(roles.map(role => role.permissions));
|
||||
|
||||
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze();
|
||||
|
||||
const overwrites = this.overwritesFor(member, true, roles);
|
||||
|
||||
return permissions
|
||||
.remove(overwrites.everyone ? overwrites.everyone.denied : 0)
|
||||
.add(overwrites.everyone ? overwrites.everyone.allowed : 0)
|
||||
.remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.denied) : 0)
|
||||
.add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allowed) : 0)
|
||||
.remove(overwrites.member ? overwrites.member.denied : 0)
|
||||
.add(overwrites.member ? overwrites.member.allowed : 0)
|
||||
.freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the overall set of permissions for a role in this channel, taking into account channel overwrites.
|
||||
* @param {Role} role The role to obtain the overall permissions for
|
||||
* @returns {Permissions}
|
||||
* @private
|
||||
*/
|
||||
rolePermissions(role) {
|
||||
if (role.permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze();
|
||||
|
||||
const everyoneOverwrites = this.permissionOverwrites.get(this.guild.id);
|
||||
const roleOverwrites = this.permissionOverwrites.get(role.id);
|
||||
|
||||
return role.permissions
|
||||
.remove(everyoneOverwrites ? everyoneOverwrites.denied : 0)
|
||||
.add(everyoneOverwrites ? everyoneOverwrites.allowed : 0)
|
||||
.remove(roleOverwrites ? roleOverwrites.denied : 0)
|
||||
.add(roleOverwrites ? roleOverwrites.allowed : 0)
|
||||
.freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* An object mapping permission flags to `true` (enabled), `null` (default) or `false` (disabled).
|
||||
* ```js
|
||||
@@ -272,8 +303,8 @@ class GuildChannel extends Channel {
|
||||
* @returns {Promise<GuildChannel>}
|
||||
* @example
|
||||
* // Edit a channel
|
||||
* channel.edit({name: 'new-channel'})
|
||||
* .then(c => console.log(`Edited channel ${c}`))
|
||||
* channel.edit({ name: 'new-channel' })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async edit(data, reason) {
|
||||
@@ -292,7 +323,7 @@ class GuildChannel extends Channel {
|
||||
name: (data.name || this.name).trim(),
|
||||
topic: data.topic,
|
||||
nsfw: data.nsfw,
|
||||
bitrate: data.bitrate || (this.bitrate ? this.bitrate * 1000 : undefined),
|
||||
bitrate: data.bitrate || this.bitrate,
|
||||
user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit,
|
||||
parent_id: data.parentID,
|
||||
lock_permissions: data.lockPermissions,
|
||||
@@ -323,14 +354,15 @@ class GuildChannel extends Channel {
|
||||
|
||||
/**
|
||||
* Sets the category parent of this channel.
|
||||
* @param {GuildChannel|Snowflake} channel Parent channel
|
||||
* @param {boolean} [options.lockPermissions] Lock the permissions to what the parent's permissions are
|
||||
* @param {?GuildChannel|Snowflake} channel Parent channel
|
||||
* @param {Object} [options={}] Options to pass
|
||||
* @param {boolean} [options.lockPermissions=true] Lock the permissions to what the parent's permissions are
|
||||
* @param {string} [options.reason] Reason for modifying the parent of this channel
|
||||
* @returns {Promise<GuildChannel>}
|
||||
*/
|
||||
setParent(channel, { lockPermissions = true, reason } = {}) {
|
||||
return this.edit({
|
||||
parentID: channel.id ? channel.id : channel,
|
||||
parentID: channel !== null ? channel.id ? channel.id : channel : null,
|
||||
lockPermissions,
|
||||
}, reason);
|
||||
}
|
||||
@@ -385,6 +417,11 @@ class GuildChannel extends Channel {
|
||||
* @param {boolean} [options.unique=false] Create a unique invite, or use an existing one with similar settings
|
||||
* @param {string} [options.reason] Reason for creating this
|
||||
* @returns {Promise<Invite>}
|
||||
* @example
|
||||
* // Create an invite to a channel
|
||||
* channel.createInvite()
|
||||
* .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createInvite({ temporary = false, maxAge = 86400, maxUses = 0, unique, reason } = {}) {
|
||||
return this.client.api.channels(this.id).invites.post({ data: {
|
||||
@@ -393,6 +430,21 @@ class GuildChannel extends Channel {
|
||||
.then(invite => new Invite(this.client, invite));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a collection of invites to this guild channel.
|
||||
* Resolves with a collection mapping invites by their codes.
|
||||
* @returns {Promise<Collection<string, Invite>>}
|
||||
*/
|
||||
async fetchInvites() {
|
||||
const inviteItems = await this.client.api.channels(this.id).invites.get();
|
||||
const invites = new Collection();
|
||||
for (const inviteItem of inviteItems) {
|
||||
const invite = new Invite(this.client, inviteItem);
|
||||
invites.set(invite.code, invite);
|
||||
}
|
||||
return invites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones this channel.
|
||||
* @param {Object} [options] The options
|
||||
@@ -401,13 +453,28 @@ class GuildChannel extends Channel {
|
||||
* @param {boolean} [options.withPermissions=true] Whether to clone the channel with this channel's
|
||||
* permission overwrites
|
||||
* @param {boolean} [options.withTopic=true] Whether to clone the channel with this channel's topic
|
||||
* @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 {ChannelResolvable} [options.parent=this.parent] The parent of the new channel
|
||||
* @param {string} [options.reason] Reason for cloning this channel
|
||||
* @returns {Promise<GuildChannel>}
|
||||
*/
|
||||
clone({ name = this.name, withPermissions = true, withTopic = true, reason } = {}) {
|
||||
const options = { overwrites: withPermissions ? this.permissionOverwrites : [], reason };
|
||||
return this.guild.createChannel(name, this.type, options)
|
||||
.then(channel => withTopic ? channel.setTopic(this.topic) : channel);
|
||||
clone(options = {}) {
|
||||
if (typeof options.withPermissions === 'undefined') options.withPermissions = true;
|
||||
Util.mergeDefault({
|
||||
name: this.name,
|
||||
overwrites: options.withPermissions ? this.permissionOverwrites : [],
|
||||
withTopic: true,
|
||||
nsfw: this.nsfw,
|
||||
parent: this.parent,
|
||||
bitrate: this.bitrate,
|
||||
userLimit: this.userLimit,
|
||||
reason: null,
|
||||
}, options);
|
||||
options.type = this.type;
|
||||
return this.guild.channels.create(options.name, options)
|
||||
.then(channel => options.withTopic ? channel.setTopic(this.topic) : channel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -488,17 +555,6 @@ class GuildChannel extends Channel {
|
||||
return MessageNotificationTypes[3];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically returns the channel's mention instead of the Channel object.
|
||||
* @returns {string}
|
||||
* @example
|
||||
* // Logs: Hello from <#123456789012345678>!
|
||||
* console.log(`Hello from ${channel}!`);
|
||||
*/
|
||||
toString() {
|
||||
return `<#${this.id}>`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildChannel;
|
||||
|
||||
144
src/structures/GuildEmoji.js
Normal file
144
src/structures/GuildEmoji.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore');
|
||||
const Snowflake = require('../util/Snowflake');
|
||||
const Emoji = require('./Emoji');
|
||||
|
||||
/**
|
||||
* Represents a custom emoji.
|
||||
* @extends {Emoji}
|
||||
*/
|
||||
class GuildEmoji extends Emoji {
|
||||
constructor(client, data, guild) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* The guild this emoji is part of
|
||||
* @type {Guild}
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
/**
|
||||
* A collection of roles this emoji is active for (empty if all), mapped by role ID
|
||||
* @type {GuildEmojiRoleStore<Snowflake, Role>}
|
||||
*/
|
||||
this.roles = new GuildEmojiRoleStore(this);
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
this.name = data.name;
|
||||
|
||||
/**
|
||||
* Whether or not this emoji requires colons surrounding it
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.requiresColons = data.require_colons;
|
||||
|
||||
/**
|
||||
* Whether this emoji is managed by an external service
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.managed = data.managed;
|
||||
|
||||
if (data.roles) this.roles._patch(data.roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the emoji was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return Snowflake.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the emoji was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the author for this emoji
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
fetchAuthor() {
|
||||
return this.client.api.guilds(this.guild.id).emojis(this.id).get()
|
||||
.then(emoji => this.client.users.add(emoji.user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for editing an emoji.
|
||||
* @typedef {Object} GuildEmojiEditData
|
||||
* @property {string} [name] The name of the emoji
|
||||
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] Roles to restrict emoji to
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits the emoji.
|
||||
* @param {Guild} data The new data for the emoji
|
||||
* @param {string} [reason] Reason for editing this emoji
|
||||
* @returns {Promise<GuildEmoji>}
|
||||
* @example
|
||||
* // Edit an emoji
|
||||
* emoji.edit({name: 'newemoji'})
|
||||
* .then(e => console.log(`Edited emoji ${e}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(data, reason) {
|
||||
return this.client.api.guilds(this.guild.id).emojis(this.id)
|
||||
.patch({ data: {
|
||||
name: data.name,
|
||||
roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : undefined,
|
||||
}, reason })
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the emoji.
|
||||
* @param {string} name The new name for the emoji
|
||||
* @param {string} [reason] Reason for changing the emoji's name
|
||||
* @returns {Promise<GuildEmoji>}
|
||||
*/
|
||||
setName(name, reason) {
|
||||
return this.edit({ name }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the emoji.
|
||||
* @param {string} [reason] Reason for deleting the emoji
|
||||
* @returns {Promise<GuildEmoji>}
|
||||
*/
|
||||
delete(reason) {
|
||||
return this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason })
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this emoji is the same as another one.
|
||||
* @param {GuildEmoji|Object} other The emoji to compare it to
|
||||
* @returns {boolean} Whether the emoji is equal to the given emoji or not
|
||||
*/
|
||||
equals(other) {
|
||||
if (other instanceof GuildEmoji) {
|
||||
return (
|
||||
other.id === this.id &&
|
||||
other.name === this.name &&
|
||||
other.managed === this.managed &&
|
||||
other.requiresColons === this.requiresColons &&
|
||||
other.roles.every(role => this.roles.has(role.id))
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
other.id === this.id &&
|
||||
other.name === this.name &&
|
||||
other.roles.every(role => this.roles.has(role))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildEmoji;
|
||||
@@ -1,10 +1,10 @@
|
||||
const TextBasedChannel = require('./interfaces/TextBasedChannel');
|
||||
const Role = require('./Role');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const Collection = require('../util/Collection');
|
||||
const GuildMemberRoleStore = require('../stores/GuildMemberRoleStore');
|
||||
const Base = require('./Base');
|
||||
const { Presence } = require('./Presence');
|
||||
const { Error, TypeError } = require('../errors');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents a member of a guild on Discord.
|
||||
@@ -22,12 +22,17 @@ class GuildMember extends Base {
|
||||
this.guild = guild;
|
||||
|
||||
/**
|
||||
* The user that this guild member instance Represents
|
||||
* The user that this guild member instance represents
|
||||
* @type {User}
|
||||
*/
|
||||
this.user = {};
|
||||
|
||||
this._roles = [];
|
||||
/**
|
||||
* A list of roles that are applied to this GuildMember, mapped by the role ID
|
||||
* @type {GuildMemberRoleStore<Snowflake, Role>}
|
||||
*/
|
||||
|
||||
this.roles = new GuildMemberRoleStore(this);
|
||||
|
||||
if (data) this._patch(data);
|
||||
|
||||
@@ -66,8 +71,14 @@ class GuildMember extends Base {
|
||||
*/
|
||||
if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime();
|
||||
|
||||
this.user = this.guild.client.users.create(data.user);
|
||||
if (data.roles) this._roles = data.roles;
|
||||
this.user = this.guild.client.users.add(data.user);
|
||||
if (data.roles) this.roles._patch(data.roles);
|
||||
}
|
||||
|
||||
_clone() {
|
||||
const clone = super._clone();
|
||||
clone.roles = this.roles.clone();
|
||||
return clone;
|
||||
}
|
||||
|
||||
get voiceState() {
|
||||
@@ -134,52 +145,13 @@ class GuildMember extends Base {
|
||||
return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(this.client);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of roles that are applied to this GuildMember, mapped by the role ID
|
||||
* @type {Collection<Snowflake, Role>}
|
||||
* @readonly
|
||||
*/
|
||||
get roles() {
|
||||
const list = new Collection();
|
||||
const everyoneRole = this.guild.roles.get(this.guild.id);
|
||||
|
||||
if (everyoneRole) list.set(everyoneRole.id, everyoneRole);
|
||||
|
||||
for (const roleID of this._roles) {
|
||||
const role = this.guild.roles.get(roleID);
|
||||
if (role) list.set(role.id, role);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the member with the highest position
|
||||
* @type {Role}
|
||||
* @readonly
|
||||
*/
|
||||
get highestRole() {
|
||||
return this.roles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the member used to set their color
|
||||
* @type {?Role}
|
||||
* @readonly
|
||||
*/
|
||||
get colorRole() {
|
||||
const coloredRoles = this.roles.filter(role => role.color);
|
||||
if (!coloredRoles.size) return null;
|
||||
return coloredRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||
}
|
||||
|
||||
/**
|
||||
* The displayed color of the member in base 10
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get displayColor() {
|
||||
const role = this.colorRole;
|
||||
const role = this.roles.color;
|
||||
return (role && role.color) || 0;
|
||||
}
|
||||
|
||||
@@ -189,21 +161,10 @@ class GuildMember extends Base {
|
||||
* @readonly
|
||||
*/
|
||||
get displayHexColor() {
|
||||
const role = this.colorRole;
|
||||
const role = this.roles.color;
|
||||
return (role && role.hexColor) || '#000000';
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the member used to hoist them in a separate category in the users list
|
||||
* @type {?Role}
|
||||
* @readonly
|
||||
*/
|
||||
get hoistRole() {
|
||||
const hoistedRoles = this.roles.filter(role => role.hoist);
|
||||
if (!hoistedRoles.size) return null;
|
||||
return hoistedRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this member is muted in any way
|
||||
* @type {boolean}
|
||||
@@ -259,17 +220,24 @@ class GuildMember extends Base {
|
||||
return new Permissions(this.roles.map(role => role.permissions)).freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the member is manageable in terms of role hierarchy by the client user
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get manageable() {
|
||||
if (this.user.id === this.guild.ownerID) return false;
|
||||
if (this.user.id === this.client.user.id) return false;
|
||||
return this.guild.me.roles.highest.comparePositionTo(this.roles.highest) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the member is kickable by the client user
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get kickable() {
|
||||
if (this.user.id === this.guild.ownerID) return false;
|
||||
if (this.user.id === this.client.user.id) return false;
|
||||
const clientMember = this.guild.member(this.client.user);
|
||||
if (!clientMember.permissions.has(Permissions.FLAGS.KICK_MEMBERS)) return false;
|
||||
return clientMember.highestRole.comparePositionTo(this.highestRole) > 0;
|
||||
return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,11 +246,7 @@ class GuildMember extends Base {
|
||||
* @readonly
|
||||
*/
|
||||
get bannable() {
|
||||
if (this.user.id === this.guild.ownerID) return false;
|
||||
if (this.user.id === this.client.user.id) return false;
|
||||
const clientMember = this.guild.member(this.client.user);
|
||||
if (!clientMember.permissions.has(Permissions.FLAGS.BAN_MEMBERS)) return false;
|
||||
return clientMember.highestRole.comparePositionTo(this.highestRole) > 0;
|
||||
return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.BAN_MEMBERS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,19 +256,20 @@ class GuildMember extends Base {
|
||||
* @returns {?Permissions}
|
||||
*/
|
||||
permissionsIn(channel) {
|
||||
channel = this.client.channels.resolve(channel);
|
||||
if (!channel || !channel.guild) throw new Error('GUILD_CHANNEL_RESOLVE');
|
||||
return channel.permissionsFor(this);
|
||||
channel = this.guild.channels.resolve(channel);
|
||||
if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
|
||||
return channel.memberPermissions(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any of the member's roles have a permission.
|
||||
* @param {PermissionResolvable|PermissionResolvable[]} permission Permission(s) to check for
|
||||
* @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override
|
||||
* @param {boolean} [checkOwner=true] Whether to allow being the guild's owner to override
|
||||
* @param {Object} [options] Options
|
||||
* @param {boolean} [options.checkAdmin=true] Whether to allow the administrator permission to override
|
||||
* @param {boolean} [options.checkOwner=true] Whether to allow being the guild's owner to override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasPermission(permission, checkAdmin = true, checkOwner = true) {
|
||||
hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) {
|
||||
if (checkOwner && this.user.id === this.guild.ownerID) return true;
|
||||
return this.roles.some(r => r.permissions.has(permission, checkAdmin));
|
||||
}
|
||||
@@ -353,7 +318,8 @@ class GuildMember extends Base {
|
||||
const clone = this._clone();
|
||||
data.user = this.user;
|
||||
clone._patch(data);
|
||||
clone._frozenVoiceState = this.voiceState;
|
||||
clone._frozenVoiceState = {};
|
||||
Object.assign(clone._frozenVoiceState, this.voiceState);
|
||||
if (typeof data.mute !== 'undefined') clone._frozenVoiceState.mute = data.mute;
|
||||
if (typeof data.deaf !== 'undefined') clone._frozenVoiceState.mute = data.deaf;
|
||||
if (typeof data.channel_id !== 'undefined') clone._frozenVoiceState.channel_id = data.channel_id;
|
||||
@@ -362,7 +328,7 @@ class GuildMember extends Base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute/unmutes a user.
|
||||
* Mutes/unmutes a user.
|
||||
* @param {boolean} mute Whether or not the member should be muted
|
||||
* @param {string} [reason] Reason for muting or unmuting
|
||||
* @returns {Promise<GuildMember>}
|
||||
@@ -372,7 +338,7 @@ class GuildMember extends Base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deafen/undeafens a user.
|
||||
* Deafens/undeafens a user.
|
||||
* @param {boolean} deaf Whether or not the member should be deafened
|
||||
* @param {string} [reason] Reason for deafening or undeafening
|
||||
* @returns {Promise<GuildMember>}
|
||||
@@ -390,94 +356,6 @@ class GuildMember extends Base {
|
||||
return this.edit({ channel });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the roles applied to the member.
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to apply
|
||||
* @param {string} [reason] Reason for applying the roles
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
setRoles(roles, reason) {
|
||||
return this.edit({ roles }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single role to the member.
|
||||
* @param {RoleResolvable} role The role or ID of the role to add
|
||||
* @param {string} [reason] Reason for adding the role
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
addRole(role, reason) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake'));
|
||||
if (this._roles.includes(role.id)) return Promise.resolve(this);
|
||||
return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id)
|
||||
.put({ reason })
|
||||
.then(() => {
|
||||
const clone = this._clone();
|
||||
if (!clone._roles.includes(role.id)) clone._roles.push(role.id);
|
||||
return clone;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple roles to the member.
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to add
|
||||
* @param {string} [reason] Reason for adding the roles
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
addRoles(roles, reason) {
|
||||
let allRoles = this._roles.slice();
|
||||
for (let role of roles instanceof Collection ? roles.values() : roles) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (!role) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
}
|
||||
allRoles.push(role.id);
|
||||
}
|
||||
return this.edit({ roles: allRoles }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single role from the member.
|
||||
* @param {RoleResolvable} role The role or ID of the role to remove
|
||||
* @param {string} [reason] Reason for removing the role
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
removeRole(role, reason) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake'));
|
||||
if (!this._roles.includes(role.id)) return Promise.resolve(this);
|
||||
return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id)
|
||||
.delete({ reason })
|
||||
.then(() => {
|
||||
const clone = this._clone();
|
||||
const index = clone._roles.indexOf(role.id);
|
||||
if (~index) clone._roles.splice(index, 1);
|
||||
return clone;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple roles from the member.
|
||||
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to remove
|
||||
* @param {string} [reason] Reason for removing the roles
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
removeRoles(roles, reason) {
|
||||
const allRoles = this._roles.slice();
|
||||
for (let role of roles instanceof Collection ? roles.values() : roles) {
|
||||
role = this.guild.roles.resolve(role);
|
||||
if (!role) {
|
||||
return Promise.reject(new TypeError('INVALID_TYPE', 'roles',
|
||||
'Array or Collection of Roles or Snowflakes', true));
|
||||
}
|
||||
const index = allRoles.indexOf(role.id);
|
||||
if (index >= 0) allRoles.splice(index, 1);
|
||||
}
|
||||
return this.edit({ roles: allRoles }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the nickname for the guild member.
|
||||
* @param {string} nick The nickname for the guild member
|
||||
@@ -511,12 +389,7 @@ class GuildMember extends Base {
|
||||
*/
|
||||
kick(reason) {
|
||||
return this.client.api.guilds(this.guild.id).members(this.user.id).delete({ reason })
|
||||
.then(() =>
|
||||
this.client.actions.GuildMemberRemove.handle({
|
||||
guild_id: this.guild.id,
|
||||
user: this.user,
|
||||
}).member
|
||||
);
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -527,10 +400,12 @@ class GuildMember extends Base {
|
||||
* @returns {Promise<GuildMember>}
|
||||
* @example
|
||||
* // ban a guild member
|
||||
* guildMember.ban(7);
|
||||
* guildMember.ban({ days: 7, reason: 'They deserved it' })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
ban(options) {
|
||||
return this.guild.ban(this, options);
|
||||
return this.guild.members.ban(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ class Invite extends Base {
|
||||
* The guild the invite is for
|
||||
* @type {Guild}
|
||||
*/
|
||||
this.guild = this.client.guilds.create(data.guild, false);
|
||||
this.guild = this.client.guilds.add(data.guild, false);
|
||||
|
||||
/**
|
||||
* The code for this invite
|
||||
@@ -78,14 +78,14 @@ class Invite extends Base {
|
||||
* The user who created this invite
|
||||
* @type {User}
|
||||
*/
|
||||
this.inviter = this.client.users.create(data.inviter);
|
||||
this.inviter = this.client.users.add(data.inviter);
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel the invite is for
|
||||
* @type {GuildChannel}
|
||||
*/
|
||||
this.channel = this.client.channels.create(data.channel, this.guild, false);
|
||||
this.channel = this.client.channels.add(data.channel, this.guild, false);
|
||||
|
||||
/**
|
||||
* The timestamp the invite was created at
|
||||
|
||||
@@ -52,14 +52,7 @@ class Message extends Base {
|
||||
* The author of the message
|
||||
* @type {User}
|
||||
*/
|
||||
this.author = this.client.users.create(data.author, !data.webhook_id);
|
||||
|
||||
/**
|
||||
* Represents the author of the message as a guild member.
|
||||
* Only available if the message comes from a guild where the author is still a member
|
||||
* @type {?GuildMember}
|
||||
*/
|
||||
this.member = this.guild ? this.guild.member(this.author) || null : null;
|
||||
this.author = this.client.users.add(data.author, !data.webhook_id);
|
||||
|
||||
/**
|
||||
* Whether or not this message is pinned
|
||||
@@ -121,7 +114,7 @@ class Message extends Base {
|
||||
this.reactions = new ReactionStore(this);
|
||||
if (data.reactions && data.reactions.length > 0) {
|
||||
for (const reaction of data.reactions) {
|
||||
this.reactions.create(reaction);
|
||||
this.reactions.add(reaction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +138,7 @@ class Message extends Base {
|
||||
|
||||
/**
|
||||
* Group activity
|
||||
* @type {?Object}
|
||||
* @type {?MessageActivity}
|
||||
*/
|
||||
this.activity = data.activity ? {
|
||||
partyID: data.activity.party_id,
|
||||
@@ -201,6 +194,16 @@ class Message extends Base {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the author of the message as a guild member.
|
||||
* Only available if the message comes from a guild where the author is still a member
|
||||
* @type {?GuildMember}
|
||||
* @readonly
|
||||
*/
|
||||
get member() {
|
||||
return this.guild ? this.guild.member(this.author) || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the message was sent at
|
||||
* @type {Date}
|
||||
@@ -273,10 +276,8 @@ class Message extends Base {
|
||||
* @returns {ReactionCollector}
|
||||
* @example
|
||||
* // Create a reaction collector
|
||||
* const collector = message.createReactionCollector(
|
||||
* (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID',
|
||||
* { time: 15000 }
|
||||
* );
|
||||
* const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID';
|
||||
* const collector = message.createReactionCollector(filter, { time: 15000 });
|
||||
* collector.on('collect', r => console.log(`Collected ${r.emoji.name}`));
|
||||
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
|
||||
*/
|
||||
@@ -296,6 +297,12 @@ class Message extends Base {
|
||||
* @param {CollectorFilter} filter The filter function to use
|
||||
* @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector
|
||||
* @returns {Promise<Collection<string, MessageReaction>>}
|
||||
* @example
|
||||
* // Create a reaction collector
|
||||
* const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID'
|
||||
* message.awaitReactions(filter, { time: 15000 })
|
||||
* .then(collected => console.log(`Collected ${collected.size} reactions`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
awaitReactions(filter, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -377,10 +384,10 @@ class Message extends Base {
|
||||
}
|
||||
if (!options.content) options.content = content;
|
||||
|
||||
const { data, files } = await createMessage(this, options);
|
||||
const { data } = await createMessage(this, options);
|
||||
|
||||
return this.client.api.channels[this.channel.id].messages[this.id]
|
||||
.patch({ data, files })
|
||||
.patch({ data })
|
||||
.then(d => {
|
||||
const clone = this._clone();
|
||||
clone._patch(d);
|
||||
@@ -425,15 +432,6 @@ class Message extends Base {
|
||||
}).reaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all reactions from a message.
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
clearReactions() {
|
||||
return this.client.api.channels(this.channel.id).messages(this.id).reactions.delete()
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the message.
|
||||
* @param {Object} [options] Options
|
||||
|
||||
@@ -51,24 +51,31 @@ class MessageCollector extends Collector {
|
||||
/**
|
||||
* Handles a message for possible collection.
|
||||
* @param {Message} message The message that could be collected
|
||||
* @returns {?{key: Snowflake, value: Message}}
|
||||
* @returns {?Snowflake}
|
||||
* @private
|
||||
*/
|
||||
collect(message) {
|
||||
/**
|
||||
* Emitted whenever a message is collected.
|
||||
* @event MessageCollector#collect
|
||||
* @param {Message} message The message that was collected
|
||||
*/
|
||||
if (message.channel.id !== this.channel.id) return null;
|
||||
this.received++;
|
||||
return {
|
||||
key: message.id,
|
||||
value: message,
|
||||
};
|
||||
return message.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a message for possible disposal.
|
||||
* @param {Message} message The message that could be disposed
|
||||
* @returns {?string}
|
||||
* @param {Message} message The message that could be disposed of
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
dispose(message) {
|
||||
/**
|
||||
* Emitted whenever a message is disposed of.
|
||||
* @event MessageCollector#dispose
|
||||
* @param {Message} message The message that was disposed of
|
||||
*/
|
||||
return message.channel.id === this.channel.id ? message.id : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ class MessageEmbed {
|
||||
this.thumbnail = data.thumbnail ? {
|
||||
url: data.thumbnail.url,
|
||||
proxyURL: data.thumbnail.proxy_url,
|
||||
height: data.height,
|
||||
width: data.width,
|
||||
height: data.thumbnail.height,
|
||||
width: data.thumbnail.width,
|
||||
} : null;
|
||||
|
||||
/**
|
||||
@@ -82,8 +82,8 @@ class MessageEmbed {
|
||||
this.image = data.image ? {
|
||||
url: data.image.url,
|
||||
proxyURL: data.image.proxy_url,
|
||||
height: data.height,
|
||||
width: data.width,
|
||||
height: data.image.height,
|
||||
width: data.image.width,
|
||||
} : null;
|
||||
|
||||
/**
|
||||
@@ -175,9 +175,9 @@ class MessageEmbed {
|
||||
addField(name, value, inline = false) {
|
||||
if (this.fields.length >= 25) throw new RangeError('EMBED_FIELD_COUNT');
|
||||
name = Util.resolveString(name);
|
||||
if (!String(name) || name.length > 256) throw new RangeError('EMBED_FIELD_NAME');
|
||||
if (!String(name)) throw new RangeError('EMBED_FIELD_NAME');
|
||||
value = Util.resolveString(value);
|
||||
if (!String(value) || value.length > 1024) throw new RangeError('EMBED_FIELD_VALUE');
|
||||
if (!String(value)) throw new RangeError('EMBED_FIELD_VALUE');
|
||||
this.fields.push({ name, value, inline });
|
||||
return this;
|
||||
}
|
||||
@@ -193,7 +193,7 @@ class MessageEmbed {
|
||||
|
||||
/**
|
||||
* Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when
|
||||
* setting an embed image or author/footer icons. Only one file may be attached.
|
||||
* setting an embed image or author/footer icons. Multiple files can be attached.
|
||||
* @param {Array<FileOptions|string|MessageAttachment>} files Files to attach
|
||||
* @returns {MessageEmbed}
|
||||
*/
|
||||
@@ -235,7 +235,6 @@ class MessageEmbed {
|
||||
*/
|
||||
setDescription(description) {
|
||||
description = Util.resolveString(description);
|
||||
if (description.length > 2048) throw new RangeError('EMBED_DESCRIPTION');
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
@@ -248,7 +247,6 @@ class MessageEmbed {
|
||||
*/
|
||||
setFooter(text, iconURL) {
|
||||
text = Util.resolveString(text);
|
||||
if (text.length > 2048) throw new RangeError('EMBED_FOOTER_TEXT');
|
||||
this.footer = { text, iconURL };
|
||||
return this;
|
||||
}
|
||||
@@ -290,7 +288,6 @@ class MessageEmbed {
|
||||
*/
|
||||
setTitle(title) {
|
||||
title = Util.resolveString(title);
|
||||
if (title.length > 256) throw new RangeError('EMBED_TITLE');
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class MessageMentions {
|
||||
} else {
|
||||
this.users = new Collection();
|
||||
for (const mention of users) {
|
||||
let user = message.client.users.create(mention);
|
||||
let user = message.client.users.add(mention);
|
||||
this.users.set(user.id, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const Emoji = require('./Emoji');
|
||||
const GuildEmoji = require('./GuildEmoji');
|
||||
const ReactionEmoji = require('./ReactionEmoji');
|
||||
const ReactionUserStore = require('../stores/ReactionUserStore');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents a reaction to a message.
|
||||
@@ -32,18 +31,18 @@ class MessageReaction {
|
||||
*/
|
||||
this.users = new ReactionUserStore(client, undefined, this);
|
||||
|
||||
this._emoji = new ReactionEmoji(this, data.emoji.name, data.emoji.id);
|
||||
this._emoji = new ReactionEmoji(this, data.emoji);
|
||||
}
|
||||
|
||||
/**
|
||||
* The emoji of this reaction, either an Emoji object for known custom emojis, or a ReactionEmoji
|
||||
* The emoji of this reaction, either an GuildEmoji object for known custom emojis, or a ReactionEmoji
|
||||
* object which has fewer properties. Whatever the prototype of the emoji, it will still have
|
||||
* `name`, `id`, `identifier` and `toString()`
|
||||
* @type {Emoji|ReactionEmoji}
|
||||
* @type {GuildEmoji|ReactionEmoji}
|
||||
* @readonly
|
||||
*/
|
||||
get emoji() {
|
||||
if (this._emoji instanceof Emoji) return this._emoji;
|
||||
if (this._emoji instanceof GuildEmoji) return this._emoji;
|
||||
// Check to see if the emoji has become known to the client
|
||||
if (this._emoji.id) {
|
||||
const emojis = this.message.client.emojis;
|
||||
@@ -56,27 +55,6 @@ class MessageReaction {
|
||||
return this._emoji;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a user from this reaction.
|
||||
* @param {UserResolvable} [user=this.message.client.user] The user to remove the reaction of
|
||||
* @returns {Promise<MessageReaction>}
|
||||
*/
|
||||
remove(user = this.message.client.user) {
|
||||
const userID = this.message.client.users.resolveID(user);
|
||||
if (!userID) return Promise.reject(new Error('REACTION_RESOLVE_USER'));
|
||||
return this.message.client.api.channels[this.message.channel.id].messages[this.message.id]
|
||||
.reactions[this.emoji.identifier][userID === this.message.client.user.id ? '@me' : userID]
|
||||
.delete()
|
||||
.then(() =>
|
||||
this.message.client.actions.MessageReactionRemove.handle({
|
||||
user_id: userID,
|
||||
message_id: this.message.id,
|
||||
emoji: this.emoji,
|
||||
channel_id: this.message.channel.id,
|
||||
}).reaction
|
||||
);
|
||||
}
|
||||
|
||||
_add(user) {
|
||||
if (!this.users.has(user.id)) {
|
||||
this.users.set(user.id, user);
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
const { ActivityTypes } = require('../util/Constants');
|
||||
const { ActivityTypes, ActivityFlags } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* Activity sent in a message.
|
||||
* @typedef {Object} MessageActivity
|
||||
* @property {string} [partyID] Id of the party represented in activity
|
||||
* @property {number} [type] Type of activity sent
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a user's presence.
|
||||
@@ -118,6 +125,17 @@ class Activity {
|
||||
* @type {?RichPresenceAssets}
|
||||
*/
|
||||
this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null;
|
||||
|
||||
this.syncID = data.sync_id;
|
||||
this._flags = data.flags;
|
||||
}
|
||||
|
||||
get flags() {
|
||||
const flags = [];
|
||||
for (const [name, flag] of Object.entries(ActivityFlags)) {
|
||||
if ((this._flags & flag) === flag) flags.push(name);
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +152,14 @@ class Activity {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically returns the activities's name instead of the Activity object.
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
_clone() {
|
||||
return Object.assign(Object.create(this), this);
|
||||
}
|
||||
@@ -193,6 +219,9 @@ class RichPresenceAssets {
|
||||
*/
|
||||
largeImageURL({ format, size } = {}) {
|
||||
if (!this.largeImage) return null;
|
||||
if (/^spotify:/.test(this.largeImage)) {
|
||||
return `https://i.scdn.co/image/${this.largeImage.slice(8)}`;
|
||||
}
|
||||
return this.activity.presence.client.rest.cdn
|
||||
.AppAsset(this.activity.applicationID, this.largeImage, { format, size });
|
||||
}
|
||||
|
||||
@@ -52,12 +52,12 @@ class ReactionCollector extends Collector {
|
||||
this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty);
|
||||
});
|
||||
|
||||
this.on('collect', (collected, reaction, user) => {
|
||||
this.on('collect', (reaction, user) => {
|
||||
this.total++;
|
||||
this.users.set(user.id, user);
|
||||
});
|
||||
|
||||
this.on('dispose', (disposed, reaction, user) => {
|
||||
this.on('remove', (reaction, user) => {
|
||||
this.total--;
|
||||
if (!this.collected.some(r => r.users.has(user.id))) this.users.delete(user.id);
|
||||
});
|
||||
@@ -66,23 +66,33 @@ class ReactionCollector extends Collector {
|
||||
/**
|
||||
* Handles an incoming reaction for possible collection.
|
||||
* @param {MessageReaction} reaction The reaction to possibly collect
|
||||
* @returns {?{key: Snowflake, value: MessageReaction}}
|
||||
* @returns {?Snowflake|string}
|
||||
* @private
|
||||
*/
|
||||
collect(reaction) {
|
||||
/**
|
||||
* Emitted whenever a reaction is collected.
|
||||
* @event ReactionCollector#collect
|
||||
* @param {MessageReaction} reaction The reaction that was collected
|
||||
* @param {User} user The user that added the reaction
|
||||
*/
|
||||
if (reaction.message.id !== this.message.id) return null;
|
||||
return {
|
||||
key: ReactionCollector.key(reaction),
|
||||
value: reaction,
|
||||
};
|
||||
return ReactionCollector.key(reaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a reaction deletion for possible disposal.
|
||||
* @param {MessageReaction} reaction The reaction to possibly dispose
|
||||
* @param {MessageReaction} reaction The reaction to possibly dispose of
|
||||
* @param {User} user The user that removed the reaction
|
||||
* @returns {?Snowflake|string}
|
||||
*/
|
||||
dispose(reaction) {
|
||||
dispose(reaction, user) {
|
||||
/**
|
||||
* Emitted whenever a reaction is disposed of.
|
||||
* @event ReactionCollector#dispose
|
||||
* @param {MessageReaction} reaction The reaction that was disposed of
|
||||
* @param {User} user The user that removed the reaction
|
||||
*/
|
||||
if (reaction.message.id !== this.message.id) return null;
|
||||
|
||||
/**
|
||||
@@ -91,8 +101,11 @@ class ReactionCollector extends Collector {
|
||||
* is removed.
|
||||
* @event ReactionCollector#remove
|
||||
* @param {MessageReaction} reaction The reaction that was removed
|
||||
* @param {User} user The user that removed the reaction
|
||||
*/
|
||||
if (this.collected.has(reaction)) this.emit('remove', reaction);
|
||||
if (this.collected.has(ReactionCollector.key(reaction))) {
|
||||
this.emit('remove', reaction, user);
|
||||
}
|
||||
return reaction.count ? null : ReactionCollector.key(reaction);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
const Emoji = require('./Emoji');
|
||||
|
||||
/**
|
||||
* Represents a limited emoji set used for both custom and unicode emojis. Custom emojis
|
||||
* will use this class opposed to the Emoji class when the client doesn't know enough
|
||||
* information about them.
|
||||
* @extends {Emoji}
|
||||
*/
|
||||
class ReactionEmoji {
|
||||
constructor(reaction, name, id) {
|
||||
class ReactionEmoji extends Emoji {
|
||||
constructor(reaction, emoji) {
|
||||
super(reaction.message.client, emoji);
|
||||
/**
|
||||
* The message reaction this emoji refers to
|
||||
* @type {MessageReaction}
|
||||
*/
|
||||
this.reaction = reaction;
|
||||
|
||||
/**
|
||||
* The name of this reaction emoji
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = name;
|
||||
|
||||
/**
|
||||
* The ID of this reaction emoji
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The identifier of this emoji, used for message reactions
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
get identifier() {
|
||||
if (this.id) return `${this.name}:${this.id}`;
|
||||
return encodeURIComponent(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord
|
||||
* instead of the ReactionEmoji object.
|
||||
* @returns {string}
|
||||
* @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}`);
|
||||
*/
|
||||
toString() {
|
||||
return this.id ? `<:${this.name}:${this.id}>` : this.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ const Snowflake = require('../util/Snowflake');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const Util = require('../util/Util');
|
||||
const Base = require('./Base');
|
||||
const { TypeError } = require('../errors');
|
||||
const { Error, TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents a role on Discord.
|
||||
@@ -95,9 +95,7 @@ class Role extends Base {
|
||||
* @readonly
|
||||
*/
|
||||
get hexColor() {
|
||||
let col = this.color.toString(16);
|
||||
while (col.length < 6) col = `0${col}`;
|
||||
return `#${col}`;
|
||||
return `#${this.color.toString(16).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +116,7 @@ class Role extends Base {
|
||||
if (this.managed) return false;
|
||||
const clientMember = this.guild.member(this.client.user);
|
||||
if (!clientMember.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return false;
|
||||
return clientMember.highestRole.comparePositionTo(this) > 0;
|
||||
return clientMember.roles.highest.comparePositionTo(this) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,6 +193,18 @@ class Role extends Base {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel,
|
||||
* taking into account permission overwrites.
|
||||
* @param {ChannelResolvable} channel The guild channel to use as context
|
||||
* @returns {?Permissions}
|
||||
*/
|
||||
permissionsIn(channel) {
|
||||
channel = this.guild.channels.resolve(channel);
|
||||
if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
|
||||
return channel.rolePermissions(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new name for the role.
|
||||
* @param {string} name The new name of the role
|
||||
|
||||
@@ -35,7 +35,7 @@ class TextChannel extends GuildChannel {
|
||||
|
||||
this.lastMessageID = data.last_message_id;
|
||||
|
||||
if (data.messages) for (const message of data.messages) this.messages.create(message);
|
||||
if (data.messages) for (const message of data.messages) this.messages.add(message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,7 @@ class VoiceChannel extends GuildChannel {
|
||||
* The bitrate of this voice channel
|
||||
* @type {number}
|
||||
*/
|
||||
this.bitrate = data.bitrate * 0.001;
|
||||
this.bitrate = data.bitrate;
|
||||
|
||||
/**
|
||||
* The maximum amount of users allowed in this channel - 0 means unlimited.
|
||||
@@ -76,18 +76,17 @@ class VoiceChannel extends GuildChannel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bitrate of the channel (in kbps).
|
||||
* Sets the bitrate of the channel.
|
||||
* @param {number} bitrate The new bitrate
|
||||
* @param {string} [reason] Reason for changing the channel's bitrate
|
||||
* @returns {Promise<VoiceChannel>}
|
||||
* @example
|
||||
* // Set the bitrate of a voice channel
|
||||
* voiceChannel.setBitrate(48)
|
||||
* .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`))
|
||||
* voiceChannel.setBitrate(48000)
|
||||
* .then(vc => console.log(`Set bitrate to ${vc.bitrate}bps for ${vc.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setBitrate(bitrate, reason) {
|
||||
bitrate *= 1000;
|
||||
return this.edit({ bitrate }, reason);
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ class Webhook {
|
||||
auth: false,
|
||||
}).then(d => {
|
||||
if (!this.client.channels) return d;
|
||||
return this.client.channels.get(d.channel_id).messages.create(d, false);
|
||||
return this.client.channels.get(d.channel_id).messages.add(d, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class Webhook {
|
||||
data: body,
|
||||
}).then(data => {
|
||||
if (!this.client.channels) return data;
|
||||
return this.client.channels.get(data.channel_id).messages.create(data, false);
|
||||
return this.client.channels.get(data.channel_id).messages.add(data, false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -76,17 +76,17 @@ class Collector extends EventEmitter {
|
||||
*/
|
||||
handleCollect(...args) {
|
||||
const collect = this.collect(...args);
|
||||
if (!collect || !this.filter(...args, this.collected)) return;
|
||||
|
||||
this.collected.set(collect.key, collect.value);
|
||||
if (collect && this.filter(...args, this.collected)) {
|
||||
this.collected.set(collect, args[0]);
|
||||
|
||||
/**
|
||||
* Emitted whenever an element is collected.
|
||||
* @event Collector#collect
|
||||
* @param {*} element The element that got collected
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
*/
|
||||
this.emit('collect', collect.value, ...args);
|
||||
/**
|
||||
* Emitted whenever an element is collected.
|
||||
* @event Collector#collect
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
*/
|
||||
this.emit('collect', ...args);
|
||||
}
|
||||
this.checkEnd();
|
||||
}
|
||||
|
||||
@@ -100,17 +100,14 @@ class Collector extends EventEmitter {
|
||||
|
||||
const dispose = this.dispose(...args);
|
||||
if (!dispose || !this.filter(...args) || !this.collected.has(dispose)) return;
|
||||
|
||||
const value = this.collected.get(dispose);
|
||||
this.collected.delete(dispose);
|
||||
|
||||
/**
|
||||
* Emitted whenever an element has been disposed.
|
||||
* Emitted whenever an element is disposed of.
|
||||
* @event Collector#dispose
|
||||
* @param {*} element The element that was disposed
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
*/
|
||||
this.emit('dispose', value, ...args);
|
||||
this.emit('dispose', ...args);
|
||||
this.checkEnd();
|
||||
}
|
||||
|
||||
|
||||
@@ -195,10 +195,8 @@ class TextBasedChannel {
|
||||
* @returns {MessageCollector}
|
||||
* @example
|
||||
* // Create a message collector
|
||||
* const collector = channel.createMessageCollector(
|
||||
* m => m.content.includes('discord'),
|
||||
* { time: 15000 }
|
||||
* );
|
||||
* const filter = m => m.content.includes('discord');
|
||||
* const collector = channel.createMessageCollector(filter, { time: 15000 });
|
||||
* collector.on('collect', m => console.log(`Collected ${m.content}`));
|
||||
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
|
||||
*/
|
||||
@@ -246,6 +244,11 @@ class TextBasedChannel {
|
||||
* Messages or number of messages to delete
|
||||
* @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically
|
||||
* @returns {Promise<Collection<Snowflake, Message>>} Deleted messages
|
||||
* @example
|
||||
* // Bulk delete messages
|
||||
* channel.bulkDelete(5)
|
||||
* .then(messages => console.log(`Bulk deleted ${messages.size} messages`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async bulkDelete(messages, filterOld = false) {
|
||||
if (messages instanceof Array || messages instanceof Collection) {
|
||||
|
||||
@@ -4,6 +4,7 @@ const MessageEmbed = require('../MessageEmbed');
|
||||
const MessageAttachment = require('../MessageAttachment');
|
||||
const { browser } = require('../../util/Constants');
|
||||
const Util = require('../../util/Util');
|
||||
const { RangeError } = require('../../errors');
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
module.exports = async function createMessage(channel, options) {
|
||||
@@ -19,18 +20,31 @@ module.exports = async function createMessage(channel, options) {
|
||||
if (isNaN(options.nonce) || options.nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
|
||||
}
|
||||
|
||||
let { content } = options;
|
||||
if (options instanceof MessageEmbed) options = webhook ? { embeds: [options] } : { embed: options };
|
||||
if (options instanceof MessageAttachment) options = { files: [options.file] };
|
||||
|
||||
if (content instanceof Array || options instanceof Array) {
|
||||
const which = content instanceof Array ? content : options;
|
||||
const attachments = which.filter(item => item instanceof MessageAttachment);
|
||||
const embeds = which.filter(item => item instanceof MessageEmbed);
|
||||
if (attachments.length) options = { files: attachments };
|
||||
if (embeds.length) options = { embeds };
|
||||
if ((embeds.length || attachments.length) && content instanceof Array) {
|
||||
content = null;
|
||||
options.content = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (options.reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') {
|
||||
const id = channel.client.users.resolveID(options.reply);
|
||||
const mention = `<@${options.reply instanceof GuildMember && options.reply.nickname ? '!' : ''}${id}>`;
|
||||
if (options.split) options.split.prepend = `${mention}, ${options.split.prepend || ''}`;
|
||||
options.content = `${mention}${typeof options.content !== 'undefined' ? `, ${options.content}` : ''}`;
|
||||
content = `${mention}${typeof options.content !== 'undefined' ? `, ${options.content}` : ''}`;
|
||||
}
|
||||
|
||||
if (options.content) {
|
||||
options.content = Util.resolveString(options.content);
|
||||
if (content) {
|
||||
options.content = Util.resolveString(content);
|
||||
if (options.split && typeof options.split !== 'object') options.split = {};
|
||||
// Wrap everything in a code block
|
||||
if (typeof options.code !== 'undefined' && (typeof options.code !== 'boolean' || options.code === true)) {
|
||||
|
||||
@@ -90,7 +90,7 @@ module.exports = function search(target, options) {
|
||||
let endpoint = target.client.api[target instanceof Channel ? 'channels' : 'guilds'](target.id).messages().search;
|
||||
return endpoint.get({ query: options }).then(body => {
|
||||
const results = body.messages.map(x =>
|
||||
x.map(m => target.client.channels.get(m.channel_id).messages.create(m, false))
|
||||
x.map(m => target.client.channels.get(m.channel_id).messages.add(m, false))
|
||||
);
|
||||
return {
|
||||
total: body.total_results,
|
||||
|
||||
@@ -192,12 +192,12 @@ class Collection extends Map {
|
||||
for (const item of this.values()) {
|
||||
if (item[propOrFn] === value) return item;
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
} else if (typeof propOrFn === 'function') {
|
||||
for (const [key, val] of this) {
|
||||
if (propOrFn(val, key, this)) return val;
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
} else {
|
||||
throw new Error('First argument must be a property string or a function.');
|
||||
}
|
||||
@@ -223,12 +223,12 @@ class Collection extends Map {
|
||||
for (const [key, val] of this) {
|
||||
if (val[propOrFn] === value) return key;
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
} else if (typeof propOrFn === 'function') {
|
||||
for (const [key, val] of this) {
|
||||
if (propOrFn(val, key, this)) return key;
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
} else {
|
||||
throw new Error('First argument must be a property string or a function.');
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user