From 65673197d4fbb10038ea4ab650177831a57fa577 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 25 Oct 2017 23:14:41 +0100 Subject: [PATCH 001/154] Start rewrite with new prism --- package.json | 4 +- src/client/voice/VoiceBroadcast.js | 5 - src/client/voice/VoiceConnection.js | 12 +- .../voice/dispatcher/StreamDispatcher.js | 323 +++--------------- src/client/voice/player/AudioPlayer.js | 128 ++----- test/voice.js | 17 +- 6 files changed, 91 insertions(+), 398 deletions(-) diff --git a/package.json b/package.json index 50fb6bb59..25688e774 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "long": "^3.0.0", "pako": "^1.0.0", - "prism-media": "^0.0.2", + "prism-media": "github:hydrabolt/prism-media#indev", "snekfetch": "^3.0.0", "tweetnacl": "^1.0.0", "ws": "^3.0.0" @@ -42,8 +42,6 @@ "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", diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 678045dca..0cc8e07ae 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -40,11 +40,6 @@ class VoiceBroadcast extends VolumeInterface { * @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} diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 1953d6b07..99e69e66e 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -35,17 +35,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} @@ -494,6 +483,7 @@ class VoiceConnection extends EventEmitter { * .catch(console.error); */ playStream(stream, options) { + console.log('VC!'); return this.player.playUnknownStream(stream, options); } diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index d128039eb..2dc3a6aa6 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -1,9 +1,12 @@ const VolumeInterface = require('../util/VolumeInterface'); const VoiceBroadcast = require('../VoiceBroadcast'); const { VoiceStatus } = require('../../../util/Constants'); +const { Writable } = require('stream'); const secretbox = require('../util/Secretbox'); +const FRAME_LENGTH = 20; + const nonce = Buffer.alloc(24); nonce.fill(0); @@ -18,138 +21,38 @@ nonce.fill(0); * ``` * @implements {VolumeInterface} */ -class StreamDispatcher extends VolumeInterface { - constructor(player, stream, streamOptions) { +class StreamDispatcher extends Writable { + constructor(player, streamOptions) { 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; - - /** - * Whether playing is paused - * @type {boolean} - */ - this.paused = false; - /** - * Whether this dispatcher has been destroyed - * @type {boolean} - */ - this.destroyed = false; - - this._opus = streamOptions.opus; + this.startTime = null; + this.on('error', this.destroy.bind(this)); + this.on('finish', () => { + this.destroy.bind(this); + this.emit('end'); + }); } - /** - * 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); - } - - /** - * The total time, taking into account pauses and skips, that the dispatcher has been streaming for - * @type {number} - * @readonly - */ - get totalStreamTime() { - return this.time + this.streamingData.pausedTime; - } - - /** - * 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. - * If set to 'auto', the voice channel's bitrate will be used - */ - setBitrate(bitrate) { - this.player.setBitrate(bitrate); - } - - sendBuffer(buffer, sequence, timestamp, opusPacket) { - opusPacket = opusPacket || this.player.opusEncoder.encode(buffer); - const packet = this.createPacket(sequence, timestamp, opusPacket); - this.sendPacket(packet); - } - - sendPacket(packet) { - let repeats = this.passes; - /** - * Emitted whenever the dispatcher has debug information. - * @event StreamDispatcher#debug - * @param {string} info The debug info - */ + _write(chunk, enc, done) { + if (!this.startTime) this.startTime = Date.now(); 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}`); - }); - } + const packet = this.createPacket(this._sdata.sequence, this._sdata.timestamp, chunk); + this.sendPacket(packet); + const next = FRAME_LENGTH + (this.startTime + (this._sdata.count * FRAME_LENGTH) - Date.now()); + setTimeout(done.bind(this), next); + // Do overflow checks here! + this._sdata.sequence++; + this._sdata.timestamp += 960; + this._sdata.count++; } createPacket(sequence, timestamp, buffer) { @@ -169,163 +72,41 @@ class StreamDispatcher extends VolumeInterface { 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 = 1; /** - * 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; + /** + * Emitted when the dispatcher starts/stops speaking. + * @event StreamDispatcher#speaking + * @param {boolean} value Whether or not the dispatcher is speaking + */ + this.emit('speaking', value); + } + + destroy() { + const streams = this.player.streams; + if (streams.opus) streams.opus.unpipe(this); + if (streams.ffmpeg) streams.ffmpeg.destroy(); + this.end(); + } } module.exports = StreamDispatcher; diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 5380de3f8..acb17e6c8 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -1,10 +1,10 @@ const EventEmitter = require('events').EventEmitter; -const Prism = require('prism-media'); +const prism = require('prism-media'); const StreamDispatcher = require('../dispatcher/StreamDispatcher'); const Collection = require('../../../util/Collection'); const OpusEncoders = require('../opus/OpusEngineList'); -const ffmpegArguments = [ +const FFMPEG_ARGUMENTS = [ '-analyzeduration', '0', '-loglevel', '0', '-f', 's16le', @@ -25,13 +25,10 @@ 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.streams = {}; + this.dispatcher = null; + this.streamingData = { channels: 2, count: 0, @@ -39,45 +36,19 @@ class AudioPlayer extends EventEmitter { 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; + this.destroyDispatcher(); } - 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'); + destroyDispatcher() { + if (this.dispatcher) { + this.dispatcher.destroy(); + this.dispatcher = null; } - this.currentStream = {}; - this.streamingData.pausedTime = 0; } /** @@ -93,76 +64,35 @@ class AudioPlayer extends EventEmitter { } 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); + this.destroyDispatcher(); + const ffmpeg = this.streams.ffmpeg = new prism.FFmpeg({ args: FFMPEG_ARGUMENTS }); + stream.pipe(ffmpeg); + return this.playPCMStream(ffmpeg, options); } - 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; + playPCMStream(stream, options = {}) { + this.destroyDispatcher(); + const opus = this.streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); + stream.pipe(opus); + return this.playOpusStream(opus, options); } playOpusStream(stream, options = {}) { - options.opus = true; - this.destroyCurrentStream(); - const dispatcher = this.createDispatcher(stream, options); - this.currentStream = { - dispatcher, - input: stream, - output: stream, - }; + this.destroyDispatcher(); + const dispatcher = this.dispatcher = this.createDispatcher(options); + stream.pipe(dispatcher); 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 } = {}) { + createDispatcher({ seek = 0, volume = 1, passes = 1 } = {}) { + this.destroyDispatcher(); const options = { seek, volume, passes }; - - const dispatcher = new StreamDispatcher(this, stream, options); - dispatcher.on('end', () => this.destroyCurrentStream()); - dispatcher.on('error', () => this.destroyCurrentStream()); + const dispatcher = new StreamDispatcher(this, options); + this.streamingData.count = 0; dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); return dispatcher; } diff --git a/test/voice.js b/test/voice.js index 0b36636a3..b66a4b38a 100644 --- a/test/voice.js +++ b/test/voice.js @@ -6,7 +6,7 @@ const ytdl = require('ytdl-core'); const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' }); -const auth = require('./auth.json'); +const auth = require('./auth.js'); client.login(auth.token).then(() => console.log('logged')).catch(console.error); @@ -14,8 +14,12 @@ const connections = new Map(); let broadcast; +client.on('debug', console.log); +client.on('error', console.log); + client.on('message', m => { if (!m.guild) return; + if (m.author.id !== '66564597481480192') return; if (m.content.startsWith('/join')) { const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel; if (channel && channel.type === 'voice') { @@ -23,23 +27,18 @@ client.on('message', m => { conn.player.on('error', (...e) => console.log('player', ...e)); if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); + conn.playStream(ytdl('https://www.youtube.com/watch?v=i3Jv9fNPjgk')); }); } else { m.reply('Specify a voice channel!'); } } else if (m.content.startsWith('/play')) { if (connections.has(m.guild.id)) { - const connData = connections.get(m.guild.id); - const queue = connData.queue; const url = m.content.split(' ').slice(1).join(' ') .replace(//g, ''); - queue.push({ url, m }); - if (queue.length > 1) { - m.reply(`OK, that's going to play after ${queue.length - 1} songs`); - return; - } - doQueue(connData); + const stream = ytdl(item.url, { filter: 'audioonly' }, { passes: 3 }); + m.guild.voiceConnection.playStream(stream); } } else if (m.content.startsWith('/skip')) { if (connections.has(m.guild.id)) { From 48452173caeaa0b95a874d58801e1782de2e16f1 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 25 Oct 2017 23:21:02 +0100 Subject: [PATCH 002/154] probably would cause an error somewhere --- src/client/voice/dispatcher/StreamDispatcher.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 2dc3a6aa6..174b9012f 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -102,6 +102,7 @@ class StreamDispatcher extends Writable { } destroy() { + if (this.player.dispatcher !== this) return; const streams = this.player.streams; if (streams.opus) streams.opus.unpipe(this); if (streams.ffmpeg) streams.ffmpeg.destroy(); From 7f90d4ebc59c8d030410ae8919d15f5d125a8437 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 25 Oct 2017 23:46:05 +0100 Subject: [PATCH 003/154] Better ending --- src/client/voice/dispatcher/StreamDispatcher.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 174b9012f..323616103 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -103,10 +103,11 @@ class StreamDispatcher extends Writable { destroy() { if (this.player.dispatcher !== this) return; + this.player.dispatcher = null; const streams = this.player.streams; + this.end(); if (streams.opus) streams.opus.unpipe(this); if (streams.ffmpeg) streams.ffmpeg.destroy(); - this.end(); } } From a76e4c064d740928cebfc3c43fa3e361c80fc5bb Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 00:32:14 +0100 Subject: [PATCH 004/154] Fix sequence and timestamp growing too large --- src/client/voice/dispatcher/StreamDispatcher.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 323616103..63294a5b0 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -50,6 +50,8 @@ class StreamDispatcher extends Writable { const next = FRAME_LENGTH + (this.startTime + (this._sdata.count * FRAME_LENGTH) - Date.now()); setTimeout(done.bind(this), next); // Do overflow checks here! + if (this._sdata.sequence === (2 ** 16) - 1) this._sdata.sequence = -1; + if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -1; this._sdata.sequence++; this._sdata.timestamp += 960; this._sdata.count++; From 79c10f7084ae14458f3cda739a2c0490ce9d3126 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 12:01:04 +0100 Subject: [PATCH 005/154] Fix resetting timestamps --- src/client/voice/dispatcher/StreamDispatcher.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 63294a5b0..34f035f7e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -49,9 +49,8 @@ class StreamDispatcher extends Writable { this.sendPacket(packet); const next = FRAME_LENGTH + (this.startTime + (this._sdata.count * FRAME_LENGTH) - Date.now()); setTimeout(done.bind(this), next); - // Do overflow checks here! if (this._sdata.sequence === (2 ** 16) - 1) this._sdata.sequence = -1; - if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -1; + if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -960; this._sdata.sequence++; this._sdata.timestamp += 960; this._sdata.count++; From f5d10a5f763b86a748b0aabec25aba5529fd1905 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 13:32:38 +0100 Subject: [PATCH 006/154] Pause/Unpause --- .../voice/dispatcher/StreamDispatcher.js | 65 ++++++++++++++----- src/client/voice/player/AudioPlayer.js | 2 - 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 34f035f7e..533b16027 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -6,6 +6,8 @@ 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); @@ -30,10 +32,17 @@ class StreamDispatcher extends Writable { */ this.player = player; this.streamOptions = streamOptions; - this.startTime = null; + + this.pausedSince = null; + this._writeCallback = null; + + this.pausedTime = 0; + this.count = 0; + this.on('error', this.destroy.bind(this)); this.on('finish', () => { this.destroy.bind(this); + // Still emitting end for backwards compatibility, probably remove it in the future! this.emit('end'); }); } @@ -44,16 +53,47 @@ class StreamDispatcher extends Writable { _write(chunk, enc, done) { if (!this.startTime) this.startTime = Date.now(); - this.setSpeaking(true); - const packet = this.createPacket(this._sdata.sequence, this._sdata.timestamp, chunk); - this.sendPacket(packet); - const next = FRAME_LENGTH + (this.startTime + (this._sdata.count * FRAME_LENGTH) - Date.now()); + this._playChunk(chunk); + this._step(done); + } + + _destroy(err, cb) { + if (this.player.dispatcher !== this) return; + this.player.dispatcher = null; + const streams = this.player.streams; + if (streams.opus) streams.opus.unpipe(this); + if (streams.ffmpeg) streams.ffmpeg.destroy(); + super._destroy(err, cb); + } + + pause() { + this.pausedSince = Date.now(); + } + + unpause() { + this.pausedTime += Date.now() - this.pausedSince; + this.pausedSince = null; + if (this._writeCallback) this._writeCallback(); + } + + _step(done) { + if (this.pausedSince) { + this._writeCallback = done; + return; + } + const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime); setTimeout(done.bind(this), next); if (this._sdata.sequence === (2 ** 16) - 1) this._sdata.sequence = -1; - if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -960; + if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -TIMESTAMP_INC; this._sdata.sequence++; - this._sdata.timestamp += 960; - this._sdata.count++; + this._sdata.timestamp += TIMESTAMP_INC; + this.count++; + } + + _playChunk(chunk) { + if (this.player.dispatcher !== this) return; + this.setSpeaking(true); + this.sendPacket(this.createPacket(this._sdata.sequence, this._sdata.timestamp, chunk)); } createPacket(sequence, timestamp, buffer) { @@ -101,15 +141,6 @@ class StreamDispatcher extends Writable { */ this.emit('speaking', value); } - - destroy() { - if (this.player.dispatcher !== this) return; - this.player.dispatcher = null; - const streams = this.player.streams; - this.end(); - if (streams.opus) streams.opus.unpipe(this); - if (streams.ffmpeg) streams.ffmpeg.destroy(); - } } module.exports = StreamDispatcher; diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index acb17e6c8..7a02aaa5f 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -1,8 +1,6 @@ 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 FFMPEG_ARGUMENTS = [ '-analyzeduration', '0', From 53ca34cbdebcc2545c6e0e70c1031668291e0199 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 13:33:14 +0100 Subject: [PATCH 007/154] Don't need to continue count and pausedTime amongst dispatchers --- src/client/voice/player/AudioPlayer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 7a02aaa5f..a219204f4 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -29,10 +29,8 @@ class AudioPlayer extends EventEmitter { this.streamingData = { channels: 2, - count: 0, sequence: 0, timestamp: 0, - pausedTime: 0, }; this.voiceConnection.once('closing', () => this.destroyCurrentStream()); From 8913096f73020ff55d0df4f62e3fb4725fea9306 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 13:42:21 +0100 Subject: [PATCH 008/154] =?UTF-8?q?Fix=20Travis=20by=20removing=20stuff=20?= =?UTF-8?q?for=20fun=20=F0=9F=91=8C=F0=9F=91=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/voice/VoiceConnection.js | 2 -- src/client/voice/dispatcher/StreamDispatcher.js | 2 -- src/client/voice/player/AudioPlayer.js | 4 ---- 3 files changed, 8 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 99e69e66e..9465caa95 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -5,7 +5,6 @@ const { OPCodes, VoiceOPCodes, VoiceStatus } = require('../../util/Constants'); const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/VoiceReceiver'); const EventEmitter = require('events'); -const Prism = require('prism-media'); const { Error } = require('../../errors'); /** @@ -483,7 +482,6 @@ class VoiceConnection extends EventEmitter { * .catch(console.error); */ playStream(stream, options) { - console.log('VC!'); return this.player.playUnknownStream(stream, options); } diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 533b16027..b8dc8e1cf 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -1,5 +1,3 @@ -const VolumeInterface = require('../util/VolumeInterface'); -const VoiceBroadcast = require('../VoiceBroadcast'); const { VoiceStatus } = require('../../../util/Constants'); const { Writable } = require('stream'); diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index a219204f4..fcf03d1ec 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -80,10 +80,6 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - playBroadcast(broadcast, options) { - - } - createDispatcher({ seek = 0, volume = 1, passes = 1 } = {}) { this.destroyDispatcher(); const options = { seek, volume, passes }; From 3e3e6f9af704c2bc38c9b23526c16bf3e5be55cf Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 13:45:36 +0100 Subject: [PATCH 009/154] Actually fix ESlint this time --- src/client/voice/VoiceBroadcast.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 0cc8e07ae..14cf2a186 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,5 +1,4 @@ const VolumeInterface = require('./util/VolumeInterface'); -const Prism = require('prism-media'); const OpusEncoders = require('./opus/OpusEngineList'); const Collection = require('../../util/Collection'); From ac0cc9a00958ba14fad3dc7cd79f9c61490ebd5a Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 14:02:44 +0100 Subject: [PATCH 010/154] Remove useless SecretKey class --- package.json | 1 - src/client/voice/VoiceWebSocket.js | 8 +++++--- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- src/client/voice/receiver/VoiceReceiver.js | 2 +- src/client/voice/util/SecretKey.js | 16 ---------------- 5 files changed, 7 insertions(+), 22 deletions(-) delete mode 100644 src/client/voice/util/SecretKey.js diff --git a/package.json b/package.json index 25688e774..4e2400baa 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "src/client/voice/receiver/VoiceReadable.js": false, "src/client/voice/receiver/VoiceReceiver.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 } diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index a34962496..18632355f 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -1,5 +1,4 @@ const { OPCodes, VoiceOPCodes } = require('../../util/Constants'); -const SecretKey = require('./util/SecretKey'); const EventEmitter = require('events'); const { Error } = require('../../errors'); const WebSocket = require('../../WebSocket'); @@ -164,14 +163,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: /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index b8dc8e1cf..99aaf57c0 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -105,7 +105,7 @@ class StreamDispatcher extends Writable { 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; diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index 4c889865c..bc7a87061 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -148,7 +148,7 @@ class VoiceReceiver extends EventEmitter { handlePacket(msg, user) { msg.copy(nonce, 0, 0, 12); - let data = secretbox.methods.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key); + let data = secretbox.methods.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey); if (!data) { /** * Emitted whenever a voice packet experiences a problem. diff --git a/src/client/voice/util/SecretKey.js b/src/client/voice/util/SecretKey.js deleted file mode 100644 index f165e5ffc..000000000 --- a/src/client/voice/util/SecretKey.js +++ /dev/null @@ -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; From f6959a848fc0d5b63f5d5471bffcc1158b13f44d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 14:17:56 +0100 Subject: [PATCH 011/154] Start some docs crap --- .../voice/dispatcher/StreamDispatcher.js | 26 ++++++++++++++----- test/voice.js | 8 ++++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 99aaf57c0..1a3f7c98d 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -10,6 +10,11 @@ const TIMESTAMP_INC = (48000 / 100) * CHANNELS; const nonce = Buffer.alloc(24); nonce.fill(0); +/** + * @external Stream.writable + * @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable} + */ + /** * The class that sends voice packet data to the voice connection. * ```js @@ -20,6 +25,7 @@ nonce.fill(0); * }); * ``` * @implements {VolumeInterface} + * @extends {stream.Writable} */ class StreamDispatcher extends Writable { constructor(player, streamOptions) { @@ -64,10 +70,16 @@ class StreamDispatcher extends Writable { super._destroy(err, cb); } + /** + * Pauses playback + */ pause() { this.pausedSince = Date.now(); } + /** + * Resumes playback + */ unpause() { this.pausedTime += Date.now() - this.pausedSince; this.pausedSince = null; @@ -90,11 +102,11 @@ class StreamDispatcher extends Writable { _playChunk(chunk) { if (this.player.dispatcher !== this) return; - this.setSpeaking(true); - this.sendPacket(this.createPacket(this._sdata.sequence, this._sdata.timestamp, chunk)); + this._setSpeaking(true); + this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk)); } - createPacket(sequence, timestamp, buffer) { + _createPacket(sequence, timestamp, buffer) { const packetBuffer = Buffer.alloc(buffer.length + 28); packetBuffer.fill(0); packetBuffer[0] = 0x80; @@ -111,24 +123,24 @@ class StreamDispatcher extends Writable { return packetBuffer; } - sendPacket(packet) { + _sendPacket(packet) { let repeats = 1; /** * Emitted whenever the dispatcher has debug information. * @event StreamDispatcher#debug * @param {string} info The debug info */ - this.setSpeaking(true); + this._setSpeaking(true); while (repeats--) { this.player.voiceConnection.sockets.udp.send(packet) .catch(e => { - this.setSpeaking(false); + this._setSpeaking(false); this.emit('debug', `Failed to send a packet ${e}`); }); } } - setSpeaking(value) { + _setSpeaking(value) { if (this.speaking === value) return; if (this.player.voiceConnection.status !== VoiceStatus.CONNECTED) return; this.speaking = value; diff --git a/test/voice.js b/test/voice.js index b66a4b38a..c88cbed31 100644 --- a/test/voice.js +++ b/test/voice.js @@ -3,6 +3,8 @@ const Discord = require('../'); const ytdl = require('ytdl-core'); +const prism = require('prism-media'); +const fs = require('fs'); const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' }); @@ -27,7 +29,9 @@ client.on('message', m => { conn.player.on('error', (...e) => console.log('player', ...e)); if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); - conn.playStream(ytdl('https://www.youtube.com/watch?v=i3Jv9fNPjgk')); + const d = conn.playOpusStream( + fs.createReadStream('C:/users/amish/downloads/s.ogg').pipe(new prism.OggOpusDemuxer()) + ); }); } else { m.reply('Specify a voice channel!'); @@ -37,7 +41,7 @@ client.on('message', m => { const url = m.content.split(' ').slice(1).join(' ') .replace(//g, ''); - const stream = ytdl(item.url, { filter: 'audioonly' }, { passes: 3 }); + const stream = ytdl(url, { filter: 'audioonly' }, { passes: 3 }); m.guild.voiceConnection.playStream(stream); } } else if (m.content.startsWith('/skip')) { From 863e38676fd59fe3c7c7edb1a43ffff0e6bd08cd Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 14:39:58 +0100 Subject: [PATCH 012/154] Add back setBitrate --- src/client/voice/dispatcher/StreamDispatcher.js | 8 ++++++++ src/client/voice/player/AudioPlayer.js | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 1a3f7c98d..e5de206bf 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -86,6 +86,14 @@ class StreamDispatcher extends Writable { if (this._writeCallback) this._writeCallback(); } + /** + * 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 + * @returns {boolean} true if the bitrate has been successfully changed. + */ + setBitrate(value) { return this.player.setBitrate(value); } + _step(done) { if (this.pausedSince) { this._writeCallback = done; diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index fcf03d1ec..6d60f22d9 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -51,12 +51,13 @@ class AudioPlayer extends EventEmitter { * 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 + * @returns {boolean} true if the bitrate has been successfully changed. */ setBitrate(value) { - if (!value) return; - if (!this.opusEncoder) return; + if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false; const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value; - this.opusEncoder.setBitrate(bitrate); + this.streams.opus.setBitrate(bitrate * 1000); + return true; } playUnknownStream(stream, options = {}) { From 780e67d19f9da711d35b98a18bae7f212e41624e Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 16:30:55 +0100 Subject: [PATCH 013/154] Volume!! --- src/client/voice/VoiceBroadcast.js | 2 +- .../voice/dispatcher/StreamDispatcher.js | 13 ++++++++++++ src/client/voice/player/AudioPlayer.js | 4 ++-- src/client/voice/util/VolumeInterface.js | 21 ++++++++++++++++--- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 14cf2a186..b9a6f5117 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,4 +1,4 @@ -const VolumeInterface = require('./util/VolumeInterface'); +class VolumeInterface {} const OpusEncoders = require('./opus/OpusEngineList'); const Collection = require('../../util/Collection'); diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index e5de206bf..b4a8ab4b2 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -1,4 +1,5 @@ const { VoiceStatus } = require('../../../util/Constants'); +const VolumeInterface = require('../util/VolumeInterface'); const { Writable } = require('stream'); const secretbox = require('../util/Secretbox'); @@ -159,6 +160,18 @@ class StreamDispatcher extends Writable { */ this.emit('speaking', value); } + + get volume() { + return this.player.streams.volume ? this.player.streams.volume.volume : 1; + } + + setVolume(value) { + if (!this.player.streams.volume) return false; + this.player.streams.volume.setVolume(value); + return true; + } } +VolumeInterface.applyToClass(StreamDispatcher); + module.exports = StreamDispatcher; diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 6d60f22d9..eeb578a64 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -69,8 +69,9 @@ class AudioPlayer extends EventEmitter { playPCMStream(stream, options = {}) { this.destroyDispatcher(); + const volume = this.streams.volume = new prism.VolumeTransformer16LE(null, { volume: 0.2 }); const opus = this.streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); - stream.pipe(opus); + stream.pipe(volume).pipe(opus); return this.playOpusStream(opus, options); } @@ -85,7 +86,6 @@ class AudioPlayer extends EventEmitter { this.destroyDispatcher(); const options = { seek, volume, passes }; const dispatcher = new StreamDispatcher(this, options); - this.streamingData.count = 0; dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); return dispatcher; } diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index 7ecd28e64..8146bb913 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -25,7 +25,7 @@ class VolumeInterface extends EventEmitter { * @type {number} */ get volumeDecibels() { - return Math.log10(this._volume) * 20; + return Math.log10(this.volume) * 20; } /** @@ -34,7 +34,7 @@ class VolumeInterface extends EventEmitter { * @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 +83,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) + ); + } +}; From b83e12634c0aa49cc785bcfdcbe11f9acc910493 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 16:35:20 +0100 Subject: [PATCH 014/154] Docs --- src/client/voice/dispatcher/StreamDispatcher.js | 7 +++++++ src/client/voice/util/VolumeInterface.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index b4a8ab4b2..173e6576e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -161,6 +161,13 @@ class StreamDispatcher extends Writable { this.emit('speaking', value); } + get volumeEditable() { return Boolean(this.player.streams.volume); } + + /** + * Whether or not the Opus bitrate of this stream is editable + */ + get bitrateEditable() { return this.streams.opus && this.streams.opus.setBitrate; } + get volume() { return this.player.streams.volume ? this.player.streams.volume.volume : 1; } diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index 8146bb913..7c25aaa56 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -10,6 +10,13 @@ class VolumeInterface extends EventEmitter { this.setVolume(volume || 1); } + /** + * Whether or not the volume of this stream is editable + */ + get volumeEditable() { + return true; + } + /** * The current volume of the broadcast * @readonly From c8a75e4c29a35daaf031c23b65e372e47ff3e0d1 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 16:44:21 +0100 Subject: [PATCH 015/154] More volume docs --- src/client/voice/dispatcher/StreamDispatcher.js | 8 ++++++++ src/client/voice/util/VolumeInterface.js | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 173e6576e..81b08548a 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -168,6 +168,7 @@ class StreamDispatcher extends Writable { */ get bitrateEditable() { return this.streams.opus && this.streams.opus.setBitrate; } + // Volume get volume() { return this.player.streams.volume ? this.player.streams.volume.volume : 1; } @@ -177,6 +178,13 @@ class StreamDispatcher extends Writable { this.player.streams.volume.setVolume(value); return true; } + + // Volume stubs for docs + /* eslint-disable no-empty-function*/ + get volumeDecibels() {} + get volumeLogarithmic() {} + setVolumeDecibels() {} + setVolumeLogarithmic() {} } VolumeInterface.applyToClass(StreamDispatcher); diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index 7c25aaa56..7a2dfe15d 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -18,7 +18,7 @@ class VolumeInterface extends EventEmitter { } /** - * The current volume of the broadcast + * The current volume of the stream * @readonly * @type {number} */ @@ -27,7 +27,7 @@ class VolumeInterface extends EventEmitter { } /** - * The current volume of the broadcast in decibels + * The current volume of the stream in decibels * @readonly * @type {number} */ @@ -36,7 +36,7 @@ class VolumeInterface extends EventEmitter { } /** - * The current volume of the broadcast from a logarithmic scale + * The current volume of the stream from a logarithmic scale * @readonly * @type {number} */ From 116b4c3788d663bc3474824f884fb81148ad0b45 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 16:47:59 +0100 Subject: [PATCH 016/154] Rename unpause to resume --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 81b08548a..acb5b8f19 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -81,7 +81,7 @@ class StreamDispatcher extends Writable { /** * Resumes playback */ - unpause() { + resume() { this.pausedTime += Date.now() - this.pausedSince; this.pausedSince = null; if (this._writeCallback) this._writeCallback(); From a79c9ac11aea950a66cd1b95d68a7bd3e9568d1f Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 17:11:03 +0100 Subject: [PATCH 017/154] add: StreamDispatcher#pausedTime --- .../voice/dispatcher/StreamDispatcher.js | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index acb5b8f19..57ff9cb88 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -38,10 +38,14 @@ class StreamDispatcher extends Writable { this.player = player; this.streamOptions = streamOptions; + /** + * The time that the stream was paused at (null if not paused) + * @type {?number} + */ this.pausedSince = null; this._writeCallback = null; - this.pausedTime = 0; + this._pausedTime = 0; this.count = 0; this.on('error', this.destroy.bind(this)); @@ -78,15 +82,41 @@ class StreamDispatcher extends Writable { this.pausedSince = Date.now(); } + /** + * Whether or not playback is paused + */ + get paused() { return Boolean(this.pausedSince); } + + /** + * Total time that this dispatcher has been paused + */ + get pausedTime() { return this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0); } + /** * Resumes playback */ resume() { - this.pausedTime += Date.now() - this.pausedSince; + 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} + */ + get totalStreamTime() { + return Date.now() - this.startTime; + } + /** * Set the bitrate of the current Opus encoder. * @param {number} value New bitrate, in kbps From eeed5f23e37097817eb7f284ebdd93ba85f90097 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 17:53:10 +0100 Subject: [PATCH 018/154] Move streams to StreamDispatcher breaking: removed AudioPlayer#setBitrate --- .../voice/dispatcher/StreamDispatcher.js | 23 +++++++----- src/client/voice/player/AudioPlayer.js | 35 ++++++------------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 57ff9cb88..927fb1d49 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -29,7 +29,7 @@ nonce.fill(0); * @extends {stream.Writable} */ class StreamDispatcher extends Writable { - constructor(player, streamOptions) { + constructor(player, streamOptions, streams) { super(streamOptions); /** * The Audio Player that controls this dispatcher @@ -37,6 +37,7 @@ class StreamDispatcher extends Writable { */ this.player = player; this.streamOptions = streamOptions; + this.streams = streams; /** * The time that the stream was paused at (null if not paused) @@ -67,9 +68,8 @@ class StreamDispatcher extends Writable { } _destroy(err, cb) { - if (this.player.dispatcher !== this) return; - this.player.dispatcher = null; - const streams = this.player.streams; + 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); @@ -123,7 +123,12 @@ class StreamDispatcher extends Writable { * If set to 'auto', the voice channel's bitrate will be used * @returns {boolean} true if the bitrate has been successfully changed. */ - setBitrate(value) { return this.player.setBitrate(value); } + setBitrate(value) { + if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false; + const bitrate = value === 'auto' ? this.player.voiceConnection.channel.bitrate : value; + this.streams.opus.setBitrate(bitrate * 1000); + return true; + } _step(done) { if (this.pausedSince) { @@ -191,7 +196,7 @@ class StreamDispatcher extends Writable { this.emit('speaking', value); } - get volumeEditable() { return Boolean(this.player.streams.volume); } + get volumeEditable() { return Boolean(this.streams.volume); } /** * Whether or not the Opus bitrate of this stream is editable @@ -200,12 +205,12 @@ class StreamDispatcher extends Writable { // Volume get volume() { - return this.player.streams.volume ? this.player.streams.volume.volume : 1; + return this.streams.volume ? this.streams.volume.volume : 1; } setVolume(value) { - if (!this.player.streams.volume) return false; - this.player.streams.volume.setVolume(value); + if (!this.streams.volume) return false; + this.streams.volume.setVolume(value); return true; } diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index eeb578a64..a9b07a4d0 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -24,7 +24,6 @@ class AudioPlayer extends EventEmitter { */ this.voiceConnection = voiceConnection; - this.streams = {}; this.dispatcher = null; this.streamingData = { @@ -47,45 +46,33 @@ class AudioPlayer extends EventEmitter { } } - /** - * 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 - * @returns {boolean} true if the bitrate has been successfully changed. - */ - setBitrate(value) { - if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false; - const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value; - this.streams.opus.setBitrate(bitrate * 1000); - return true; - } - playUnknownStream(stream, options = {}) { this.destroyDispatcher(); - const ffmpeg = this.streams.ffmpeg = new prism.FFmpeg({ args: FFMPEG_ARGUMENTS }); + const ffmpeg = new prism.FFmpeg({ args: FFMPEG_ARGUMENTS }); stream.pipe(ffmpeg); - return this.playPCMStream(ffmpeg, options); + return this.playPCMStream(ffmpeg, options, { ffmpeg }); } - playPCMStream(stream, options = {}) { + playPCMStream(stream, options = {}, streams = {}) { this.destroyDispatcher(); - const volume = this.streams.volume = new prism.VolumeTransformer16LE(null, { volume: 0.2 }); - const opus = this.streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); + const volume = streams.volume = new prism.VolumeTransformer16LE(null, { volume: 0.2 }); + const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); stream.pipe(volume).pipe(opus); - return this.playOpusStream(opus, options); + return this.playOpusStream(opus, options, streams); } - playOpusStream(stream, options = {}) { + playOpusStream(stream, options = {}, streams = {}) { this.destroyDispatcher(); - const dispatcher = this.dispatcher = this.createDispatcher(options); + streams.opus = stream; + const dispatcher = this.dispatcher = this.createDispatcher(options, streams); stream.pipe(dispatcher); return dispatcher; } - createDispatcher({ seek = 0, volume = 1, passes = 1 } = {}) { + createDispatcher({ seek = 0, volume = 1, passes = 1 } = {}, streams) { this.destroyDispatcher(); const options = { seek, volume, passes }; - const dispatcher = new StreamDispatcher(this, options); + const dispatcher = new StreamDispatcher(this, options, streams); dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); return dispatcher; } From 45ef80b92f6e250653c89cefb13530e510e3a50b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 18:05:45 +0100 Subject: [PATCH 019/154] Fix docs --- src/client/voice/dispatcher/StreamDispatcher.js | 3 +++ src/client/voice/util/VolumeInterface.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 927fb1d49..5c2d1576f 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -84,11 +84,13 @@ class StreamDispatcher extends Writable { /** * 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); } @@ -200,6 +202,7 @@ class StreamDispatcher extends Writable { /** * Whether or not the Opus bitrate of this stream is editable + * @type {boolean} */ get bitrateEditable() { return this.streams.opus && this.streams.opus.setBitrate; } diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index 7a2dfe15d..5a03e6677 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -12,6 +12,7 @@ class VolumeInterface extends EventEmitter { /** * Whether or not the volume of this stream is editable + * @type {boolean} */ get volumeEditable() { return true; From 387d96bd6b509c370527cc4b76609e80b8a4d9fc Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 18:22:18 +0100 Subject: [PATCH 020/154] Add setPLP and setFEC --- .../voice/dispatcher/StreamDispatcher.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 5c2d1576f..9bb7e2bd6 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -120,7 +120,7 @@ class StreamDispatcher extends Writable { } /** - * Set the bitrate of the current Opus encoder. + * 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. @@ -132,6 +132,28 @@ class StreamDispatcher extends Writable { return true; } + /** + * Sets the expected packet loss percentage if using a compatible Opus stream. + * @param {number} value between 0 and 1 + * @returns {boolean} Returns true if it was successfully set. + */ + setPLP(value) { + if (!this.bitrateEditable) return false; + this.streams.opus.setPLP(value); + return true; + } + + /** + * Enables or disables forward error correction if using a compatible Opus stream. + * @param {boolean} enabled true to enable + * @returns {boolean} Returns true if it was successfully set. + */ + setFEC(enabled) { + if (!this.bitrateEditable) return false; + this.streams.opus.setPLP(enabled); + return true; + } + _step(done) { if (this.pausedSince) { this._writeCallback = done; From cc4aa75a7112dc3bc7b11cc0d65c820f16dee3cb Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 18:22:43 +0100 Subject: [PATCH 021/154] Fix closing bug --- src/client/voice/player/AudioPlayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index a9b07a4d0..cae69f099 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -32,7 +32,7 @@ class AudioPlayer extends EventEmitter { timestamp: 0, }; - this.voiceConnection.once('closing', () => this.destroyCurrentStream()); + this.voiceConnection.once('closing', () => this.destroy()); } destroy() { From 6490d1b911a8922ca048a1d48a71206cebd78ed1 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 18:36:04 +0100 Subject: [PATCH 022/154] FEC and PLP exposed --- src/client/voice/VoiceConnection.js | 4 +++- src/client/voice/dispatcher/StreamDispatcher.js | 8 +++++++- src/client/voice/player/AudioPlayer.js | 9 ++++----- test/voice.js | 10 +++++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 9465caa95..42b7fabe0 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -434,7 +434,9 @@ class VoiceConnection extends EventEmitter { * @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. + * @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 */ diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 9bb7e2bd6..82f30b4dc 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -29,7 +29,8 @@ nonce.fill(0); * @extends {stream.Writable} */ class StreamDispatcher extends Writable { - constructor(player, streamOptions, streams) { + constructor(player, { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96 } = {}, streams) { + const streamOptions = { seek, volume, passes, fec, plp, bitrate }; super(streamOptions); /** * The Audio Player that controls this dispatcher @@ -55,6 +56,11 @@ class StreamDispatcher extends Writable { // 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); } get _sdata() { diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index cae69f099..8ef3d7673 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -46,14 +46,14 @@ class AudioPlayer extends EventEmitter { } } - playUnknownStream(stream, options = {}) { + playUnknownStream(stream, options) { this.destroyDispatcher(); const ffmpeg = new prism.FFmpeg({ args: FFMPEG_ARGUMENTS }); stream.pipe(ffmpeg); return this.playPCMStream(ffmpeg, options, { ffmpeg }); } - playPCMStream(stream, options = {}, streams = {}) { + playPCMStream(stream, options, streams = {}) { this.destroyDispatcher(); const volume = streams.volume = new prism.VolumeTransformer16LE(null, { volume: 0.2 }); const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); @@ -61,7 +61,7 @@ class AudioPlayer extends EventEmitter { return this.playOpusStream(opus, options, streams); } - playOpusStream(stream, options = {}, streams = {}) { + playOpusStream(stream, options, streams = {}) { this.destroyDispatcher(); streams.opus = stream; const dispatcher = this.dispatcher = this.createDispatcher(options, streams); @@ -69,9 +69,8 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - createDispatcher({ seek = 0, volume = 1, passes = 1 } = {}, streams) { + createDispatcher(options, streams) { this.destroyDispatcher(); - const options = { seek, volume, passes }; const dispatcher = new StreamDispatcher(this, options, streams); dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); return dispatcher; diff --git a/test/voice.js b/test/voice.js index c88cbed31..60f923278 100644 --- a/test/voice.js +++ b/test/voice.js @@ -14,7 +14,7 @@ client.login(auth.token).then(() => console.log('logged')).catch(console.error); const connections = new Map(); -let broadcast; +var d; client.on('debug', console.log); client.on('error', console.log); @@ -29,9 +29,7 @@ client.on('message', m => { conn.player.on('error', (...e) => console.log('player', ...e)); if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); - const d = conn.playOpusStream( - fs.createReadStream('C:/users/amish/downloads/s.ogg').pipe(new prism.OggOpusDemuxer()) - ); + d = conn.playStream(ytdl('https://www.youtube.com/watch?v=EUoe7cf0HYw', { filter: 'audioonly' }, { passes: 3 })); }); } else { m.reply('Specify a voice channel!'); @@ -42,7 +40,9 @@ client.on('message', m => { .replace(//g, ''); const stream = ytdl(url, { filter: 'audioonly' }, { passes: 3 }); - m.guild.voiceConnection.playStream(stream); + d = m.guild.voiceConnection.playStream(stream); + d.setBitrate(1); + setTimeout(() => d.setBitrate(320), 5000); } } else if (m.content.startsWith('/skip')) { if (connections.has(m.guild.id)) { From 3696b4a8109054f50571c1e1dda418e242875759 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 18:56:02 +0100 Subject: [PATCH 023/154] Add ability to disable volume transform --- src/client/voice/VoiceConnection.js | 3 ++- src/client/voice/player/AudioPlayer.js | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 42b7fabe0..4fca12952 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -432,7 +432,8 @@ class VoiceConnection extends EventEmitter { * 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|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 diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 8ef3d7673..a681f3a95 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -55,8 +55,12 @@ class AudioPlayer extends EventEmitter { playPCMStream(stream, options, streams = {}) { this.destroyDispatcher(); - const volume = streams.volume = new prism.VolumeTransformer16LE(null, { volume: 0.2 }); const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); + if (options && options.volume === false) { + stream.pipe(opus); + return this.playOpusStream(opus, options, streams); + } + const volume = streams.volume = new prism.VolumeTransformer16LE(null, { volume: options ? options.volume : 1 }); stream.pipe(volume).pipe(opus); return this.playOpusStream(opus, options, streams); } From bdf8955098e203773896947c79c6afe0cb428b22 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 21:00:53 +0100 Subject: [PATCH 024/154] "yeah we need voice broadcasts cause we make big big music bots" no stop --- src/client/voice/VoiceBroadcast.js | 348 +++--------------- src/client/voice/VoiceConnection.js | 2 + .../voice/dispatcher/BroadcastDispatcher.js | 38 ++ .../voice/dispatcher/StreamDispatcher.js | 13 +- src/client/voice/player/AudioPlayer.js | 67 +--- src/client/voice/player/BasePlayer.js | 76 ++++ .../voice/player/BroadcastAudioPlayer.js | 27 ++ 7 files changed, 200 insertions(+), 371 deletions(-) create mode 100644 src/client/voice/dispatcher/BroadcastDispatcher.js create mode 100644 src/client/voice/player/BasePlayer.js create mode 100644 src/client/voice/player/BroadcastAudioPlayer.js diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index b9a6f5117..dadbbc5ef 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,14 +1,5 @@ -class VolumeInterface {} -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'); /** * A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency. @@ -24,7 +15,7 @@ const ffmpegArguments = [ * ``` * @implements {VolumeInterface} */ -class VoiceBroadcast extends VolumeInterface { +class VoiceBroadcast extends EventEmitter { constructor(client) { super(); /** @@ -32,119 +23,8 @@ 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 current audio transcoder that is being used - * @type {Object} - */ - this.currentTranscoder = null; - this.tickInterval = null; - this._volume = 1; - } - - /** - * An array of subscribed dispatchers - * @type {StreamDispatcher[]} - * @readonly - */ - 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'); - } - } - - /** - * 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} - * @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); + this.dispatchers = []; + this.player = new BroadcastAudioPlayer(this); } /** @@ -154,66 +34,54 @@ class VoiceBroadcast extends VolumeInterface { * @returns {StreamDispatcher} * @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); + * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); * }) * .catch(console.error); */ - playFile(file, options = {}) { - this.setVolume(options.volume || 1); - return this._playTranscodable(`file:${file}`, options); + playFile(file, options) { + return this.player.playUnknownStream(`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 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 {VoiceBroadcast} + * @returns {StreamDispatcher} */ - playConvertedStream(stream, options = {}) { - this.killCurrentTranscoder(); - this.setVolume(options.volume || 1); - this.currentTranscoder = { options: { stream } }; - stream.once('readable', () => this._startPlaying()); - return this; + playConvertedStream(stream, options) { + return this.player.playPCMStream(stream, options); } /** @@ -223,142 +91,8 @@ class VoiceBroadcast extends VolumeInterface { * @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'); - } - } + playOpusStream(stream, options) { + return this.player.playOpusStream(stream, options); } } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 4fca12952..8cea545f4 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -89,6 +89,8 @@ class VoiceConnection extends EventEmitter { this.emit('warn', e); }); + this.once('closing', () => this.player.destroy()); + /** * Map SSRC to speaking values * @type {Map} diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js new file mode 100644 index 000000000..902935cc4 --- /dev/null +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -0,0 +1,38 @@ +const Collection = require('../../../util/Collection'); +const StreamDispatcher = require('./StreamDispatcher'); + +/** + * The class that sends voice packet data to the voice connection. + * @implements {VolumeInterface} + */ +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; diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 82f30b4dc..8a6cb75df 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -47,6 +47,12 @@ class StreamDispatcher extends Writable { this.pausedSince = null; this._writeCallback = null; + /** + * The broadcast controlling this dispatcher, if any + * @type {?VoiceBroadcast} + */ + this.broadcast = this.streams.broadcast; + this._pausedTime = 0; this.count = 0; @@ -165,8 +171,10 @@ class StreamDispatcher extends Writable { this._writeCallback = done; return; } - const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime); - setTimeout(done.bind(this), next); + if (!this.streams.broadcast) { + const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime); + setTimeout(done.bind(this), next); + } if (this._sdata.sequence === (2 ** 16) - 1) this._sdata.sequence = -1; if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -TIMESTAMP_INC; this._sdata.sequence++; @@ -218,6 +226,7 @@ class StreamDispatcher extends Writable { 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 diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index a681f3a95..954b0b914 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -1,21 +1,13 @@ -const EventEmitter = require('events').EventEmitter; const prism = require('prism-media'); const StreamDispatcher = require('../dispatcher/StreamDispatcher'); - -const FFMPEG_ARGUMENTS = [ - '-analyzeduration', '0', - '-loglevel', '0', - '-f', 's16le', - '-ar', '48000', - '-ac', '2', -]; +const BasePlayer = require('./BasePlayer'); /** * An Audio Player for a Voice Connection. * @private * @extends {EventEmitter} */ -class AudioPlayer extends EventEmitter { +class AudioPlayer extends BasePlayer { constructor(voiceConnection) { super(); /** @@ -23,60 +15,11 @@ class AudioPlayer extends EventEmitter { * @type {VoiceConnection} */ this.voiceConnection = voiceConnection; - - this.dispatcher = null; - - this.streamingData = { - channels: 2, - sequence: 0, - timestamp: 0, - }; - - this.voiceConnection.once('closing', () => this.destroy()); } - destroy() { - this.destroyDispatcher(); - } - - destroyDispatcher() { - if (this.dispatcher) { - this.dispatcher.destroy(); - this.dispatcher = null; - } - } - - playUnknownStream(stream, options) { - this.destroyDispatcher(); - const ffmpeg = new prism.FFmpeg({ args: FFMPEG_ARGUMENTS }); - stream.pipe(ffmpeg); - return this.playPCMStream(ffmpeg, options, { ffmpeg }); - } - - playPCMStream(stream, options, streams = {}) { - this.destroyDispatcher(); - const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); - if (options && options.volume === false) { - stream.pipe(opus); - return this.playOpusStream(opus, options, streams); - } - 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.dispatcher = this.createDispatcher(options, streams); - stream.pipe(dispatcher); - return dispatcher; - } - - createDispatcher(options, streams) { - this.destroyDispatcher(); - const dispatcher = new StreamDispatcher(this, options, streams); - dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); + playBroadcast(broadcast, options) { + const dispatcher = this.createDispatcher(options, { broadcast }); + broadcast.dispatchers.push(dispatcher); return dispatcher; } } diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js new file mode 100644 index 000000000..45009c20f --- /dev/null +++ b/src/client/voice/player/BasePlayer.js @@ -0,0 +1,76 @@ +const EventEmitter = require('events').EventEmitter; +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(voiceConnection) { + 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; + } + } + + playUnknownStream(stream, options) { + this.destroyDispatcher(); + const ffmpeg = new prism.FFmpeg({ args: FFMPEG_ARGUMENTS }); + stream.pipe(ffmpeg); + return this.playPCMStream(ffmpeg, options, { ffmpeg }); + } + + playPCMStream(stream, options, streams = {}) { + this.destroyDispatcher(); + const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); + if (options && options.volume === false) { + stream.pipe(opus); + return this.playOpusStream(opus, options, streams); + } + 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; diff --git a/src/client/voice/player/BroadcastAudioPlayer.js b/src/client/voice/player/BroadcastAudioPlayer.js new file mode 100644 index 000000000..41441276d --- /dev/null +++ b/src/client/voice/player/BroadcastAudioPlayer.js @@ -0,0 +1,27 @@ +const prism = require('prism-media'); +const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher'); +const BasePlayer = require('./BasePlayer'); + +/** + * An Audio Player for a Voice Connection. + * @private + * @extends {EventEmitter} + */ +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 = new BroadcastDispatcher(this, options, streams); + return dispatcher; + } +} + +module.exports = AudioPlayer; From 393130dedbcfce684700aeee69f293c159ce90a6 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 21:26:00 +0100 Subject: [PATCH 025/154] things --- test/voice.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/voice.js b/test/voice.js index 60f923278..8cdc9dc70 100644 --- a/test/voice.js +++ b/test/voice.js @@ -14,7 +14,7 @@ client.login(auth.token).then(() => console.log('logged')).catch(console.error); const connections = new Map(); -var d; +var d, b; client.on('debug', console.log); client.on('error', console.log); @@ -29,7 +29,8 @@ client.on('message', m => { conn.player.on('error', (...e) => console.log('player', ...e)); if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); - d = conn.playStream(ytdl('https://www.youtube.com/watch?v=EUoe7cf0HYw', { filter: 'audioonly' }, { passes: 3 })); + // conn.playOpusStream(fs.createReadStream('C:/users/amish/downloads/z.ogg').pipe(new prism.OggOpusDemuxer())); + d = conn.playStream(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); }); } else { m.reply('Specify a voice channel!'); @@ -59,6 +60,11 @@ client.on('message', m => { console.log(e); m.channel.send(e, { code: true }); } + } else if (m.content === 'mb') { + b = client.createVoiceBroadcast(); + b.playStream(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); + } else if (m.content === 'subscribe!') { + m.guild.voiceConnection.playBroadcast(b); } }); From a8511ebfafdc8b19d25e1a6f0ce738392ea38535 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 26 Oct 2017 21:50:52 +0100 Subject: [PATCH 026/154] Fix player --- src/client/voice/player/BasePlayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index 45009c20f..bb0de6bb7 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -48,7 +48,7 @@ class BasePlayer extends EventEmitter { playPCMStream(stream, options, streams = {}) { this.destroyDispatcher(); - const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 }); + const opus = streams.opus = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 1920 }); if (options && options.volume === false) { stream.pipe(opus); return this.playOpusStream(opus, options, streams); From 8a87cbf404dfbfed4466c5f77c8ea464ad3c03ce Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 27 Oct 2017 14:32:02 +0100 Subject: [PATCH 027/154] Better broadcast stuff --- src/client/voice/VoiceBroadcast.js | 2 +- src/client/voice/dispatcher/StreamDispatcher.js | 5 +++-- src/client/voice/player/AudioPlayer.js | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index dadbbc5ef..41aa1a082 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -23,7 +23,7 @@ class VoiceBroadcast extends EventEmitter { * @type {Client} */ this.client = client; - this.dispatchers = []; + this.dispatchers = new Set(); this.player = new BroadcastAudioPlayer(this); } diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 8a6cb75df..eae032c40 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -29,8 +29,8 @@ nonce.fill(0); * @extends {stream.Writable} */ class StreamDispatcher extends Writable { - constructor(player, { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96 } = {}, streams) { - const streamOptions = { seek, volume, passes, fec, plp, bitrate }; + constructor(player, { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 4096 } = {}, streams) { + const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark }; super(streamOptions); /** * The Audio Player that controls this dispatcher @@ -82,6 +82,7 @@ class StreamDispatcher extends Writable { _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); diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 954b0b914..93394b1c8 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -1,5 +1,3 @@ -const prism = require('prism-media'); -const StreamDispatcher = require('../dispatcher/StreamDispatcher'); const BasePlayer = require('./BasePlayer'); /** @@ -19,7 +17,7 @@ class AudioPlayer extends BasePlayer { playBroadcast(broadcast, options) { const dispatcher = this.createDispatcher(options, { broadcast }); - broadcast.dispatchers.push(dispatcher); + broadcast.dispatchers.add(dispatcher); return dispatcher; } } From dbf4ef9a7c1cbbb8fdb9f5519d6415c96151176f Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Fri, 27 Oct 2017 14:42:21 -0700 Subject: [PATCH 028/154] handle string ffmpeg input (#2064) * handle string ffmpeg input * rename stuff for new purpose * file prefix isn't needed * pass tests * remove dumb spaces in dispatcher docs --- src/client/voice/VoiceBroadcast.js | 6 +++--- src/client/voice/VoiceConnection.js | 6 +++--- src/client/voice/dispatcher/BroadcastDispatcher.js | 1 - src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- src/client/voice/player/BasePlayer.js | 13 +++++++++---- src/client/voice/player/BroadcastAudioPlayer.js | 1 - typings | 2 +- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 41aa1a082..c709eb64c 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -41,7 +41,7 @@ class VoiceBroadcast extends EventEmitter { * .catch(console.error); */ playFile(file, options) { - return this.player.playUnknownStream(`file:${file}`, options); + return this.player.playUnknown(file, options); } /** @@ -51,7 +51,7 @@ class VoiceBroadcast extends EventEmitter { * @returns {StreamDispatcher} */ playArbitraryInput(input, options) { - return this.player.playUnknownStream(input, options); + return this.player.playUnknown(input, options); } /** @@ -71,7 +71,7 @@ class VoiceBroadcast extends EventEmitter { * .catch(console.error); */ playStream(stream, options) { - return this.player.playUnknownStream(stream, options); + return this.player.playUnknown(stream, options); } /** diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 8cea545f4..a270e1039 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -457,7 +457,7 @@ class VoiceConnection extends EventEmitter { * .catch(console.error); */ playFile(file, options) { - return this.player.playUnknownStream(`file:${file}`, options); + return this.player.playUnknown(file, options); } /** @@ -467,7 +467,7 @@ class VoiceConnection extends EventEmitter { * @returns {StreamDispatcher} */ playArbitraryInput(input, options) { - return this.player.playUnknownStream(input, options); + return this.player.playUnknown(input, options); } /** @@ -487,7 +487,7 @@ class VoiceConnection extends EventEmitter { * .catch(console.error); */ playStream(stream, options) { - return this.player.playUnknownStream(stream, options); + return this.player.playUnknown(stream, options); } /** diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index 902935cc4..330d05292 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -1,4 +1,3 @@ -const Collection = require('../../../util/Collection'); const StreamDispatcher = require('./StreamDispatcher'); /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index eae032c40..63f352304 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -147,7 +147,7 @@ class StreamDispatcher extends Writable { /** * Sets the expected packet loss percentage if using a compatible Opus stream. - * @param {number} value between 0 and 1 + * @param {number} value between 0 and 1 * @returns {boolean} Returns true if it was successfully set. */ setPLP(value) { @@ -158,7 +158,7 @@ class StreamDispatcher extends Writable { /** * Enables or disables forward error correction if using a compatible Opus stream. - * @param {boolean} enabled true to enable + * @param {boolean} enabled true to enable * @returns {boolean} Returns true if it was successfully set. */ setFEC(enabled) { diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index bb0de6bb7..59cfef916 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -1,4 +1,5 @@ const EventEmitter = require('events').EventEmitter; +const { Readable: ReadableStream } = require('stream'); const prism = require('prism-media'); const StreamDispatcher = require('../dispatcher/StreamDispatcher'); @@ -16,7 +17,7 @@ const FFMPEG_ARGUMENTS = [ * @extends {EventEmitter} */ class BasePlayer extends EventEmitter { - constructor(voiceConnection) { + constructor() { super(); this.dispatcher = null; @@ -39,10 +40,14 @@ class BasePlayer extends EventEmitter { } } - playUnknownStream(stream, options) { + playUnknown(input, options) { this.destroyDispatcher(); - const ffmpeg = new prism.FFmpeg({ args: FFMPEG_ARGUMENTS }); - stream.pipe(ffmpeg); + + const isStream = input instanceof ReadableStream; + const args = isStream ? FFMPEG_ARGUMENTS : ['-i', input, ...FFMPEG_ARGUMENTS]; + const ffmpeg = new prism.FFmpeg({ args }); + if (isStream) input.pipe(ffmpeg); + return this.playPCMStream(ffmpeg, options, { ffmpeg }); } diff --git a/src/client/voice/player/BroadcastAudioPlayer.js b/src/client/voice/player/BroadcastAudioPlayer.js index 41441276d..8a9071692 100644 --- a/src/client/voice/player/BroadcastAudioPlayer.js +++ b/src/client/voice/player/BroadcastAudioPlayer.js @@ -1,4 +1,3 @@ -const prism = require('prism-media'); const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher'); const BasePlayer = require('./BasePlayer'); diff --git a/typings b/typings index 697fc933d..5131e88ff 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 697fc933de90209b81b69bd0fe87883e3c7a217d +Subproject commit 5131e88ffe0b61c2f69318e53e54a3e3edec6f1e From d4a9e5ec9cd69478fad05c945d0ff28d1c42cc35 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 29 Oct 2017 13:19:38 +0000 Subject: [PATCH 029/154] Fix ESLint and handle stream errors --- .../voice/dispatcher/BroadcastDispatcher.js | 1 - .../voice/dispatcher/StreamDispatcher.js | 13 +++- src/client/voice/player/BasePlayer.js | 2 +- .../voice/player/BroadcastAudioPlayer.js | 1 - test/voice.js | 71 ++++++++----------- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index 902935cc4..330d05292 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -1,4 +1,3 @@ -const Collection = require('../../../util/Collection'); const StreamDispatcher = require('./StreamDispatcher'); /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index eae032c40..ce88524be 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -29,8 +29,8 @@ nonce.fill(0); * @extends {stream.Writable} */ class StreamDispatcher extends Writable { - constructor(player, { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 4096 } = {}, streams) { - const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark }; + constructor(player, { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96 } = {}, streams) { + const streamOptions = { seek, volume, passes, fec, plp, bitrate }; super(streamOptions); /** * The Audio Player that controls this dispatcher @@ -67,6 +67,15 @@ class StreamDispatcher extends Writable { if (typeof fec !== 'undefined') this.setFEC(fec); if (typeof plp !== 'undefined') this.setPLP(plp); if (typeof bitrate !== 'undefined') this.setBitrate(bitrate); + + const streamError = err => { + this.emit('warn', err); + this.destroy(); + }; + + if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', streamError); + if (this.streams.opus) this.streams.opus.on('error', streamError); + if (this.streams.volume) this.streams.volume.on('error', streamError); } get _sdata() { diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index bb0de6bb7..5beb5b033 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -16,7 +16,7 @@ const FFMPEG_ARGUMENTS = [ * @extends {EventEmitter} */ class BasePlayer extends EventEmitter { - constructor(voiceConnection) { + constructor() { super(); this.dispatcher = null; diff --git a/src/client/voice/player/BroadcastAudioPlayer.js b/src/client/voice/player/BroadcastAudioPlayer.js index 41441276d..8a9071692 100644 --- a/src/client/voice/player/BroadcastAudioPlayer.js +++ b/src/client/voice/player/BroadcastAudioPlayer.js @@ -1,4 +1,3 @@ -const prism = require('prism-media'); const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher'); const BasePlayer = require('./BasePlayer'); diff --git a/test/voice.js b/test/voice.js index 8cdc9dc70..29e1eacc9 100644 --- a/test/voice.js +++ b/test/voice.js @@ -1,6 +1,11 @@ /* eslint no-console: 0 */ 'use strict'; +var profiler = require('gc-profiler'); +profiler.on('gc', function (info) { + console.log(info); +}); + const Discord = require('../'); const ytdl = require('ytdl-core'); const prism = require('prism-media'); @@ -19,6 +24,32 @@ var d, b; client.on('debug', console.log); client.on('error', console.log); +async function wait(time = 1000) { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +} + +var count = 0; + +client.on('ready', async () => { + for (const guild of client.guilds.values()) { + const channels = guild.channels.filter(c => c.type === 'voice' && c.joinable && c.members.size === 0); + const channel = channels.first(); + if (channel) { + channel.join().then(conn => { + conn.playOpusStream(fs.createReadStream('C:/users/amish/downloads/z.ogg').pipe(new prism.OggOpusDemuxer())); + }); + count++; + console.log(`Playing in ${channel.name} in ${channel.guild.name} at count ${count} ${process.memoryUsage().rss / (1024 * 1024)}`); + await wait(); + } + } + console.log('done!'); +}); + +process.on('unhandledRejection', console.log); + client.on('message', m => { if (!m.guild) return; if (m.author.id !== '66564597481480192') return; @@ -35,23 +66,6 @@ client.on('message', m => { } else { m.reply('Specify a voice channel!'); } - } else if (m.content.startsWith('/play')) { - if (connections.has(m.guild.id)) { - const url = m.content.split(' ').slice(1).join(' ') - .replace(//g, ''); - const stream = ytdl(url, { filter: 'audioonly' }, { passes: 3 }); - d = m.guild.voiceConnection.playStream(stream); - d.setBitrate(1); - setTimeout(() => d.setBitrate(320), 5000); - } - } else if (m.content.startsWith('/skip')) { - if (connections.has(m.guild.id)) { - const connData = connections.get(m.guild.id); - if (connData.dispatcher) { - connData.dispatcher.end(); - } - } } else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') { try { const com = eval(m.content.split(' ').slice(1).join(' ')); @@ -60,28 +74,5 @@ client.on('message', m => { console.log(e); m.channel.send(e, { code: true }); } - } else if (m.content === 'mb') { - b = client.createVoiceBroadcast(); - b.playStream(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); - } else if (m.content === 'subscribe!') { - m.guild.voiceConnection.playBroadcast(b); } }); - -function doQueue(connData) { - const conn = connData.conn; - const queue = connData.queue; - const item = queue[0]; - if (!item) return; - const stream = ytdl(item.url, { filter: 'audioonly' }, { passes: 3 }); - const dispatcher = conn.playStream(stream); - stream.on('info', info => { - item.m.reply(`OK, playing **${info.title}**`); - }); - dispatcher.on('end', () => { - queue.shift(); - doQueue(connData); - }); - dispatcher.on('error', (...e) => console.log('dispatcher', ...e)); - connData.dispatcher = dispatcher; -} From 5cb757add6137c65419986c2b420729db922641d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 29 Oct 2017 13:48:07 +0000 Subject: [PATCH 030/154] Add highWaterMark --- src/client/voice/VoiceConnection.js | 5 ++++- src/client/voice/dispatcher/StreamDispatcher.js | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index a270e1039..1f1c7a18a 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -439,8 +439,11 @@ class VoiceConnection extends EventEmitter { * @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 + * @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=8] 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. */ /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 0c8639634..84500bcce 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -29,8 +29,11 @@ nonce.fill(0); * @extends {stream.Writable} */ class StreamDispatcher extends Writable { - constructor(player, { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96 } = {}, streams) { - const streamOptions = { seek, volume, passes, fec, plp, bitrate }; + constructor( + player, + { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 8 } = {}, + streams) { + const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark }; super(streamOptions); /** * The Audio Player that controls this dispatcher From cf30b1ef08120de924638eb7a51eab0436ad6cc2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 29 Oct 2017 17:43:22 +0000 Subject: [PATCH 031/154] Add @extends to some classes --- src/client/voice/dispatcher/BroadcastDispatcher.js | 1 + src/client/voice/player/AudioPlayer.js | 2 +- src/client/voice/player/BroadcastAudioPlayer.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index 330d05292..90cff6a40 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -3,6 +3,7 @@ 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) { diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 93394b1c8..e3381f5cc 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -3,7 +3,7 @@ const BasePlayer = require('./BasePlayer'); /** * An Audio Player for a Voice Connection. * @private - * @extends {EventEmitter} + * @extends {BasePlayer} */ class AudioPlayer extends BasePlayer { constructor(voiceConnection) { diff --git a/src/client/voice/player/BroadcastAudioPlayer.js b/src/client/voice/player/BroadcastAudioPlayer.js index 8a9071692..a26f90efc 100644 --- a/src/client/voice/player/BroadcastAudioPlayer.js +++ b/src/client/voice/player/BroadcastAudioPlayer.js @@ -4,7 +4,7 @@ const BasePlayer = require('./BasePlayer'); /** * An Audio Player for a Voice Connection. * @private - * @extends {EventEmitter} + * @extends {BasePlayer} */ class AudioPlayer extends BasePlayer { constructor(broadcast) { From 2e1a28a6eeb65b38b381038aa60197583a143ac7 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 29 Oct 2017 18:04:36 +0000 Subject: [PATCH 032/154] Use destroy and end in apt places --- src/client/voice/dispatcher/StreamDispatcher.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 84500bcce..20650c128 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -59,9 +59,9 @@ class StreamDispatcher extends Writable { this._pausedTime = 0; this.count = 0; - this.on('error', this.destroy.bind(this)); + this.on('error', err => this.destroy(err)); this.on('finish', () => { - this.destroy.bind(this); + this.end.bind(this); // Still emitting end for backwards compatibility, probably remove it in the future! this.emit('end'); }); @@ -73,7 +73,7 @@ class StreamDispatcher extends Writable { const streamError = err => { this.emit('warn', err); - this.destroy(); + this.destroy(err); }; if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', streamError); From fa7f391b3aac0a2a0a4313c97e24fc05a13e8f43 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 31 Oct 2017 18:12:49 +0000 Subject: [PATCH 033/154] Fix ECONNRESET (again) --- src/client/voice/dispatcher/StreamDispatcher.js | 6 +++--- src/client/voice/player/BasePlayer.js | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 20650c128..1b24fae3a 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -59,9 +59,7 @@ class StreamDispatcher extends Writable { this._pausedTime = 0; this.count = 0; - this.on('error', err => this.destroy(err)); this.on('finish', () => { - this.end.bind(this); // Still emitting end for backwards compatibility, probably remove it in the future! this.emit('end'); }); @@ -73,9 +71,11 @@ class StreamDispatcher extends Writable { const streamError = err => { this.emit('warn', err); - this.destroy(err); + this.destroy(); }; + this.on('error', streamError); + if (this.streams.input) this.streams.input.on('error', streamError); if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', streamError); if (this.streams.opus) this.streams.opus.on('error', streamError); if (this.streams.volume) this.streams.volume.on('error', streamError); diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index 59cfef916..fac2b68be 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -46,9 +46,12 @@ class BasePlayer extends EventEmitter { const isStream = input instanceof ReadableStream; const args = isStream ? FFMPEG_ARGUMENTS : ['-i', input, ...FFMPEG_ARGUMENTS]; const ffmpeg = new prism.FFmpeg({ args }); - if (isStream) input.pipe(ffmpeg); - - return this.playPCMStream(ffmpeg, options, { ffmpeg }); + const streams = { ffmpeg }; + if (isStream) { + streams.input = input; + input.pipe(ffmpeg); + } + return this.playPCMStream(ffmpeg, options, streams); } playPCMStream(stream, options, streams = {}) { From 121a40bb4a38a28fa1b6356cb99b7217d35c29da Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 31 Oct 2017 19:43:58 +0000 Subject: [PATCH 034/154] Use passes and increase highWaterMark to 12 --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 1b24fae3a..3ff2aaee7 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -31,7 +31,7 @@ nonce.fill(0); class StreamDispatcher extends Writable { constructor( player, - { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 8 } = {}, + { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, streams) { const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark }; super(streamOptions); @@ -219,7 +219,7 @@ class StreamDispatcher extends Writable { } _sendPacket(packet) { - let repeats = 1; + let repeats = this.streamOptions.passes; /** * Emitted whenever the dispatcher has debug information. * @event StreamDispatcher#debug From 995cd181c946a27c11247b2a96dc1708b54c53da Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 31 Oct 2017 19:47:58 +0000 Subject: [PATCH 035/154] Fix setFEC bug and use bitrateEditable in setBitrate --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 3ff2aaee7..4f005460e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -151,7 +151,7 @@ class StreamDispatcher extends Writable { * @returns {boolean} true if the bitrate has been successfully changed. */ setBitrate(value) { - if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false; + if (!value || !this.bitrateEditable) return false; const bitrate = value === 'auto' ? this.player.voiceConnection.channel.bitrate : value; this.streams.opus.setBitrate(bitrate * 1000); return true; @@ -175,7 +175,7 @@ class StreamDispatcher extends Writable { */ setFEC(enabled) { if (!this.bitrateEditable) return false; - this.streams.opus.setPLP(enabled); + this.streams.opus.setFEC(enabled); return true; } From 8efafb6a43ba114814537cdbbe3d34d45983f5c0 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 31 Oct 2017 19:50:02 +0000 Subject: [PATCH 036/154] StreamDispatcher documented as extending WritableStream, not stream.Writable --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 4f005460e..0214ca1c8 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -12,7 +12,7 @@ const nonce = Buffer.alloc(24); nonce.fill(0); /** - * @external Stream.writable + * @external WritableStream * @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable} */ @@ -26,7 +26,7 @@ nonce.fill(0); * }); * ``` * @implements {VolumeInterface} - * @extends {stream.Writable} + * @extends {WritableStream} */ class StreamDispatcher extends Writable { constructor( From 6a523ba96a596c66302188e46d95eed9628d7cf4 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 3 Nov 2017 20:01:51 +0000 Subject: [PATCH 037/154] Reimplement StreamDispatcher error and start event in docs, emit error instead of debug if there are listeners for errors --- src/client/voice/dispatcher/StreamDispatcher.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 0214ca1c8..8f08170a3 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -70,7 +70,11 @@ class StreamDispatcher extends Writable { if (typeof bitrate !== 'undefined') this.setBitrate(bitrate); const streamError = err => { - this.emit('warn', err); + /** + * Emitted when the dispatcher encounters an error. + * @event StreamDispatcher#error + */ + this.emit(this.listenerCount('error') > 0 ? 'error' : 'warn', err); this.destroy(); }; @@ -86,7 +90,14 @@ class StreamDispatcher extends Writable { } _write(chunk, enc, done) { - if (!this.startTime) this.startTime = Date.now(); + 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); } From 2531065bbda5a1e9db66299523b88e2c6b6f1c3b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 4 Nov 2017 15:02:58 +0000 Subject: [PATCH 038/154] Fix loop and only emit error, not warn --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 8f08170a3..7c6b98c3e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -74,11 +74,11 @@ class StreamDispatcher extends Writable { * Emitted when the dispatcher encounters an error. * @event StreamDispatcher#error */ - this.emit(this.listenerCount('error') > 0 ? 'error' : 'warn', err); + if (err) this.emit('error', err); this.destroy(); }; - this.on('error', streamError); + this.on('error', () => streamError()); if (this.streams.input) this.streams.input.on('error', streamError); if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', streamError); if (this.streams.opus) this.streams.opus.on('error', streamError); From 20689a51a1c37ff7b7edaff357621640d68bac67 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 2 Dec 2017 17:02:17 -0500 Subject: [PATCH 039/154] Add exports for new util functions --- src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.js b/src/index.js index 244c6a5ea..42de34564 100644 --- a/src/index.js +++ b/src/index.js @@ -37,8 +37,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 From 443961ce43bc37dbe8705e42a28afcfaf8e94a7d Mon Sep 17 00:00:00 2001 From: Isabella Date: Sun, 10 Dec 2017 14:22:59 -0600 Subject: [PATCH 040/154] fix(CreateMessage): fix attachment and array sending (#2158) --- src/structures/shared/CreateMessage.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js index 5abf0799e..8ae2c369f 100644 --- a/src/structures/shared/CreateMessage.js +++ b/src/structures/shared/CreateMessage.js @@ -19,9 +19,22 @@ 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}>`; @@ -29,8 +42,8 @@ module.exports = async function createMessage(channel, options) { options.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)) { From fe9ea02f8bc9d025012ee8c42fc51360f0b4e2ec Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 11 Dec 2017 22:02:53 -0600 Subject: [PATCH 041/154] Make all VoiceChannel bitrates in bps (#2165) * Make all VoiceChannel bitrates in bps instead of mixed kbps/bps * fix edit method in GuildChannel too --- src/structures/GuildChannel.js | 2 +- src/structures/VoiceChannel.js | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index a84ad0847..fa0f8f844 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -292,7 +292,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, diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 2fa37d233..05b840444 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -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} * @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); } From 1598efa0b8a22d6ca50e10bd15c215fcef27fcb5 Mon Sep 17 00:00:00 2001 From: Isabella Date: Mon, 11 Dec 2017 23:33:10 -0600 Subject: [PATCH 042/154] fix(CreateMessage): reassigning wrong content (#2169) --- src/structures/shared/CreateMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js index 8ae2c369f..d0fa35726 100644 --- a/src/structures/shared/CreateMessage.js +++ b/src/structures/shared/CreateMessage.js @@ -39,7 +39,7 @@ module.exports = async function createMessage(channel, options) { 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 (content) { From 0c16859746c039a1eb62d029bff534b2d3505219 Mon Sep 17 00:00:00 2001 From: Yukine Date: Thu, 21 Dec 2017 09:30:32 +0100 Subject: [PATCH 043/154] enchanced docs for GuildChannel (#2155) * enchanced docs for GuildChannel * changes due request --- src/structures/GuildChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index a84ad0847..316bdbefe 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -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 { From 4f8f2087c16ec8b6d9b785edbaba1c073ad15fc3 Mon Sep 17 00:00:00 2001 From: Isabella Date: Thu, 21 Dec 2017 02:31:17 -0600 Subject: [PATCH 044/154] docs/fix(setParent): docs update and nullable channel param (#2160) * fix(setParent): no longer in GuildChannel * refactored * little bit less ugly * space/appel suggestion * docs fix * shhhhhhhh * fun docs trip * prototype thing * mark nullable --- src/structures/CategoryChannel.js | 13 +++++++++++++ src/structures/GuildChannel.js | 7 ++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index d7121a32b..b9d3ceff2 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -13,6 +13,19 @@ class CategoryChannel extends GuildChannel { get children() { return this.guild.channels.filter(c => c.parentID === this.id); } + + /** + * Sets the category parent of this channel. + * It is not currently possible to set the parent of a CategoryChannel. + * @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} + */ } module.exports = CategoryChannel; diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 316bdbefe..0862f1455 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -323,14 +323,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} */ 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); } From 4063a3a16b581f643d5601ffcd735d9140c7c940 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sat, 23 Dec 2017 05:29:42 -0600 Subject: [PATCH 045/154] fix(Guild#createChannel): default type to 'text' (#2184) * fix(Guild#createChannel): default type to 'text' * gus suggestion --- src/structures/Guild.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 307e5eda2..5f9674585 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -910,8 +910,8 @@ class Guild extends Base { /** * 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 {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) @@ -925,7 +925,7 @@ class Guild extends Base { * .then(channel => console.log(`Created new channel ${channel}`)) * .catch(console.error); */ - createChannel(name, type, { nsfw, bitrate, userLimit, parent, overwrites, reason } = {}) { + 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); @@ -955,7 +955,7 @@ class Guild extends Base { return this.client.api.guilds(this.id).channels.post({ data: { name, - type: ChannelTypes[type.toUpperCase()], + type: type ? ChannelTypes[type.toUpperCase()] : 'text', nsfw, bitrate, user_limit: userLimit, From 59d5c5a9475fef746542bdf4b7de826d3d632f10 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 31 Dec 2017 21:19:05 +0200 Subject: [PATCH 046/154] Don't send files in edit request (#2199) --- src/structures/Message.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index a409e411a..9b86279ea 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -377,10 +377,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); From 84e4dd6a998294e8a9a50c885006c1a908ba09ba Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 31 Dec 2017 13:20:52 -0600 Subject: [PATCH 047/154] animated emojis (#2182) --- src/structures/Emoji.js | 14 ++++++++++++-- src/util/Constants.js | 2 +- src/util/Util.js | 13 ++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index d1cca2843..302ebfccd 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -45,6 +45,12 @@ class Emoji extends Base { */ this.managed = data.managed; + /** + * Whether this emoji is animated + * @type {boolean} + */ + this.animated = data.animated; + this._roles = data.roles; } @@ -85,7 +91,7 @@ class Emoji extends Base { * @readonly */ get url() { - return this.client.rest.cdn.Emoji(this.id); + return this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png'); } /** @@ -198,7 +204,11 @@ class Emoji extends Base { * msg.reply(`Hello! ${emoji}`); */ toString() { - return this.requiresColons ? `<:${this.name}:${this.id}>` : this.name; + if (!this.id || !this.requiresColons) { + return this.name; + } + + return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index efeeb29fa..bc67df35c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -110,7 +110,7 @@ function makeImageUrl(root, { format = 'webp', size } = {}) { exports.Endpoints = { CDN(root) { return { - Emoji: emojiID => `${root}/emojis/${emojiID}.png`, + Emoji: (emojiID, format = 'png') => `${root}/emojis/${emojiID}.${format}`, Asset: name => `${root}/assets/${name}`, DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, Avatar: (userID, hash, format = 'default', size) => { diff --git a/src/util/Util.js b/src/util/Util.js index d92dc94b7..f1758000d 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -70,18 +70,21 @@ class Util { * Parses emoji info out of a string. The string must be one of: * * A UTF-8 emoji (no ID) * * A URL-encoded UTF-8 emoji (no ID) - * * A Discord custom emoji (`<:name:id>`) + * * A Discord custom emoji (`<:name:id>` or ``) * @param {string} text Emoji string to parse - * @returns {Object} Object with `name` and `id` properties + * @returns {Object} Object with `animated`, `name`, and `id` properties * @private */ static parseEmoji(text) { if (text.includes('%')) text = decodeURIComponent(text); if (text.includes(':')) { - const [name, id] = text.split(':'); - return { name, id }; + const m = text.match(/?/); + if (!m) { + return null; + } + return { animated: Boolean(m[1]), name: m[2], id: m[3] }; } else { - return { name: text, id: null }; + return { animated: false, name: text, id: null }; } } From 229eb2be2da11563c3c9978664e050044de720df Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 31 Dec 2017 13:21:29 -0600 Subject: [PATCH 048/154] Fix Collector bug where checkEnd is only called on a valid message (#2186) --- src/structures/interfaces/Collector.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 2c4fd158c..0e7f1f956 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -76,17 +76,18 @@ 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.key, collect.value); - /** - * 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 {*} element The element that got collected + * @param {...*} args The arguments emitted by the listener + */ + this.emit('collect', collect.value, ...args); + } this.checkEnd(); } From 954a1c8d1afdf50f068ca9ca9320d265f6cb0633 Mon Sep 17 00:00:00 2001 From: Darqam Date: Wed, 3 Jan 2018 18:00:39 -0600 Subject: [PATCH 049/154] updating channelCreate example to reflect recent changes (#2194) * updating channelCreate example to reflect recent changes Type is now part of the options object, so the example should reflect that. * changing type to reason per comment --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 5f9674585..b11548b56 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -921,7 +921,7 @@ class Guild extends Base { * @returns {Promise} * @example * // Create a new text channel - * guild.createChannel('new-general', 'text') + * guild.createChannel('new-general', { reason: 'My reason here.' }) * .then(channel => console.log(`Created new channel ${channel}`)) * .catch(console.error); */ From abb93c9eb99ee1cf1580b3d09e46679186e8b6b7 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 3 Jan 2018 18:01:39 -0600 Subject: [PATCH 050/154] fix location of toString for channels (#2202) --- src/structures/Channel.js | 11 +++++++++++ src/structures/GuildChannel.js | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 04867b118..833d9ef12 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -52,6 +52,17 @@ 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} diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 0862f1455..25c26274b 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -489,17 +489,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; From 61da73fee00078a88401d82b66fa3402e0d2785d Mon Sep 17 00:00:00 2001 From: Isabella Date: Wed, 3 Jan 2018 18:04:19 -0600 Subject: [PATCH 051/154] GuildMember#permissionsFor takes object (#2210) --- src/structures/GuildMember.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 21f84ec98..f0e3952dd 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -300,11 +300,12 @@ class GuildMember extends Base { /** * 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)); } From 45127bb408e668f6e1ca4b001b857f64314243c9 Mon Sep 17 00:00:00 2001 From: Isabella Date: Wed, 3 Jan 2018 18:12:05 -0600 Subject: [PATCH 052/154] docs: improve examples (master branch) (#2209) * docs: improve examples * more improvements fix maybe this another example collectors * stuff --- src/stores/GuildMemberStore.js | 23 ++++++++++--------- src/structures/Channel.js | 4 ++-- src/structures/Guild.js | 9 ++++++-- src/structures/GuildChannel.js | 9 ++++++-- src/structures/GuildMember.js | 14 ++++++++++- src/structures/Message.js | 12 ++++++---- src/structures/interfaces/TextBasedChannel.js | 11 +++++---- 7 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index ad0acbed9..692d72784 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -72,21 +72,22 @@ class GuildMemberStore extends DataStore { * @returns {Promise|Promise>} * @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(); diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 833d9ef12..af17de57d 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -69,8 +69,8 @@ class Channel extends Base { * @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); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index b11548b56..c47fd74f5 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -514,6 +514,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} + * @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; @@ -921,8 +926,8 @@ class Guild extends Base { * @returns {Promise} * @example * // Create a new text channel - * guild.createChannel('new-general', { reason: 'My reason here.' }) - * .then(channel => console.log(`Created new channel ${channel}`)) + * guild.createChannel('new-general', { reason: 'Cool new channel' }) + * .then(console.log) * .catch(console.error); */ createChannel(name, { type, nsfw, bitrate, userLimit, parent, overwrites, reason } = {}) { diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 25c26274b..518e12253 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -272,8 +272,8 @@ class GuildChannel extends Channel { * @returns {Promise} * @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) { @@ -386,6 +386,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} + * @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: { diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index f0e3952dd..8a22c0e4d 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -396,6 +396,16 @@ class GuildMember extends Base { * @param {Collection|RoleResolvable[]} roles The roles or role IDs to apply * @param {string} [reason] Reason for applying the roles * @returns {Promise} + * @example + * // Set the member's roles to a single role + * guildMember.setRoles(['391156570408615936']) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all the roles from a member + * guildMember.setRoles([]) + * .then(member => console.log(`Member roles is now of ${member.roles.size} size`)) + * .catch(console.error); */ setRoles(roles, reason) { return this.edit({ roles }, reason); @@ -528,7 +538,9 @@ class GuildMember extends Base { * @returns {Promise} * @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); diff --git a/src/structures/Message.js b/src/structures/Message.js index 9b86279ea..050adae9f 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -273,10 +273,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 +294,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>} + * @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) => { diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 39aac0937..701d56036 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -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>} 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) { From 01f1f1b58e5b4a3ba5de77c85802520ab4cafee3 Mon Sep 17 00:00:00 2001 From: 1Computer1 <1Computer1@users.noreply.github.com> Date: Wed, 3 Jan 2018 19:17:15 -0500 Subject: [PATCH 053/154] Fix query string on requests after ratelimited (#2215) * Fix querystring being appended multiple times when ratelimited * Better way? * Better better way * Fix empty queries --- src/rest/APIRequest.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index ae4a352f3..e0527320d 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -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()); From 780a311c0a63d0e06bb13362ccb546b819cfbff9 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Jan 2018 02:18:56 +0200 Subject: [PATCH 054/154] Minor refactor to Util methods (#2213) * Minor refactor to Util methods * Fix derp --- src/util/Util.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index f1758000d..1d6f2d17b 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -77,15 +77,10 @@ class Util { */ static parseEmoji(text) { if (text.includes('%')) text = decodeURIComponent(text); - if (text.includes(':')) { - const m = text.match(/?/); - if (!m) { - return null; - } - return { animated: Boolean(m[1]), name: m[2], id: m[3] }; - } else { - return { animated: false, name: text, id: null }; - } + if (!text.includes(':')) return { animated: false, name: text, id: null }; + const m = text.match(/?/); + if (!m) return null; + return { animated: Boolean(m[1]), name: m[2], id: m[3] }; } /** @@ -184,11 +179,11 @@ class Util { * @private */ static makePlainError(err) { - const obj = {}; - obj.name = err.name; - obj.message = err.message; - obj.stack = err.stack; - return obj; + return { + name: err.name, + message: err.message, + stack: err.stack, + }; } /** @@ -318,8 +313,8 @@ class Util { * @private */ static basename(path, ext) { - let f = splitPathRe.exec(path).slice(1)[2]; - if (ext && f.substr(-1 * ext.length) === ext) f = f.substr(0, f.length - ext.length); + let f = splitPathRe.exec(path)[3]; + if (ext && f.endsWith(ext)) f = f.slice(0, -ext.length); return f; } From 2318812f7f2229e0934a28967b406c4ef7f58073 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 4 Jan 2018 20:29:32 +0100 Subject: [PATCH 055/154] fix: allow the Util#parseEmoji regex to match emoji identifier (#2229) --- src/util/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index 1d6f2d17b..c1020ec10 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -78,7 +78,7 @@ class Util { static parseEmoji(text) { if (text.includes('%')) text = decodeURIComponent(text); if (!text.includes(':')) return { animated: false, name: text, id: null }; - const m = text.match(/?/); + const m = text.match(/?/); if (!m) return null; return { animated: Boolean(m[1]), name: m[2], id: m[3] }; } From eeded907828e904d7e3b241304bd4147096a9ae2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 5 Jan 2018 21:22:11 +0000 Subject: [PATCH 056/154] Fix: "value" argument is out of bounds when writing timestamp header to packet --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 7c6b98c3e..1dbaceeb9 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -199,8 +199,8 @@ class StreamDispatcher extends Writable { const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime); setTimeout(done.bind(this), next); } - if (this._sdata.sequence === (2 ** 16) - 1) this._sdata.sequence = -1; - if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -TIMESTAMP_INC; + if (this._sdata.sequence >= (2 ** 16) - 1) this._sdata.sequence = -1; + if (this._sdata.timestamp >= (2 ** 32) - 1) this._sdata.timestamp = -TIMESTAMP_INC; this._sdata.sequence++; this._sdata.timestamp += TIMESTAMP_INC; this.count++; From eb40a663ddd9ab974037955e9ba98120d5d63365 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 5 Jan 2018 21:23:21 +0000 Subject: [PATCH 057/154] god damn it i forgot to save the file the first time --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 1dbaceeb9..3ae780af7 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -200,7 +200,7 @@ class StreamDispatcher extends Writable { setTimeout(done.bind(this), next); } if (this._sdata.sequence >= (2 ** 16) - 1) this._sdata.sequence = -1; - if (this._sdata.timestamp >= (2 ** 32) - 1) this._sdata.timestamp = -TIMESTAMP_INC; + if (this._sdata.timestamp >= (2 ** 32) - TIMESTAMP_INC) this._sdata.timestamp = -TIMESTAMP_INC; this._sdata.sequence++; this._sdata.timestamp += TIMESTAMP_INC; this.count++; From e792757e3244416bb821cc4cb4e231f9787b7499 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 8 Jan 2018 22:13:46 -0500 Subject: [PATCH 058/154] Only create ShardClientUtil when spawned by ShardingManager --- src/client/Client.js | 5 ++--- src/sharding/Shard.js | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 4b335e88a..77f0682c2 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -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 diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 0e44f91a2..98bafff2e 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -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, From 5efddac0250a6c92573ee107a82db511dd4f95cf Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 9 Jan 2018 14:17:08 -0600 Subject: [PATCH 059/154] Fix Channel.delete() example (#2238) --- src/structures/Channel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Channel.js b/src/structures/Channel.js index af17de57d..49248275a 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -69,7 +69,7 @@ class Channel extends Base { * @example * // Delete the channel * channel.delete() - * then(console.log) + * .then(console.log) * .catch(console.error); */ delete() { From 90fc161159cb6d1a5a631418b2305a246fc649ad Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 11 Jan 2018 16:14:54 +0000 Subject: [PATCH 060/154] Update copyright notice on license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 90cf11032..5f32c7958 100644 --- a/LICENSE +++ b/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. From 9eac19d9d8bfb81dc0079e3bff073a20b9c9768f Mon Sep 17 00:00:00 2001 From: Isabella Date: Thu, 11 Jan 2018 10:33:30 -0600 Subject: [PATCH 061/154] refactor: more oop with stores (#2216) * refactor: more oop with stores * forgot bulk delete * Revert "forgot bulk delete" This reverts commit 1b4fb999ee07b358ee6e1af9efb8981b84f83af1. * appease linter * missed some shh * fail --- src/client/actions/ChannelCreate.js | 2 +- src/client/actions/GuildBanRemove.js | 2 +- src/client/actions/GuildEmojiCreate.js | 2 +- src/client/actions/GuildRoleCreate.js | 2 +- src/client/actions/GuildSync.js | 4 +- src/client/actions/MessageCreate.js | 2 +- src/client/actions/MessageReactionAdd.js | 2 +- .../websocket/packets/handlers/GuildCreate.js | 2 +- .../packets/handlers/GuildMemberAdd.js | 2 +- .../packets/handlers/GuildMembersChunk.js | 2 +- .../packets/handlers/PresenceUpdate.js | 10 +- .../websocket/packets/handlers/Ready.js | 10 +- src/stores/ChannelStore.js | 2 +- src/stores/DataStore.js | 4 +- src/stores/EmojiStore.js | 47 +++- src/stores/GuildChannelStore.js | 74 +++++- src/stores/GuildMemberStore.js | 80 +++++- src/stores/GuildStore.js | 40 +++ src/stores/MessageStore.js | 10 +- src/stores/PresenceStore.js | 4 +- src/stores/ReactionStore.js | 13 +- src/stores/ReactionUserStore.js | 24 +- src/stores/RoleStore.js | 42 +++- src/stores/UserStore.js | 2 +- src/structures/ClientApplication.js | 2 +- src/structures/ClientUser.js | 43 +--- src/structures/DMChannel.js | 2 +- src/structures/GroupDMChannel.js | 2 +- src/structures/Guild.js | 235 +----------------- src/structures/GuildAuditLogs.js | 2 +- src/structures/GuildMember.js | 2 +- src/structures/Invite.js | 6 +- src/structures/Message.js | 13 +- src/structures/MessageMentions.js | 2 +- src/structures/MessageReaction.js | 22 -- src/structures/TextChannel.js | 2 +- src/structures/Webhook.js | 4 +- src/structures/shared/Search.js | 2 +- 38 files changed, 364 insertions(+), 359 deletions(-) diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 1195d7345..09e74f0f3 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -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); } diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index fe15f17f7..782b5fe2a 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -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); } } diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index cac3d8c4d..4b5b913c8 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -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 }; } diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index 7f5bbb485..b4930399d 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -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 }; diff --git a/src/client/actions/GuildSync.js b/src/client/actions/GuildSync.js index 2019c5d22..f7dbde6ad 100644 --- a/src/client/actions/GuildSync.js +++ b/src/client/actions/GuildSync.js @@ -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); } } } diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index 1755cec71..e76c6071d 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -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; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index b26ad5949..073ba05a7 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -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, diff --git a/src/client/websocket/packets/handlers/GuildCreate.js b/src/client/websocket/packets/handlers/GuildCreate.js index a920b02cf..96c5ae987 100644 --- a/src/client/websocket/packets/handlers/GuildCreate.js +++ b/src/client/websocket/packets/handlers/GuildCreate.js @@ -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) { /** diff --git a/src/client/websocket/packets/handlers/GuildMemberAdd.js b/src/client/websocket/packets/handlers/GuildMemberAdd.js index de244ed63..15201b825 100644 --- a/src/client/websocket/packets/handlers/GuildMemberAdd.js +++ b/src/client/websocket/packets/handlers/GuildMemberAdd.js @@ -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); } diff --git a/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js index 5985a9e42..4e821f5cc 100644 --- a/src/client/websocket/packets/handlers/GuildMembersChunk.js +++ b/src/client/websocket/packets/handlers/GuildMembersChunk.js @@ -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); diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js index 4da269513..87732b8ce 100644 --- a/src/client/websocket/packets/handlers/PresenceUpdate.js +++ b/src/client/websocket/packets/handlers/PresenceUpdate.js @@ -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); } } } diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index b1a833d5f..367406ba0 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -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', diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 2ceee8000..53a81358c 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -51,7 +51,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; diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index c4256dfe2..72f86028e 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -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; diff --git a/src/stores/EmojiStore.js b/src/stores/EmojiStore.js index 035cc3d4d..1e50c07f4 100644 --- a/src/stores/EmojiStore.js +++ b/src/stores/EmojiStore.js @@ -1,6 +1,8 @@ +const Collection = require('../util/Collection'); const DataStore = require('./DataStore'); const Emoji = require('../structures/Emoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); +const DataResolver = require('../util/DataResolver'); /** * Stores emojis. @@ -13,8 +15,49 @@ class EmojiStore 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 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|RoleResolvable[]} [options.roles] Roles to limit the emoji to + * @param {string} [options.reason] Reason for creating the emoji + * @returns {Promise} 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 })); } /** diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 0bc3e8c4e..ee9e8f013 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -1,6 +1,9 @@ -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 { resolve } = require('../util/Permissions'); /** * Stores guild channels. @@ -13,13 +16,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} [options.overwrites] Permission overwrites + * @param {string} [options.reason] Reason for creating the channel + * @returns {Promise} + * @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 = resolve(allow); + if (deny instanceof Array) deny = 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 diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 692d72784..8cdd7611e 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -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] }); } /** @@ -100,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} 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} 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} + * @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 } = {}) { diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 45e9c9cfc..7b7810aee 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -1,4 +1,6 @@ const DataStore = require('./DataStore'); +const DataResolver = require('../util/DataResolver'); +const { Events } = require('../util/Constants'); const Guild = require('../structures/Guild'); /** @@ -35,6 +37,44 @@ class GuildStore extends DataStore { * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Snowflake} */ + + /** + * Creates a guild. + * This is only available when using a user account. + * @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} 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; diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index cc0ff60b0..565f4c13d 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -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; }); } diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index 8322c9c65..1b927934e 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -11,9 +11,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 }); } /** diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index c11b4e176..4a689a447 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -12,8 +12,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 +40,15 @@ class ReactionStore extends DataStore { * @param {MessageReactionResolvable} role The role resolvable to resolve * @returns {?Snowflake} */ + + /** + * Removes all reactions from a message. + * @returns {Promise} + */ + removeAll() { + return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete() + .then(() => this.message); + } } module.exports = ReactionStore; diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js index b3c3ec012..d246d10b9 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/stores/ReactionUserStore.js @@ -23,11 +23,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} + */ + 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; diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index bb8cd749d..14f034337 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -1,5 +1,7 @@ const DataStore = require('./DataStore'); const Role = require('../structures/Role'); +const { resolveColor } = require('../util/Util'); +const { resolve } = require('../util/Permissions'); /** * Stores roles. @@ -12,8 +14,44 @@ 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. + * The position will silently reset to 1 if an invalid one is provided, or none. + * @param {RoleData} [data] The data to update the role with + * @param {string} [reason] Reason for creating this role + * @returns {Promise} + * @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({ + * 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 = 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; + }); } /** diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 432a0768a..48e988405 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -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)); } } diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 073fce7ba..749f635ed 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -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); } } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index b0b3452d4..42556ae2c 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -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'); @@ -260,45 +259,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. - * This is only available when using a user account. - * @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} 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 +287,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)); } } diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index fe6ba57c6..0567b98d4 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -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; } diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 2ade06b06..4868fbbb9 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -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); } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index c47fd74f5..977e7b463 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1,14 +1,12 @@ 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'); @@ -183,7 +181,7 @@ class Guild extends Base { 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) { @@ -197,18 +195,18 @@ class Guild extends Base { if (data.channels) { this.channels.clear(); for (const rawChannel of data.channels) { - this.client.channels.create(rawChannel, this); + this.client.channels.add(rawChannel, this); } } if (data.roles) { this.roles.clear(); - for (const role of data.roles) this.roles.create(role); + for (const role of data.roles) this.roles.add(role); } if (data.presences) { for (const presence of data.presences) { - this.presences.create(presence); + this.presences.add(presence); } } @@ -223,7 +221,7 @@ class Guild extends Base { * @type {EmojiStore} */ this.emojis = new EmojiStore(this); - if (data.emojis) for (const emoji of data.emojis) this.emojis.create(emoji); + 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()) @@ -565,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)); } /** @@ -823,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} 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} - * @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} 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). * This is only available when using a user account. @@ -904,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 {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} [options.overwrites] Permission overwrites - * @param {string} [options.reason] Reason for creating the channel - * @returns {Promise} - * @example - * // Create a new text channel - * guild.createChannel('new-general', { reason: 'Cool new channel' }) - * .then(console.log) - * .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: 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); - } - /** * The data needed for updating a channel's position. * @typedef {Object} ChannelPosition @@ -1001,85 +859,6 @@ class Guild extends Base { ); } - /** - * Creates a new role in the guild with given information. - * The position will silently reset to 1 if an invalid one is provided, or none. - * @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} - * @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|RoleResolvable[]} [options.roles] Roles to limit the emoji to - * @param {string} [options.reason] Reason for creating the emoji - * @returns {Promise} 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} diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index d43a48b29..148e199a9 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -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} diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8a22c0e4d..182f9cb12 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -66,7 +66,7 @@ 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); + this.user = this.guild.client.users.add(data.user); if (data.roles) this._roles = data.roles; } diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 89d5c9c99..1f02d11b7 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -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 diff --git a/src/structures/Message.js b/src/structures/Message.js index 050adae9f..8fb9688fa 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -52,7 +52,7 @@ class Message extends Base { * The author of the message * @type {User} */ - this.author = this.client.users.create(data.author, !data.webhook_id); + this.author = this.client.users.add(data.author, !data.webhook_id); /** * Represents the author of the message as a guild member. @@ -121,7 +121,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); } } @@ -429,15 +429,6 @@ class Message extends Base { }).reaction); } - /** - * Removes all reactions from a message. - * @returns {Promise} - */ - clearReactions() { - return this.client.api.channels(this.channel.id).messages(this.id).reactions.delete() - .then(() => this); - } - /** * Deletes the message. * @param {Object} [options] Options diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index ee4a0e69d..102e63aaa 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -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); } } diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 28a320a63..b65721134 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,7 +1,6 @@ const Emoji = require('./Emoji'); const ReactionEmoji = require('./ReactionEmoji'); const ReactionUserStore = require('../stores/ReactionUserStore'); -const { Error } = require('../errors'); /** * Represents a reaction to a message. @@ -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} - */ - 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); diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 5679f841a..2c512dcee 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -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); } /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 7ee44745e..ba56ebdd3 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -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); }); } diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index 3adca7fcc..db30b572c 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -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, From 2249da464fb188a75149ff76727015c307627202 Mon Sep 17 00:00:00 2001 From: bdistin Date: Thu, 11 Jan 2018 12:44:35 -0600 Subject: [PATCH 062/154] Fix member.ban() and channel.clone() (#2241) * Fix member.ban() * also fix channel.clone() * type is an option parameter in the new create * lint * better fix for clone --- src/structures/GuildChannel.js | 4 ++-- src/structures/GuildMember.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 518e12253..11cc571e9 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -411,8 +411,8 @@ class GuildChannel extends Channel { * @returns {Promise} */ clone({ name = this.name, withPermissions = true, withTopic = true, reason } = {}) { - const options = { overwrites: withPermissions ? this.permissionOverwrites : [], reason }; - return this.guild.createChannel(name, this.type, options) + const options = { overwrites: withPermissions ? this.permissionOverwrites : [], reason, type: this.type }; + return this.guild.channels.create(name, options) .then(channel => withTopic ? channel.setTopic(this.topic) : channel); } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 182f9cb12..80c6ce48c 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -543,7 +543,7 @@ class GuildMember extends Base { * .catch(console.error); */ ban(options) { - return this.guild.ban(this, options); + return this.guild.members.ban(this, options); } /** From 8aafcd6cdea4b524e910c5e765e080cfff3477f2 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 12 Jan 2018 07:05:27 -0600 Subject: [PATCH 063/154] allow passing a function to shard eval (#2193) --- src/sharding/Shard.js | 7 ++++--- src/sharding/ShardClientUtil.js | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 98bafff2e..96ab796dc 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -175,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) { @@ -191,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); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index b0e9d57ed..1c47f0296 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -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); From 8cab673fea2751f20079e04f29c1e80b73781304 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 13 Jan 2018 12:47:19 +0000 Subject: [PATCH 064/154] fix: stream dispatcher throwing key error due to missing secretKey --- src/client/voice/VoiceWebSocket.js | 3 ++- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index 18632355f..8eefe32d8 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -155,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 diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 3ae780af7..eeda8a5b1 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -207,7 +207,7 @@ class StreamDispatcher extends Writable { } _playChunk(chunk) { - if (this.player.dispatcher !== this) return; + 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)); } From ea028ae0749a20a8d4817a05ece9a761fa36a640 Mon Sep 17 00:00:00 2001 From: Jisagi Date: Sat, 13 Jan 2018 13:51:09 +0100 Subject: [PATCH 065/154] Fix destructuring errors in GuildChannelStore and RoleStore (#2244) * Permissions#resolve fix * Travis failed because >120 chars per line * better fix Permissions#resolve reverted removed destructuring in GuildChannelStore and RoleStore --- src/stores/GuildChannelStore.js | 6 +++--- src/stores/RoleStore.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index ee9e8f013..f37c58853 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -3,7 +3,7 @@ const Channel = require('../structures/Channel'); const { ChannelTypes } = require('../util/Constants'); const DataStore = require('./DataStore'); const GuildChannel = require('../structures/GuildChannel'); -const { resolve } = require('../util/Permissions'); +const Permissions = require('../util/Permissions'); /** * Stores guild channels. @@ -54,8 +54,8 @@ class GuildChannelStore extends DataStore { 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 = resolve(allow); - if (deny instanceof Array) deny = resolve(deny); + 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) { diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 14f034337..2b5f9ee10 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -1,7 +1,7 @@ const DataStore = require('./DataStore'); const Role = require('../structures/Role'); const { resolveColor } = require('../util/Util'); -const { resolve } = require('../util/Permissions'); +const Permissions = require('../util/Permissions'); /** * Stores roles. @@ -42,7 +42,7 @@ class RoleStore extends DataStore { */ create(data = {}, reason) { if (data.color) data.color = resolveColor(data.color); - if (data.permissions) data.permissions = resolve(data.permissions); + 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({ From 352bd13e6f590e12d5e2e3be0cf836e929cef74b Mon Sep 17 00:00:00 2001 From: Yukine Date: Sat, 13 Jan 2018 13:52:25 +0100 Subject: [PATCH 066/154] fix typo in Error constants (#2243) * fix typo in Error constants * another one (#1) --- src/util/Constants.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index bc67df35c..69e4f4995 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -657,7 +657,7 @@ exports.Colors = { * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE * * BULK_DELETE_MESSAGE_TOO_OLD - * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTANING_BOT + * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT * * REACTION_BLOCKED * @typedef {string} APIError */ @@ -703,7 +703,7 @@ exports.APIErrors = { CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, BULK_DELETE_MESSAGE_TOO_OLD: 50034, - INVITE_ACCEPTED_TO_GUILD_NOT_CONTANING_BOT: 50036, + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, REACTION_BLOCKED: 90001, }; From 83640a26404722f7c3882687ceb6dbadd5a40aa8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 13 Jan 2018 20:50:24 +0000 Subject: [PATCH 067/154] refactor: tidier overflow checking in StreamDispatcher --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index eeda8a5b1..7513ec6b0 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -199,10 +199,10 @@ class StreamDispatcher extends Writable { const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime); setTimeout(done.bind(this), next); } - if (this._sdata.sequence >= (2 ** 16) - 1) this._sdata.sequence = -1; - if (this._sdata.timestamp >= (2 ** 32) - TIMESTAMP_INC) this._sdata.timestamp = -TIMESTAMP_INC; 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++; } From a3be0f3726af76420e20f2406c15b1813039b8ea Mon Sep 17 00:00:00 2001 From: Dim Date: Sun, 14 Jan 2018 08:28:00 -0500 Subject: [PATCH 068/154] docs: Collection > RoleStore (#2251) --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 977e7b463..a75829313 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -40,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} + * @type {RoleStore} */ this.roles = new RoleStore(this); From 351f5f72091686ee94c9e1bbcb1e66fb65595b72 Mon Sep 17 00:00:00 2001 From: Dim Date: Sun, 14 Jan 2018 08:28:46 -0500 Subject: [PATCH 069/154] fix: missing height & width in embeds (#2249) --- src/structures/MessageEmbed.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index b7f430b75..9c8271372 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -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; /** From 42c0e50c92d28dca78057907d89d4df576daedf2 Mon Sep 17 00:00:00 2001 From: pedall Date: Sun, 14 Jan 2018 20:46:08 +0100 Subject: [PATCH 070/154] Fix ShardClientUtil#broadcastEval - now really accepting functions (#2248) * small fix to broadcastEval accepting functions * dont ignore idea anymore (separate PR then) --- src/sharding/ShardClientUtil.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 1c47f0296..8b8262150 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -86,7 +86,7 @@ class ShardClientUtil { */ broadcastEval(script) { return new Promise((resolve, reject) => { - script = typeof script !== 'function' ? `(${script})(this)` : script; + script = typeof script === 'function' ? `(${script})(this)` : script; const listener = message => { if (!message || message._sEval !== script) return; process.removeListener('message', listener); From 4fb7e64a39b1889191634f5f60fe9e20ad71ebfe Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 15 Jan 2018 18:20:09 -0600 Subject: [PATCH 071/154] Add parent, nsfw, bitrate, and userLimit options to GuildChannel.clone() (#2259) * Add parent, nsfw, bitrate, and userLimit options to GuildChannel.clone() * fix lint --- src/structures/GuildChannel.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 11cc571e9..a7c5b5244 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -407,11 +407,23 @@ 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} */ - clone({ name = this.name, withPermissions = true, withTopic = true, reason } = {}) { - const options = { overwrites: withPermissions ? this.permissionOverwrites : [], reason, type: this.type }; + clone({ name = this.name, withPermissions = true, withTopic = true, nsfw, parent, bitrate, userLimit, reason } = {}) { + const options = { + overwrites: withPermissions ? this.permissionOverwrites : [], + nsfw: typeof nsfw === 'undefined' ? this.nsfw : nsfw, + parent: parent || this.parent, + bitrate: bitrate || this.bitrate, + userLimit: userLimit || this.userLimit, + reason, + type: this.type, + }; return this.guild.channels.create(name, options) .then(channel => withTopic ? channel.setTopic(this.topic) : channel); } From 3038d4b2c70f9e72dab4ed1d808e565a22c3c230 Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 15 Jan 2018 18:20:36 -0600 Subject: [PATCH 072/154] Address missing application docs in setPresence (#2257) fixes #2103 according to how crawl says it should be fixed in #2104 --- src/structures/ClientUser.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 42556ae2c..92fe176f5 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -187,7 +187,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 From 3d32dea5e1a0e6ada1d04b924e6543ac5260c0f5 Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 15 Jan 2018 18:20:51 -0600 Subject: [PATCH 073/154] remove pointless function from GuildEmojisUpdate (#2256) --- src/client/actions/GuildEmojisUpdate.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/client/actions/GuildEmojisUpdate.js b/src/client/actions/GuildEmojisUpdate.js index 8656a34cd..90f43eeba 100644 --- a/src/client/actions/GuildEmojisUpdate.js +++ b/src/client/actions/GuildEmojisUpdate.js @@ -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 From 4122db027531a0e9382a6d01ce60f3848ca3bb35 Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 15 Jan 2018 18:24:19 -0600 Subject: [PATCH 074/154] Return undefined from Collection.find() / findKey() (#2260) To be compliant with Array.find() / findIndex() --- src/util/Collection.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index 1f3fc6307..acddf0518 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -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.'); } From 36555c1cea5d559eb5cfbaebec7866b21cd1a835 Mon Sep 17 00:00:00 2001 From: Isabella Date: Mon, 15 Jan 2018 18:32:40 -0600 Subject: [PATCH 075/154] refactor(GuildMember#manageable): refactored kickable and bannable (#2211) * refactor(GuildMember#manageable): merged kickable and bannable code * hydar suggestion --- src/structures/GuildMember.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 80c6ce48c..c7384d875 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -259,17 +259,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.highestRole.comparePositionTo(this.highestRole) > 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 +285,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); } /** From e576387fea2a973eefe298a6f3610eb0d8b54999 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 16 Jan 2018 02:33:58 +0200 Subject: [PATCH 076/154] Fix ReactionCollector#remove and make Collector interface more consistent (#2221) * Fix ReactionCollector#remove and make Collector interface more consistent * Move those below the doc * Remove object spread * Only emit event arguments * Forgot to delete this line * Update docs * Also fix this * More edits to docs * Snowflake|string --- src/structures/MessageCollector.js | 21 ++++++++++------ src/structures/ReactionCollector.js | 33 ++++++++++++++++++-------- src/structures/interfaces/Collector.js | 12 ++++------ 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index a9260382c..d034a96db 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -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; } diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index 60bdabc48..27582893a 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -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); } diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 0e7f1f956..0578dadfd 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -78,15 +78,14 @@ class Collector extends EventEmitter { const collect = this.collect(...args); if (collect && this.filter(...args, this.collected)) { - this.collected.set(collect.key, collect.value); + 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); + this.emit('collect', ...args); } this.checkEnd(); } @@ -101,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(); } From c125cc9c108873f3c071add939c469d23552af67 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 17 Jan 2018 17:20:45 +0100 Subject: [PATCH 077/154] update typings --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 0b5b13f4a..829f4271f 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 0b5b13f4a521cba0fc42aa0f9b2c4a1abca2de3d +Subproject commit 829f4271fed258be72151c32d471c9cd411d052a From 19591b0bb190bb091dee3b3f5321f3c9e89f12aa Mon Sep 17 00:00:00 2001 From: Michel Nguyen Date: Thu, 18 Jan 2018 07:42:05 +0100 Subject: [PATCH 078/154] docs: fix attachFiles() docs (#2267) * docs change * fix attachFiles docs * why was this still here --- src/structures/MessageEmbed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 9c8271372..de3ceddf7 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -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} files Files to attach * @returns {MessageEmbed} */ From 1db09064831e4bf040d0fcd867c2ac3c35aca127 Mon Sep 17 00:00:00 2001 From: Artful Date: Thu, 18 Jan 2018 17:47:05 +1100 Subject: [PATCH 079/154] Client method examples. (#2264) * Client method examples. * Consistency * As per hydras request :bow: * Thanks kya --- src/client/Client.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index 77f0682c2..b16d20eed 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -287,6 +287,11 @@ class Client extends BaseClient { * Obtains an invite from Discord. * @param {InviteResolvable} invite Invite code or URL * @returns {Promise} + * @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); @@ -299,6 +304,11 @@ class Client extends BaseClient { * @param {Snowflake} id ID of the webhook * @param {string} [token] Token for the webhook * @returns {Promise} + * @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)); @@ -307,6 +317,11 @@ class Client extends BaseClient { /** * Obtains the available voice regions from Discord. * @returns {Collection} + * @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 => { @@ -323,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)) { @@ -359,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} + * @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() @@ -374,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) { From d5b0cf9ffb5456efb270fc7ca8a0995feffe08e8 Mon Sep 17 00:00:00 2001 From: John Leuenhagen Date: Thu, 18 Jan 2018 03:17:50 -0500 Subject: [PATCH 080/154] Permissions improvements (#2126) * add Permissions.toArray() * accept Permissions objects to Permissions.missing() * accept Permissions as parameter to Permissions.has() * style fixes * remove redundant line, update JSDoc for Permission.resolve() * JSDoc, style, and checkAdmin fixes * add Permissions.resolveToObject() * accept PermissionResolvable to Permissions.missing() * remove `resolveToObject`, fix constructor JSDoc * remove redundant parameter type * fix `Permissions.missing()` * fix checkAdmin * update Permissions.toArray() description * eliminate ambiguity in Permissions.toArray() description * add backticks to permission example * remove irrelevant type in Permission ctor description * use this.constructor to properly support OOP * use simplified approach for Permissions.toArray() * fix return type on Permissions.toArray() * move `Permissions#toArray` to more suitable position * bitwise approach to `Permissions#missing` * allow `Permissions` to be iterated over * don't checkAdmin on return array * remove unnecessary conditional * fix JSDoc indentation * use simpler & more reliable approach for missing() * update PermissionResolvable typedef --- src/util/Permissions.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 7ccd9009c..e9ef9d1c5 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -7,19 +7,19 @@ const { RangeError } = require('../errors'); */ class Permissions { /** - * @param {number|PermissionResolvable[]} permissions Permissions or bitfield to read from + * @param {PermissionResolvable} permissions Permission(s) to read from */ constructor(permissions) { /** * Bitfield of the packed permissions * @type {number} */ - this.bitfield = typeof permissions === 'number' ? permissions : this.constructor.resolve(permissions); + this.bitfield = this.constructor.resolve(permissions); } /** * Checks whether the bitfield has a permission, or multiple permissions. - * @param {PermissionResolvable|PermissionResolvable[]} permission Permission(s) to check for + * @param {PermissionResolvable} permission Permission(s) to check for * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override * @returns {boolean} */ @@ -32,11 +32,12 @@ class Permissions { /** * Gets all given permissions that are missing from the bitfield. - * @param {PermissionResolvable[]} permissions Permissions to check for + * @param {PermissionResolvable} permissions Permission(s) to check for * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {PermissionResolvable[]} + * @returns {string[]} */ missing(permissions, checkAdmin = true) { + if (!(permissions instanceof Array)) permissions = new this.constructor(permissions).toArray(false); return permissions.filter(p => !this.has(p, checkAdmin)); } @@ -92,17 +93,32 @@ class Permissions { return serialized; } + /** + * Gets an {@link Array} of permission names (such as `VIEW_CHANNEL`) based on the permissions available. + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {string[]} + */ + toArray(checkAdmin = true) { + return Object.keys(this.constructor.FLAGS).filter(perm => this.has(perm, checkAdmin)); + } + + *[Symbol.iterator]() { + const keys = this.toArray(); + while (keys.length) yield keys.shift(); + } + /** * Data that can be resolved to give a permission number. This can be: * * A string (see {@link Permissions.FLAGS}) * * A permission number * * An instance of Permissions - * @typedef {string|number|Permissions} PermissionResolvable + * * An Array of PermissionResolvable + * @typedef {string|number|Permissions|PermissionResolvable[]} PermissionResolvable */ /** * Resolves permissions to their numeric form. - * @param {PermissionResolvable|PermissionResolvable[]} permission - Permission(s) to resolve + * @param {PermissionResolvable} permission - Permission(s) to resolve * @returns {number} */ static resolve(permission) { From b846cbd2b3760bc9438a0c75974bd15b9246d5e8 Mon Sep 17 00:00:00 2001 From: bdistin Date: Thu, 18 Jan 2018 02:28:14 -0600 Subject: [PATCH 081/154] GuildChannel.permissionsFor(role) (#2254) * GuildChannel.permissionsFor(role) * 1Comp's requested changes --- src/structures/GuildChannel.js | 77 ++++++++++++++++++++++++---------- src/structures/GuildMember.js | 6 +-- src/structures/Role.js | 12 ++++++ 3 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index a7c5b5244..36c8a47d6 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -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 diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index c7384d875..08561c757 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -295,9 +295,9 @@ 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); } /** diff --git a/src/structures/Role.js b/src/structures/Role.js index 39c28e11b..1d82f63ba 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -195,6 +195,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 From aa3407f705002384acd8bb824437351d6f341305 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 18 Jan 2018 09:38:45 +0100 Subject: [PATCH 082/154] Base Emoji class for ReactionEmoji and renamed GuildEmoji classes (#2230) * feat: create base Emoji class for ReactionEmoji and new GuildEmoji * rename EmojiStore to GuildEmojiStore to account for the new class' name --- src/client/Client.js | 6 +- src/client/actions/GuildEmojiCreate.js | 2 +- src/client/actions/GuildEmojiDelete.js | 4 +- src/client/actions/GuildEmojiUpdate.js | 6 +- src/client/actions/MessageReactionAdd.js | 2 +- src/errors/Messages.js | 2 +- src/index.js | 3 +- .../{EmojiStore.js => GuildEmojiStore.js} | 22 +- src/structures/Emoji.js | 236 ++---------------- src/structures/Guild.js | 6 +- src/structures/GuildAuditLogs.js | 2 +- src/structures/GuildEmoji.js | 197 +++++++++++++++ src/structures/MessageReaction.js | 10 +- src/structures/ReactionEmoji.js | 42 +--- src/util/Structures.js | 2 +- 15 files changed, 264 insertions(+), 278 deletions(-) rename src/stores/{EmojiStore.js => GuildEmojiStore.js} (87%) create mode 100644 src/structures/GuildEmoji.js diff --git a/src/client/Client.js b/src/client/Client.js index b16d20eed..db0fb151a 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -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'); @@ -208,11 +208,11 @@ class Client extends BaseClient { /** * All custom emojis that the client has access to, mapped by their IDs - * @type {EmojiStore} + * @type {GuildEmojiStore} * @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); } diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index 4b5b913c8..7fc955a0f 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -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; diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index 36a674b33..d8a83fc3e 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -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; diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js index b3ebb4b63..e6accf2c5 100644 --- a/src/client/actions/GuildEmojiUpdate.js +++ b/src/client/actions/GuildEmojiUpdate.js @@ -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; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 073ba05a7..9d307ceee 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -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; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 545452703..70ee8d47e 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -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.', }; diff --git a/src/index.js b/src/index.js index 42de34564..1cd14b654 100644 --- a/src/index.js +++ b/src/index.js @@ -26,8 +26,8 @@ module.exports = { // Stores ChannelStore: require('./stores/ChannelStore'), ClientPresenceStore: require('./stores/ClientPresenceStore'), - EmojiStore: require('./stores/EmojiStore'), GuildChannelStore: require('./stores/GuildChannelStore'), + GuildEmojiStore: require('./stores/GuildEmojiStore'), GuildMemberStore: require('./stores/GuildMemberStore'), GuildStore: require('./stores/GuildStore'), ReactionUserStore: require('./stores/ReactionUserStore'), @@ -64,6 +64,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'), diff --git a/src/stores/EmojiStore.js b/src/stores/GuildEmojiStore.js similarity index 87% rename from src/stores/EmojiStore.js rename to src/stores/GuildEmojiStore.js index 1e50c07f4..0fc4cc60b 100644 --- a/src/stores/EmojiStore.js +++ b/src/stores/GuildEmojiStore.js @@ -1,17 +1,17 @@ const Collection = require('../util/Collection'); const DataStore = require('./DataStore'); -const Emoji = require('../structures/Emoji'); +const GuildEmoji = require('../structures/GuildEmoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); const DataResolver = require('../util/DataResolver'); /** - * Stores emojis. + * Stores guild emojis. * @private * @extends {DataStore} */ -class EmojiStore extends DataStore { +class GuildEmojiStore extends DataStore { constructor(guild, iterable) { - super(guild.client, iterable, Emoji); + super(guild.client, iterable, GuildEmoji); this.guild = guild; } @@ -61,17 +61,17 @@ class EmojiStore extends DataStore { } /** - * Data that can be resolved into an Emoji object. This can be: + * Data that can be resolved into an GuildEmoji object. This can be: * * A custom emoji ID - * * An Emoji object + * * A GuildEmoji object * * A ReactionEmoji object - * @typedef {Snowflake|Emoji|ReactionEmoji} EmojiResolvable + * @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable */ /** - * Resolves a EmojiResolvable to a Emoji object. + * Resolves an EmojiResolvable to an Emoji object. * @param {EmojiResolvable} emoji The Emoji resolvable to identify - * @returns {?Emoji} + * @returns {?GuildEmoji} */ resolve(emoji) { if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id); @@ -79,7 +79,7 @@ class EmojiStore extends DataStore { } /** - * Resolves a EmojiResolvable to a Emoji ID string. + * Resolves an EmojiResolvable to an Emoji ID string. * @param {EmojiResolvable} emoji The Emoji resolvable to identify * @returns {?Snowflake} */ @@ -111,4 +111,4 @@ class EmojiStore extends DataStore { } } -module.exports = EmojiStore; +module.exports = GuildEmojiStore; diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 302ebfccd..f5682046f 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -1,97 +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} - */ - this.guild = guild; - - this._patch(data); - } - - _patch(data) { - /** - * The ID of the emoji - * @type {Snowflake} - */ - this.id = data.id; - - /** - * The name of the emoji - * @type {string} - */ - 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; - /** * Whether this emoji is animated * @type {boolean} */ - this.animated = data.animated; + this.animated = emoji.animated; - this._roles = data.roles; - } + /** + * The name of this emoji + * @type {string} + */ + this.name = emoji.name; - /** - * 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} - * @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.animated ? 'gif' : 'png'); + /** + * The ID of this emoji + * @type {?Snowflake} + */ + this.id = emoji.id; } /** @@ -100,148 +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|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} - * @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} - */ - 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} - */ - addRestrictedRole(role) { - return this.addRestrictedRoles([role]); - } - - /** - * Adds multiple roles to the list of roles that can use this emoji. - * @param {Collection|RoleResolvable[]} roles Roles to add - * @returns {Promise} - */ - 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} - */ - removeRestrictedRole(role) { - return this.removeRestrictedRoles([role]); - } - - /** - * Removes multiple roles from the list of roles that can use this emoji. - * @param {Collection|RoleResolvable[]} roles Roles to remove - * @returns {Promise} - */ - 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() { - if (!this.id || !this.requiresColons) { - return this.name; - } - - return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; - } - - /** - * Deletes the emoji. - * @param {string} [reason] Reason for deleting the emoji - * @returns {Promise} - */ - 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; } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a75829313..51ea81d32 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -10,7 +10,7 @@ const Snowflake = require('../util/Snowflake'); 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'); @@ -218,9 +218,9 @@ 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} + * @type {GuildEmojiStore} */ - this.emojis = new EmojiStore(this); + this.emojis = new GuildEmojiStore(this); if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji); } else { this.client.actions.GuildEmojisUpdate.handle({ diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 148e199a9..6df77707e 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -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 */ /** diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js new file mode 100644 index 000000000..6bf63152d --- /dev/null +++ b/src/structures/GuildEmoji.js @@ -0,0 +1,197 @@ +const Collection = require('../util/Collection'); +const Snowflake = require('../util/Snowflake'); +const Emoji = require('./Emoji'); +const { TypeError } = require('../errors'); + +/** + * 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; + + 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; + + 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} + * @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; + } + + /** + * Data for editing an emoji. + * @typedef {Object} GuildEmojiEditData + * @property {string} [name] The name of the emoji + * @property {Collection|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} + * @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} + */ + 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} + */ + addRestrictedRole(role) { + return this.addRestrictedRoles([role]); + } + + /** + * Adds multiple roles to the list of roles that can use this emoji. + * @param {Collection|RoleResolvable[]} roles Roles to add + * @returns {Promise} + */ + 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} + */ + removeRestrictedRole(role) { + return this.removeRestrictedRoles([role]); + } + + /** + * Removes multiple roles from the list of roles that can use this emoji. + * @param {Collection|RoleResolvable[]} roles Roles to remove + * @returns {Promise} + */ + 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 }); + } + + /** + * Deletes the emoji. + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise} + */ + 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 === this._roles + ); + } else { + return ( + other.id === this.id && + other.name === this.name && + other._roles === this._roles + ); + } + } +} + +module.exports = GuildEmoji; diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index b65721134..660967f0b 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,4 +1,4 @@ -const Emoji = require('./Emoji'); +const GuildEmoji = require('./GuildEmoji'); const ReactionEmoji = require('./ReactionEmoji'); const ReactionUserStore = require('../stores/ReactionUserStore'); @@ -31,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; diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index 94ea38930..9bb23c120 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -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; } } diff --git a/src/util/Structures.js b/src/util/Structures.js index a1cb7e156..e7f615c79 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -61,7 +61,7 @@ class Structures { } const structures = { - Emoji: require('../structures/Emoji'), + GuildEmoji: require('../structures/GuildEmoji'), DMChannel: require('../structures/DMChannel'), GroupDMChannel: require('../structures/GroupDMChannel'), TextChannel: require('../structures/TextChannel'), From c522b65adbfd004084fe1bb1d1d48e7f85bf13db Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 18 Jan 2018 17:54:45 +0000 Subject: [PATCH 083/154] Point discord.js-docgen dependency to its new location --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 208de266c..1a8639a9b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@types/node": "^8.0.0", - "discord.js-docgen": "hydrabolt/discord.js-docgen", + "discord.js-docgen": "discordjs/discord.js-docgen", "eslint": "^4.11.0", "jsdoc-strip-async-await": "^0.1.0", "json-filter-loader": "^1.0.0", From 16a910c98826ec62131af21b4286582ef7494f3f Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Thu, 18 Jan 2018 14:46:35 -0500 Subject: [PATCH 084/154] Update repository references --- .github/CONTRIBUTING.md | 2 +- README.md | 14 +++++++------- docs/general/updating.md | 4 ++-- docs/general/welcome.md | 10 +++++----- docs/topics/web.md | 2 +- package.json | 8 ++++---- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 69e3725fe..9f84b1073 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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) diff --git a/README.md b/README.md index 989d2d652..3d0f665d2 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Discord server NPM version NPM downloads - Build status - Dependencies + Build status + Dependencies Patreon

@@ -40,7 +40,7 @@ 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`) +- [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`) - 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`) @@ -67,21 +67,21 @@ client.login('your 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)) +* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](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 diff --git a/docs/general/updating.md b/docs/general/updating.md index cc2c7399f..0eab3f4b9 100644 --- a/docs/general/updating.md +++ b/docs/general/updating.md @@ -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) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 84b06ed33..803837b72 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -8,8 +8,8 @@ Discord server NPM version NPM downloads - Build status - Dependencies + Build status + Dependencies

NPM info @@ -68,18 +68,18 @@ client.login('your 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) +* [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)) ## 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 diff --git a/docs/topics/web.md b/docs/topics/web.md index 660651bb7..863051212 100644 --- a/docs/topics/web.md +++ b/docs/topics/web.md @@ -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: diff --git a/package.json b/package.json index 1a8639a9b..3f764c196 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/hydrabolt/discord.js.git" + "url": "git+https://github.com/discordjs/discord.js.git" }, "keywords": [ "discord", @@ -27,9 +27,9 @@ "author": "Amish Shah ", "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", "dependencies": { "pako": "^1.0.0", @@ -50,7 +50,7 @@ }, "devDependencies": { "@types/node": "^8.0.0", - "discord.js-docgen": "discordjs/discord.js-docgen", + "discord.js-docgen": "discordjs/docgen", "eslint": "^4.11.0", "jsdoc-strip-async-await": "^0.1.0", "json-filter-loader": "^1.0.0", From 95d35a9efa47c759cede27aa1be4c7a3be2c7537 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Thu, 18 Jan 2018 19:48:43 -0500 Subject: [PATCH 085/154] Update typings submodule URL --- .gitmodules | 2 +- typings | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index d5aa0ecce..44fff6d5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "typings"] path = typings - url = https://github.com/zajrik/discord.js-typings + url = https://github.com/discordjs/discord.js-typings diff --git a/typings b/typings index 829f4271f..895af7f3d 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 829f4271fed258be72151c32d471c9cd411d052a +Subproject commit 895af7f3dad233139b8246fe0e44079867e6cc95 From e9bdd3ad7eb73aac8b52e4cbc141e7a7764f39e4 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Thu, 18 Jan 2018 19:59:21 -0500 Subject: [PATCH 086/154] Tweak readme and docs welcome page --- README.md | 32 ++++++++++++++++---------------- docs/general/welcome.md | 41 ++++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 3d0f665d2..6677c38ae 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Patreon

- NPM info + npm installnfo

@@ -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,16 +54,16 @@ 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 @@ -76,7 +76,7 @@ client.login('your token'); * [Related libraries](https://discordapi.com/unofficial/libs.html) ### Extensions -* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](https://github.com/discordjs/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 diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 803837b72..ace4a20a9 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -17,7 +17,10 @@ # 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,26 +59,29 @@ 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/discordjs/website)) * [Documentation](https://discord.js.org/#/docs) -* [Discord.js server](https://discord.gg/bRCvFy9) -* [Discord API server](https://discord.gg/rV4BwdK) +* [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 From e7375aa0fd1b696d05bddf5e465ff26fb78c90af Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 14:32:51 +0000 Subject: [PATCH 087/154] Reimplement broadcast (un)subscribe events --- src/client/voice/VoiceBroadcast.js | 3 +- src/client/voice/util/DispatcherSet.js | 40 ++++++++++++++++++++++++++ src/util/Constants.js | 2 ++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/client/voice/util/DispatcherSet.js diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index c709eb64c..421988a65 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,5 +1,6 @@ const EventEmitter = require('events'); const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer'); +const DispatcherSet = require('./util/DispatcherSet'); /** * A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency. @@ -23,7 +24,7 @@ class VoiceBroadcast extends EventEmitter { * @type {Client} */ this.client = client; - this.dispatchers = new Set(); + this.dispatchers = new DispatcherSet(this); this.player = new BroadcastAudioPlayer(this); } diff --git a/src/client/voice/util/DispatcherSet.js b/src/client/voice/util/DispatcherSet.js new file mode 100644 index 000000000..a1ab7e943 --- /dev/null +++ b/src/client/voice/util/DispatcherSet.js @@ -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; diff --git a/src/util/Constants.js b/src/util/Constants.js index 69e4f4995..bf6106cc1 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -234,6 +234,8 @@ exports.Events = { USER_GUILD_SETTINGS_UPDATE: 'clientUserGuildSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', + VOICE_BROADCAST_SUBSCRIBE: 'subscribe', + VOICE_BROADCAST_UNSUBSCRIBE: 'unsubscribe', TYPING_START: 'typingStart', TYPING_STOP: 'typingStop', DISCONNECT: 'disconnect', From 3b1c5d3494aed2d4d428cda5388f7cc7da262e69 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 14:39:51 +0000 Subject: [PATCH 088/154] Expose VoiceBroadcast#dispatcher so that the broadcast can be controlled --- src/client/voice/VoiceBroadcast.js | 8 ++++++++ src/client/voice/player/BroadcastAudioPlayer.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 421988a65..8f83c4749 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -28,6 +28,14 @@ class VoiceBroadcast extends EventEmitter { this.player = new BroadcastAudioPlayer(this); } + /** + * The current master dispatcher, if any. This dispatcher controls all that is played by subscribed dispatchers. + * @type {?BroadcastDispatcher} + */ + get dispatcher() { + return this.player.dispatcher; + } + /** * Plays the given file in the voice connection. * @param {string} file The absolute path to the file diff --git a/src/client/voice/player/BroadcastAudioPlayer.js b/src/client/voice/player/BroadcastAudioPlayer.js index a26f90efc..052c9ea0b 100644 --- a/src/client/voice/player/BroadcastAudioPlayer.js +++ b/src/client/voice/player/BroadcastAudioPlayer.js @@ -18,7 +18,7 @@ class AudioPlayer extends BasePlayer { createDispatcher(options, streams) { this.destroyDispatcher(); - const dispatcher = new BroadcastDispatcher(this, options, streams); + const dispatcher = this.dispatcher = new BroadcastDispatcher(this, options, streams); return dispatcher; } } From 580bda46ea6b2db704f45590731104a5e2935a0c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 14:40:58 +0000 Subject: [PATCH 089/154] Fix documentation for VoiceBroadcast (play methods return BroadcastDispatcher not StreamDispatcher) --- src/client/voice/VoiceBroadcast.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 8f83c4749..f06ae96c6 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -40,7 +40,7 @@ class VoiceBroadcast extends EventEmitter { * 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} + * @returns {BroadcastDispatcher} * @example * // Play files natively * voiceChannel.join() @@ -57,7 +57,7 @@ class VoiceBroadcast extends EventEmitter { * 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} + * @returns {BroadcastDispatcher} */ playArbitraryInput(input, options) { return this.player.playUnknown(input, options); @@ -67,7 +67,7 @@ class VoiceBroadcast extends EventEmitter { * 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} + * @returns {BroadcastDispatcher} * @example * // Play streams using ytdl-core * const ytdl = require('ytdl-core'); @@ -87,7 +87,7 @@ class VoiceBroadcast extends EventEmitter { * 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} + * @returns {BroadcastDispatcher} */ playConvertedStream(stream, options) { return this.player.playPCMStream(stream, options); @@ -98,7 +98,7 @@ class VoiceBroadcast extends EventEmitter { * Note that inline volume is not compatible with this method. * @param {ReadableStream} stream The Opus audio stream to play * @param {StreamOptions} [options] Options for playing the stream - * @returns {StreamDispatcher} + * @returns {BroadcastDispatcher} */ playOpusStream(stream, options) { return this.player.playOpusStream(stream, options); From 6058ea488890267403f22362b35955035e5cc588 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 17:45:37 +0000 Subject: [PATCH 090/154] Start voice receive rewrite --- src/client/voice/receiver/PacketHandler.js | 44 ++++++++++++++++++++++ src/client/voice/receiver/Receiver.js | 16 ++++++++ src/client/voice/receiver/VoiceReceiver.js | 2 +- 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/client/voice/receiver/PacketHandler.js create mode 100644 src/client/voice/receiver/Receiver.js diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js new file mode 100644 index 000000000..b6eb09378 --- /dev/null +++ b/src/client/voice/receiver/PacketHandler.js @@ -0,0 +1,44 @@ +const nonce = Buffer.alloc(24); + +class PacketHandler { + constructor(receiver) { + this.receiver = receiver; + } + + parseBuffer(buffer) { + // reuse nonce + buffer.copy(nonce, 0, 0, 12); + + let packet = secretbox.methods.open(buffer.slice(12), nonce, this.receiver.connection.authentication.secretKey); + if (!packet) return 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 opusPacket = this.parseBuffer(buffer); + if (opusPacket instanceof Error) return; + } +} + +module.exports = PacketHandler; diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js new file mode 100644 index 000000000..fa386c06a --- /dev/null +++ b/src/client/voice/receiver/Receiver.js @@ -0,0 +1,16 @@ +const EventEmitter = require('events'); + +class VoiceReceiver extends EventEmitter { + constructor(connection) { + super(); + this.connection = connection; + this.packets = new PacketHandler(this); + this.connection.sockets.udp.socket.on('message', buffer => this.packets.push(buffer)); + } + + createStream(user, pcm=false) { + + } +} + +module.exports = VoiceReceiver; diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index bc7a87061..07c4894bd 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -131,7 +131,7 @@ class VoiceReceiver extends EventEmitter { 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 From 2c1a302eeab9cb971b3b43d511e4511e4f8becdd Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 21:14:03 +0000 Subject: [PATCH 091/154] Redesign voice receiving, still needs cleaning up --- src/client/voice/VoiceConnection.js | 2 +- src/client/voice/receiver/PacketHandler.js | 13 ++++++++++++ src/client/voice/receiver/Receiver.js | 17 +++++++++++++++- test/voice.js | 23 ++-------------------- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 1f1c7a18a..064d1c8f9 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -3,7 +3,7 @@ const VoiceUDP = require('./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 { Error } = require('../../errors'); diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index b6eb09378..27e094557 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -1,8 +1,18 @@ const nonce = Buffer.alloc(24); +const Readable = require('./VoiceReadable'); +const secretbox = require('../util/Secretbox'); class PacketHandler { constructor(receiver) { 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) { @@ -36,8 +46,11 @@ class PacketHandler { 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) return; + stream.push(opusPacket); } } diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index fa386c06a..cfa217be4 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -1,15 +1,30 @@ const EventEmitter = require('events'); +const prism = require('prism-media'); +const PacketHandler = require('./PacketHandler'); class VoiceReceiver extends EventEmitter { constructor(connection) { super(); this.connection = connection; - this.packets = new PacketHandler(this); + this.packets = new PacketHandler(this); this.connection.sockets.udp.socket.on('message', buffer => this.packets.push(buffer)); } createStream(user, pcm=false) { + user = this.connection.client.users.resolve(user); + if (!user) throw new Error('VOICE_USER_MISSING'); + console.log('making stream for', user.tag); + const stream = this.packets.makeStream(user.id); + if (pcm) { + const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 1920 }); + stream.pipe(decoder); + return decoder; + } + return stream; + } + stoppedSpeaking() { + console.log('remember to remove this :)'); } } diff --git a/test/voice.js b/test/voice.js index 29e1eacc9..2f57ee8dc 100644 --- a/test/voice.js +++ b/test/voice.js @@ -1,11 +1,6 @@ /* eslint no-console: 0 */ 'use strict'; -var profiler = require('gc-profiler'); -profiler.on('gc', function (info) { - console.log(info); -}); - const Discord = require('../'); const ytdl = require('ytdl-core'); const prism = require('prism-media'); @@ -32,22 +27,6 @@ async function wait(time = 1000) { var count = 0; -client.on('ready', async () => { - for (const guild of client.guilds.values()) { - const channels = guild.channels.filter(c => c.type === 'voice' && c.joinable && c.members.size === 0); - const channel = channels.first(); - if (channel) { - channel.join().then(conn => { - conn.playOpusStream(fs.createReadStream('C:/users/amish/downloads/z.ogg').pipe(new prism.OggOpusDemuxer())); - }); - count++; - console.log(`Playing in ${channel.name} in ${channel.guild.name} at count ${count} ${process.memoryUsage().rss / (1024 * 1024)}`); - await wait(); - } - } - console.log('done!'); -}); - process.on('unhandledRejection', console.log); client.on('message', m => { @@ -57,6 +36,8 @@ client.on('message', m => { const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel; if (channel && channel.type === 'voice') { channel.join().then(conn => { + const receiver = conn.createReceiver(); + receiver.createStream(m.author, true).on('data', b => console.log(b.toString())); conn.player.on('error', (...e) => console.log('player', ...e)); if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); From 4a1b55d145bdad7383b6bcf56c930c8cd205db08 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:38:10 +0000 Subject: [PATCH 092/154] Receiver#createStream should take options --- src/client/voice/receiver/Receiver.js | 4 ++-- src/client/voice/receiver/VoiceReadable.js | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index cfa217be4..07163ab23 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -10,12 +10,12 @@ class VoiceReceiver extends EventEmitter { this.connection.sockets.udp.socket.on('message', buffer => this.packets.push(buffer)); } - createStream(user, pcm=false) { + createStream(user, { mode = 'opus' } = {}) { user = this.connection.client.users.resolve(user); if (!user) throw new Error('VOICE_USER_MISSING'); console.log('making stream for', user.tag); const stream = this.packets.makeStream(user.id); - if (pcm) { + if (mode === 'pcm') { const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 1920 }); stream.pipe(decoder); return decoder; diff --git a/src/client/voice/receiver/VoiceReadable.js b/src/client/voice/receiver/VoiceReadable.js index b29d37aad..0c4665dbc 100644 --- a/src/client/voice/receiver/VoiceReadable.js +++ b/src/client/voice/receiver/VoiceReadable.js @@ -3,15 +3,9 @@ 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; From dd618584d016e4b036e5c79909d8b99980d628fa Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:38:39 +0000 Subject: [PATCH 093/154] Simplify VolumeInterface constructor --- src/client/voice/util/VolumeInterface.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index 5a03e6677..f3bafe1bb 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -5,9 +5,9 @@ const EventEmitter = require('events'); * @extends {EventEmitter} */ class VolumeInterface extends EventEmitter { - constructor({ volume = 0 } = {}) { + constructor({ volume = 1 } = {}) { super(); - this.setVolume(volume || 1); + this.setVolume(volume); } /** From 83140f11b729bcd55ca1c874a17e9a18b20617cb Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:41:31 +0000 Subject: [PATCH 094/154] Fix ESLint and inline the voice readable stream --- src/client/voice/receiver/PacketHandler.js | 7 ++++--- src/client/voice/receiver/Receiver.js | 3 +-- src/client/voice/receiver/VoiceReadable.js | 8 +------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 27e094557..8fcd3d089 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -1,7 +1,8 @@ const nonce = Buffer.alloc(24); -const Readable = require('./VoiceReadable'); const secretbox = require('../util/Secretbox'); +class Readable extends require('stream').Readable { _read() {} } // eslint-disable-line no-empty-function + class PacketHandler { constructor(receiver) { this.receiver = receiver; @@ -16,13 +17,13 @@ class PacketHandler { } parseBuffer(buffer) { - // reuse nonce + // 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 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); diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index 07163ab23..6e63ade5a 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -13,7 +13,6 @@ class VoiceReceiver extends EventEmitter { createStream(user, { mode = 'opus' } = {}) { user = this.connection.client.users.resolve(user); if (!user) throw new Error('VOICE_USER_MISSING'); - console.log('making stream for', user.tag); const stream = this.packets.makeStream(user.id); if (mode === 'pcm') { const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 1920 }); @@ -24,7 +23,7 @@ class VoiceReceiver extends EventEmitter { } stoppedSpeaking() { - console.log('remember to remove this :)'); + return false; } } diff --git a/src/client/voice/receiver/VoiceReadable.js b/src/client/voice/receiver/VoiceReadable.js index 0c4665dbc..db27bf9f5 100644 --- a/src/client/voice/receiver/VoiceReadable.js +++ b/src/client/voice/receiver/VoiceReadable.js @@ -1,11 +1,5 @@ const { Readable } = require('stream'); -class VoiceReadable extends Readable { - constructor() { - super(); - } - - _read() {} // eslint-disable-line no-empty-function -} +class VoiceReadable extends Readable { _read() {} } // eslint-disable-line no-empty-function module.exports = VoiceReadable; From cb161a8a40dd2563f2642a17bcbd14b763ae7440 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:47:01 +0000 Subject: [PATCH 095/154] Implement Receiver debug events --- src/client/voice/VoiceConnection.js | 5 ----- src/client/voice/receiver/PacketHandler.js | 5 ++++- src/client/voice/receiver/Receiver.js | 10 ++++++---- src/client/voice/receiver/VoiceReceiver.js | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 064d1c8f9..4147fdf94 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -415,11 +415,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 diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 8fcd3d089..1e2115e54 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -50,7 +50,10 @@ class PacketHandler { const stream = this.streams.get(user.id); if (!stream) return; const opusPacket = this.parseBuffer(buffer); - if (opusPacket instanceof Error) return; + if (opusPacket instanceof Error) { + this.emit('error', opusPacket); + return; + } stream.push(opusPacket); } } diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index 6e63ade5a..c6b03ea2d 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -7,6 +7,12 @@ class VoiceReceiver extends EventEmitter { 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)); } @@ -21,10 +27,6 @@ class VoiceReceiver extends EventEmitter { } return stream; } - - stoppedSpeaking() { - return false; - } } module.exports = VoiceReceiver; diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index 07c4894bd..bc7a87061 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -131,7 +131,7 @@ class VoiceReceiver extends EventEmitter { 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 From c6c9c0918a6c35ca47ca9fb187fb474d404a9f99 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:51:26 +0000 Subject: [PATCH 096/154] Fix PacketHandler runtime error (EventEmitter not imported) --- src/client/voice/receiver/PacketHandler.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 1e2115e54..2227d55cf 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -1,10 +1,12 @@ 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 { +class PacketHandler extends EventEmitter { constructor(receiver) { + super(); this.receiver = receiver; this.streams = new Map(); } From 60c5c1486b2048810ed44f7e45f31d35fa63905d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:51:53 +0000 Subject: [PATCH 097/154] Remove dead files --- src/client/voice/opus/BaseOpusEngine.js | 60 ------ src/client/voice/opus/NodeOpusEngine.js | 40 ---- src/client/voice/opus/OpusEngineList.js | 30 --- src/client/voice/opus/OpusScriptEngine.js | 45 ----- src/client/voice/receiver/VoiceReadable.js | 5 - src/client/voice/receiver/VoiceReceiver.js | 220 --------------------- 6 files changed, 400 deletions(-) delete mode 100644 src/client/voice/opus/BaseOpusEngine.js delete mode 100644 src/client/voice/opus/NodeOpusEngine.js delete mode 100644 src/client/voice/opus/OpusEngineList.js delete mode 100644 src/client/voice/opus/OpusScriptEngine.js delete mode 100644 src/client/voice/receiver/VoiceReadable.js delete mode 100644 src/client/voice/receiver/VoiceReceiver.js diff --git a/src/client/voice/opus/BaseOpusEngine.js b/src/client/voice/opus/BaseOpusEngine.js deleted file mode 100644 index a51044905..000000000 --- a/src/client/voice/opus/BaseOpusEngine.js +++ /dev/null @@ -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; diff --git a/src/client/voice/opus/NodeOpusEngine.js b/src/client/voice/opus/NodeOpusEngine.js deleted file mode 100644 index 02e880637..000000000 --- a/src/client/voice/opus/NodeOpusEngine.js +++ /dev/null @@ -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; diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js deleted file mode 100644 index 01e3ff6d1..000000000 --- a/src/client/voice/opus/OpusEngineList.js +++ /dev/null @@ -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'); -}; diff --git a/src/client/voice/opus/OpusScriptEngine.js b/src/client/voice/opus/OpusScriptEngine.js deleted file mode 100644 index a5e046d40..000000000 --- a/src/client/voice/opus/OpusScriptEngine.js +++ /dev/null @@ -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; diff --git a/src/client/voice/receiver/VoiceReadable.js b/src/client/voice/receiver/VoiceReadable.js deleted file mode 100644 index db27bf9f5..000000000 --- a/src/client/voice/receiver/VoiceReadable.js +++ /dev/null @@ -1,5 +0,0 @@ -const { Readable } = require('stream'); - -class VoiceReadable extends Readable { _read() {} } // eslint-disable-line no-empty-function - -module.exports = VoiceReadable; diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js deleted file mode 100644 index bc7a87061..000000000 --- a/src/client/voice/receiver/VoiceReceiver.js +++ /dev/null @@ -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. - * Any streams that you had prior to destroying the receiver will not be recreated. - */ - 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); - 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; From 066fbfe3304466c6b415a6c63033c3c5994b63f8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:53:55 +0000 Subject: [PATCH 098/154] Move voice UDP client and Websocket client to networking folder --- package.json | 15 ++++----------- src/client/voice/VoiceConnection.js | 4 ++-- .../voice/{ => networking}/VoiceUDPClient.js | 0 .../voice/{ => networking}/VoiceWebSocket.js | 0 4 files changed, 6 insertions(+), 13 deletions(-) rename src/client/voice/{ => networking}/VoiceUDPClient.js (100%) rename src/client/voice/{ => networking}/VoiceWebSocket.js (100%) diff --git a/package.json b/package.json index cd7d010cc..16e7f5b2b 100644 --- a/package.json +++ b/package.json @@ -75,19 +75,12 @@ "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/VolumeInterface.js": false, "src/client/voice/VoiceBroadcast.js": false diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 4147fdf94..700db600e 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -1,5 +1,5 @@ -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'); diff --git a/src/client/voice/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js similarity index 100% rename from src/client/voice/VoiceUDPClient.js rename to src/client/voice/networking/VoiceUDPClient.js diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js similarity index 100% rename from src/client/voice/VoiceWebSocket.js rename to src/client/voice/networking/VoiceWebSocket.js From c57c2889b7b088946e449986c843d70e5aa8a7ac Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 22:54:54 +0000 Subject: [PATCH 099/154] Fix import errors for networking classes --- src/client/voice/networking/VoiceUDPClient.js | 4 ++-- src/client/voice/networking/VoiceWebSocket.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index 813d3a34b..38dd389f3 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -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. diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 8eefe32d8..19c0a2127 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -1,7 +1,7 @@ -const { OPCodes, VoiceOPCodes } = require('../../util/Constants'); +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. From 8e5e1ad8fe8e76b53d0f25bd75eda62381bf2b5c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 23:03:01 +0000 Subject: [PATCH 100/154] Document Receiver --- src/client/voice/receiver/Receiver.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index c6b03ea2d..51beb682c 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -2,6 +2,13 @@ const EventEmitter = require('events'); const prism = require('prism-media'); const PacketHandler = require('./PacketHandler'); +/** + * 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(); @@ -16,6 +23,21 @@ class VoiceReceiver extends EventEmitter { 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'); From 2b5fc77a67de2d9143a31e18ab95d3452aff7360 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 19 Jan 2018 23:55:59 +0000 Subject: [PATCH 101/154] =?UTF-8?q?Rudimentary=20support=20for=20unified?= =?UTF-8?q?=20audio=20playing!=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/voice/VoiceBroadcast.js | 72 +---------------- src/client/voice/VoiceConnection.js | 104 +------------------------ src/client/voice/util/PlayInterface.js | 55 +++++++++++++ src/errors/Messages.js | 2 + test/voice.js | 2 +- 5 files changed, 66 insertions(+), 169 deletions(-) create mode 100644 src/client/voice/util/PlayInterface.js diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index f06ae96c6..d8bcf9ede 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,6 +1,7 @@ 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. @@ -15,6 +16,7 @@ const DispatcherSet = require('./util/DispatcherSet'); * } * ``` * @implements {VolumeInterface} + * @implements {PlayInterface} */ class VoiceBroadcast extends EventEmitter { constructor(client) { @@ -35,74 +37,8 @@ class VoiceBroadcast extends EventEmitter { get dispatcher() { return this.player.dispatcher; } - - /** - * 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 {BroadcastDispatcher} - * @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.playUnknown(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 {BroadcastDispatcher} - */ - playArbitraryInput(input, options) { - return this.player.playUnknown(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 {BroadcastDispatcher} - * @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.playUnknown(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 {BroadcastDispatcher} - */ - playConvertedStream(stream, options) { - return this.player.playPCMStream(stream, options); - } - - /** - * Plays an Opus encoded stream. - * Note that inline volume is not compatible with this method. - * @param {ReadableStream} stream The Opus audio stream to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {BroadcastDispatcher} - */ - playOpusStream(stream, options) { - return this.player.playOpusStream(stream, options); - } } +PlayInterface.applyToClass(VoiceBroadcast); + module.exports = VoiceBroadcast; diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 700db600e..5543f96ff 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -6,6 +6,7 @@ const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/Receiver'); const EventEmitter = require('events'); const { Error } = require('../../errors'); +const PlayInterface = require('./util/PlayInterface'); /** * Represents a connection to a guild's voice server. @@ -17,6 +18,7 @@ const { Error } = require('../../errors'); * }); * ``` * @extends {EventEmitter} + * @implements {PlayInterface} */ class VoiceConnection extends EventEmitter { constructor(voiceManager, channel) { @@ -425,106 +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|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=8] 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. - */ - - /** - * 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.playUnknown(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.playUnknown(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.playUnknown(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. - * Note that inline volume is not compatible with this method. - * @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. @@ -537,4 +439,6 @@ class VoiceConnection extends EventEmitter { } } +PlayInterface.applyToClass(VoiceConnection); + module.exports = VoiceConnection; diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js new file mode 100644 index 000000000..1c3eaeef1 --- /dev/null +++ b/src/client/voice/util/PlayInterface.js @@ -0,0 +1,55 @@ +/** + * Options that can be passed to stream-playing methods: + * @typedef {Object} StreamOptions + * @property {string} [type='unknown'] The type of stream. 'unknown', 'converted', 'opus', 'broadcast. + * @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 interface class to allow you to play audio over VoiceConnections and VoiceBroadcasts. + */ +class PlayInterface { + constructor(player) { + this.player = player; + } + + /** + * Play an audio resource. + * @param {ReadableStream|string} resource The resource to play. + * @param {StreamOptions} [options] The options to play. + * @returns {StreamDispatcher} + */ + play(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 === 'broadcast') { + if (!this.player.playBroadcast) throw Error('VOICE_PLAY_INTERFACE_NO_BROADCAST'); + return this.player.playBroadcast(resource, options); + } + throw 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; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 70ee8d47e..91956cf99 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -54,6 +54,8 @@ 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', OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', diff --git a/test/voice.js b/test/voice.js index 2f57ee8dc..07cc3a4bf 100644 --- a/test/voice.js +++ b/test/voice.js @@ -42,7 +42,7 @@ client.on('message', m => { if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); // conn.playOpusStream(fs.createReadStream('C:/users/amish/downloads/z.ogg').pipe(new prism.OggOpusDemuxer())); - d = conn.playStream(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); + d = conn.play(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); }); } else { m.reply('Specify a voice channel!'); From ef02bd29354a301a228d7327a338ee9aa51922a3 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 00:05:37 +0000 Subject: [PATCH 102/154] Add stubs for docs --- src/client/voice/VoiceBroadcast.js | 2 ++ src/client/voice/VoiceConnection.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index d8bcf9ede..6258cd4d3 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -37,6 +37,8 @@ class VoiceBroadcast extends EventEmitter { get dispatcher() { return this.player.dispatcher; } + + play() {} // eslint-disable-line no-empty-function } PlayInterface.applyToClass(VoiceBroadcast); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 5543f96ff..f99e3c265 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -437,6 +437,8 @@ class VoiceConnection extends EventEmitter { this.receivers.push(receiver); return receiver; } + + play() {} // eslint-disable-line no-empty-function } PlayInterface.applyToClass(VoiceConnection); From bf0a68dbac7b0fa551041089898034e631360005 Mon Sep 17 00:00:00 2001 From: Yukine Date: Sat, 20 Jan 2018 08:00:44 +0100 Subject: [PATCH 103/154] Mark DataStores as public to directly display them in the docs. (#2268) * make EmojiStore not Private anymore. because why have something private if there is priority functionality on that class? also that causes that the docs wont show it directly * make GuildChannelStore not private anymore because why have something private if there is priority functionality on that class? also that causes that the docs wont show it directly * make RoleStore not private anymore because why have something private if there is priority functionality on that class? also that causes that the docs wont show it directly * make ReactionStore not private anymore because why have something private if there is priority functionality on that class? also that causes that the docs wont show it directly * make all non private to stay consistent * fix merge conflicts because of other PRs. --- src/stores/ChannelStore.js | 1 - src/stores/ClientPresenceStore.js | 1 - src/stores/GuildChannelStore.js | 1 - src/stores/GuildEmojiStore.js | 1 - src/stores/GuildStore.js | 1 - src/stores/PresenceStore.js | 1 - src/stores/ReactionStore.js | 1 - src/stores/RoleStore.js | 1 - 8 files changed, 8 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 53a81358c..6e2e4081a 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -7,7 +7,6 @@ const lruable = ['group', 'dm']; /** * Stores channels. - * @private * @extends {DataStore} */ class ChannelStore extends DataStore { diff --git a/src/stores/ClientPresenceStore.js b/src/stores/ClientPresenceStore.js index 12213059c..a6eedb7ce 100644 --- a/src/stores/ClientPresenceStore.js +++ b/src/stores/ClientPresenceStore.js @@ -7,7 +7,6 @@ const { TypeError } = require('../errors'); /** * Stores the client presence and other presences. * @extends {PresenceStore} - * @private */ class ClientPresenceStore extends PresenceStore { constructor(...args) { diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index f37c58853..c4d0c6fea 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -7,7 +7,6 @@ const Permissions = require('../util/Permissions'); /** * Stores guild channels. - * @private * @extends {DataStore} */ class GuildChannelStore extends DataStore { diff --git a/src/stores/GuildEmojiStore.js b/src/stores/GuildEmojiStore.js index 0fc4cc60b..bc5c57280 100644 --- a/src/stores/GuildEmojiStore.js +++ b/src/stores/GuildEmojiStore.js @@ -6,7 +6,6 @@ const DataResolver = require('../util/DataResolver'); /** * Stores guild emojis. - * @private * @extends {DataStore} */ class GuildEmojiStore extends DataStore { diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 7b7810aee..35701087f 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -5,7 +5,6 @@ const Guild = require('../structures/Guild'); /** * Stores guilds. - * @private * @extends {DataStore} */ class GuildStore extends DataStore { diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index 1b927934e..79f0c525c 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -3,7 +3,6 @@ const { Presence } = require('../structures/Presence'); /** * Stores presences. - * @private * @extends {DataStore} */ class PresenceStore extends DataStore { diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 4a689a447..38c467b79 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -3,7 +3,6 @@ const MessageReaction = require('../structures/MessageReaction'); /** * Stores reactions. - * @private * @extends {DataStore} */ class ReactionStore extends DataStore { diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 2b5f9ee10..55eeaa944 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -5,7 +5,6 @@ const Permissions = require('../util/Permissions'); /** * Stores roles. - * @private * @extends {DataStore} */ class RoleStore extends DataStore { From a22b856494a8c599c9d1acea4edb8a210c1a12d1 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sat, 20 Jan 2018 09:05:07 +0100 Subject: [PATCH 104/154] fix(WebSocketConnection): make errors in event handlers throw again The error from something like client.on('ready', () => undefined.f); would just be emitted as debug event instead of being thrown. Simply moving the emitting part out of the try catch again solves this. --- src/client/websocket/WebSocketConnection.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index 4e3835ef0..5e07b1a69 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -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); } /** From fbd25f867742bac5511e47bbf69274bdc82ccb23 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sat, 20 Jan 2018 12:44:27 +0100 Subject: [PATCH 105/154] fix(GuildMember): make edit method only modify a copy of the voice state This is to fix stale members in voice channels. --- src/structures/GuildMember.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 08561c757..038527929 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -357,7 +357,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; From f14193b93a881d4c5bb383419d0f57fbecb6451b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 12:48:28 +0000 Subject: [PATCH 106/154] Document examples --- src/client/voice/util/PlayInterface.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 1c3eaeef1..07130c31b 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -27,10 +27,19 @@ class PlayInterface { * Play an audio resource. * @param {ReadableStream|string} resource The resource to play. * @param {StreamOptions} [options] The options to play. + * @example + * // Play a local audio file + * connection.play('/home/hydrabolt/audio.mp3', { volume: 0.5 }); + * + * // Play a ReadableStream + * connection.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' })); + * + * // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html + * connection.play('http://www.sample-videos.com/audio/mp3/wave.mp3'); * @returns {StreamDispatcher} */ play(resource, options = {}) { - const type = options.type || 'unknown'; + const type = resource instanceof Broadcast ? options.type || 'unknown' : 'broadcast'; if (type === 'unknown') { return this.player.playUnknown(resource, options); } else if (type === 'converted') { @@ -53,3 +62,5 @@ class PlayInterface { } module.exports = PlayInterface; + +const Broadcast = require('../VoiceBroadcast'); From 791740220e500ee41f95fdd1c8f82c10ec5cbe62 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 13:01:48 +0000 Subject: [PATCH 107/154] Improve docs, allow for webm/opus demuxing and playing broadcasts without specifying a type --- src/client/voice/util/PlayInterface.js | 42 +++++++++++++++++++++----- src/errors/Messages.js | 1 + 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 07130c31b..f814683d3 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -1,7 +1,10 @@ +const { Readable } = require('stream'); +const prism = require('prism-media'); + /** * Options that can be passed to stream-playing methods: * @typedef {Object} StreamOptions - * @property {string} [type='unknown'] The type of stream. 'unknown', 'converted', 'opus', 'broadcast. + * @property {StreamType} [type='unknown'] The type of stream. 'unknown', 'converted', 'opus', 'broadcast. * @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. @@ -15,6 +18,17 @@ * 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. */ @@ -25,30 +39,42 @@ class PlayInterface { /** * Play an audio resource. - * @param {ReadableStream|string} resource The resource to play. + * @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 = {}) { - const type = resource instanceof Broadcast ? options.type || 'unknown' : 'broadcast'; + if (resource instanceof Broadcast) { + if (!this.player.playBroadcast) throw 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 === 'broadcast') { - if (!this.player.playBroadcast) throw Error('VOICE_PLAY_INTERFACE_NO_BROADCAST'); - return this.player.playBroadcast(resource, options); + } else if (type === 'ogg/opus') { + if (!(resource instanceof Readable)) throw 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 Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); + return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer())); } throw Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); } diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 91956cf99..a57ef6eed 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -56,6 +56,7 @@ const Messages = { 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.', From ca96e1478a4123302bb5811b3f1d1ebffe5e1438 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 13:45:22 +0000 Subject: [PATCH 108/154] Make docs technically correct --- src/client/voice/VoiceBroadcast.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 6258cd4d3..e81157ece 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -38,7 +38,27 @@ class VoiceBroadcast extends EventEmitter { return this.player.dispatcher; } - play() {} // eslint-disable-line no-empty-function + /** + * Play an audio resource. + * @param {ReadableStream|string} resource The resource to play. + * @param {StreamOptions} [options] The options to play. + * @example + * // Play a local audio file + * 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 {BroadcastDispatcher} + */ + play() { return null; } } PlayInterface.applyToClass(VoiceBroadcast); From c63bdb5fb16514cf568d5052bddc6dd48551ee7c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 13:47:35 +0000 Subject: [PATCH 109/154] Remove redundant doc tag descriptions --- src/client/voice/util/PlayInterface.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index f814683d3..e607bfd90 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -4,7 +4,7 @@ const prism = require('prism-media'); /** * Options that can be passed to stream-playing methods: * @typedef {Object} StreamOptions - * @property {StreamType} [type='unknown'] The type of stream. 'unknown', 'converted', 'opus', 'broadcast. + * @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. From ac65ea41b4fae883c12ec32809f13ed1c485c780 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 16:26:44 +0000 Subject: [PATCH 110/154] Update voice docs --- docs/topics/voice.md | 131 ++++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 57 deletions(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index c7e88d04b..2493ff380 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -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,94 @@ 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 +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') +``` + +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â„¢ From f728d2be692813c43802c438506b1216b137bdb5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 16:31:34 +0000 Subject: [PATCH 111/154] Update voice docs --- docs/topics/voice.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index 2493ff380..0bfe45054 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -79,12 +79,16 @@ 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') +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: From b16e6f8262de7bcba70499739cf90d74c5720c94 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 20 Jan 2018 16:57:27 +0000 Subject: [PATCH 112/154] Fix VoiceBroadcast#play documentation --- src/client/voice/VoiceBroadcast.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index e81157ece..6dcb00cd6 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -44,18 +44,13 @@ class VoiceBroadcast extends EventEmitter { * @param {StreamOptions} [options] The options to play. * @example * // Play a local audio file - * connection.play('/home/hydrabolt/audio.mp3', { volume: 0.5 }); + * broadcast.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); + * broadcast.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' })); * @example * // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html - * connection.play('http://www.sample-videos.com/audio/mp3/wave.mp3'); + * broadcast.play('http://www.sample-videos.com/audio/mp3/wave.mp3'); * @returns {BroadcastDispatcher} */ play() { return null; } From 986e6da196525ddf64d666a71c5dc70a450e6cd1 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 20 Jan 2018 19:30:30 +0100 Subject: [PATCH 113/154] Fix(GuildChannel#clone) options.parent not accepting (falsy) null. (#2262) * Fixed (falsy) options not being set correctly * Requested changes. As a side note, I also default `options.withPermissions` for simplicity, and because it gets ignored in [`GuildChannelStore#create()`](https://discord.js.org/#/docs/main/master/class/GuildChannelStore?scrollTo=create). * Fixed the overwrites option --- src/structures/GuildChannel.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index c4d4c7bbf..a79f11bae 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -445,18 +445,21 @@ class GuildChannel extends Channel { * @param {string} [options.reason] Reason for cloning this channel * @returns {Promise} */ - clone({ name = this.name, withPermissions = true, withTopic = true, nsfw, parent, bitrate, userLimit, reason } = {}) { - const options = { - overwrites: withPermissions ? this.permissionOverwrites : [], - nsfw: typeof nsfw === 'undefined' ? this.nsfw : nsfw, - parent: parent || this.parent, - bitrate: bitrate || this.bitrate, - userLimit: userLimit || this.userLimit, - reason, - type: this.type, - }; - return this.guild.channels.create(name, 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); } /** From 93e083da4fd8a936d534abb6d16e42f37837da4d Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 21 Jan 2018 07:30:59 +0100 Subject: [PATCH 114/154] fix(Guild): memberCount not decrementing when an uncached member leaves This leads to GuildMemberStore#_fetchMany to always reject because it expects more member than possible. Also no longer call the GuildMemberRemove handler locally to not decrement twice. --- src/client/actions/GuildMemberRemove.js | 2 +- src/structures/GuildMember.js | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index b649eba8d..95bff6abf 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -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); } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 038527929..bee96c814 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -526,12 +526,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); } /** From 0e262ea8d7e146a91e330806539856a138cf4ee0 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 21 Jan 2018 10:29:03 +0000 Subject: [PATCH 115/154] More informative stream errors --- src/client/voice/dispatcher/StreamDispatcher.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 7513ec6b0..26cc33630 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -69,20 +69,23 @@ class StreamDispatcher extends Writable { if (typeof plp !== 'undefined') this.setPLP(plp); if (typeof bitrate !== 'undefined') this.setBitrate(bitrate); - const streamError = err => { + const streamError = (type, err) => { /** * Emitted when the dispatcher encounters an error. * @event StreamDispatcher#error */ - if (err) this.emit('error', err); + if (type && err) { + err.message = `${type} stream: `; + this.emit('error', err); + } this.destroy(); }; this.on('error', () => streamError()); - if (this.streams.input) this.streams.input.on('error', streamError); - if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', streamError); - if (this.streams.opus) this.streams.opus.on('error', streamError); - if (this.streams.volume) this.streams.volume.on('error', streamError); + if (this.streams.input) this.streams.input.on('error', err => streamError('input', err)); + if (this.streams.ffmpeg) this.streams.ffmpeg.on('error', err => streamError('ffmpeg', err)); + if (this.streams.opus) this.streams.opus.on('error', err => streamError('opus', err)); + if (this.streams.volume) this.streams.volume.on('error', err => streamError('volume', err)); } get _sdata() { From aa094907245ba5fa9a9ad31cb702fed0658635e0 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 21 Jan 2018 10:31:12 +0000 Subject: [PATCH 116/154] whoops --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 26cc33630..9ea297ab7 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -75,7 +75,7 @@ class StreamDispatcher extends Writable { * @event StreamDispatcher#error */ if (type && err) { - err.message = `${type} stream: `; + err.message = `${type} stream: ${err.message}`; this.emit('error', err); } this.destroy(); From 76891a1e00e6c422f8a2c48b6b5652c0c55c1129 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 21 Jan 2018 10:34:14 +0000 Subject: [PATCH 117/154] try fix --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 9ea297ab7..ef1f0b985 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -76,7 +76,7 @@ class StreamDispatcher extends Writable { */ if (type && err) { err.message = `${type} stream: ${err.message}`; - this.emit('error', err); + this.emit(this.player.dispatcher === this ? 'error' : 'debug', err); } this.destroy(); }; From 3300e39690652a2b593d0d5949118d50cca30dbf Mon Sep 17 00:00:00 2001 From: Pascal Date: Sun, 21 Jan 2018 13:12:31 +0100 Subject: [PATCH 118/154] chore: update typings submodule --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 895af7f3d..16f6834cd 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 895af7f3dad233139b8246fe0e44079867e6cc95 +Subproject commit 16f6834cdce57324797d95d777872603262629a5 From 83de7c0d4cbe7081791bf38172940a4d5151d959 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 21 Jan 2018 15:52:32 +0000 Subject: [PATCH 119/154] Fix error messages --- src/client/voice/util/PlayInterface.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index e607bfd90..4f5915650 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -59,7 +59,7 @@ class PlayInterface { */ play(resource, options = {}) { if (resource instanceof Broadcast) { - if (!this.player.playBroadcast) throw Error('VOICE_PLAY_INTERFACE_NO_BROADCAST'); + if (!this.player.playBroadcast) throw new Error('VOICE_PLAY_INTERFACE_NO_BROADCAST'); return this.player.playBroadcast(resource, options); } const type = options.type || 'unknown'; @@ -70,13 +70,13 @@ class PlayInterface { } else if (type === 'opus') { return this.player.playOpusStream(resource, options); } else if (type === 'ogg/opus') { - if (!(resource instanceof Readable)) throw Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); + 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 Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); + if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer())); } - throw Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); + throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); } static applyToClass(structure) { From edc4e2b751753c46a32a11eff8c757ca02d9dde4 Mon Sep 17 00:00:00 2001 From: Michel Nguyen Date: Sun, 21 Jan 2018 18:23:00 +0100 Subject: [PATCH 120/154] docs: fix streamdispatcher documentation (#2275) * docs change * fix streamdispatcher documentation Signed-off-by: Michel Nguyen * fucks sake Signed-off-by: Michel Nguyen * typings Signed-off-by: Michel Nguyen * typings again :eyes: Signed-off-by: Michel Nguyen --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index ef1f0b985..b99be2e88 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -22,7 +22,7 @@ nonce.fill(0); * // 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} From 8a2ace45d81dccd86afa99f607de1d346b5d6a62 Mon Sep 17 00:00:00 2001 From: Cat Date: Sun, 21 Jan 2018 19:23:23 +0200 Subject: [PATCH 121/154] docs: fix VoiceBroadcast documentation (#2277) --- src/client/voice/VoiceBroadcast.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 6dcb00cd6..1bc4e2827 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -9,13 +9,12 @@ const PlayInterface = require('./util/PlayInterface'); * 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 EventEmitter { From 86da7af4f18abc295dccf7535b1ce8c1effd8b6e Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 21 Jan 2018 17:47:26 +0000 Subject: [PATCH 122/154] Update prism dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 16e7f5b2b..fb1dc3ced 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "dependencies": { "pako": "^1.0.0", - "prism-media": "github:hydrabolt/prism-media#indev", + "prism-media": "github:hydrabolt/prism-media", "snekfetch": "^3.5.0", "tweetnacl": "^1.0.0", "ws": "^3.3.1" From 07c48a68450b79e7319d1034546846441565624b Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 22 Jan 2018 20:23:36 +0100 Subject: [PATCH 123/154] fix: require discordjserror to throw meaningful errors where necessary --- src/client/voice/receiver/PacketHandler.js | 2 +- src/client/voice/receiver/Receiver.js | 1 + src/client/voice/util/PlayInterface.js | 1 + src/stores/GuildEmojiStore.js | 1 + src/stores/GuildMemberStore.js | 2 +- src/stores/ReactionUserStore.js | 2 ++ src/structures/Role.js | 2 +- src/structures/shared/CreateMessage.js | 1 + src/util/DataResolver.js | 4 ++-- 9 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 2227d55cf..9ebe69d85 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -23,7 +23,7 @@ class PacketHandler extends EventEmitter { buffer.copy(nonce, 0, 0, 12); let packet = secretbox.methods.open(buffer.slice(12), nonce, this.receiver.connection.authentication.secretKey); - if (!packet) return Error('Failed to decrypt voice packet'); + if (!packet) return new Error('Failed to decrypt voice packet'); packet = Buffer.from(packet); // Strip RTP Header Extensions (one-byte only) diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index 51beb682c..9636377d7 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -1,6 +1,7 @@ const EventEmitter = require('events'); const prism = require('prism-media'); const PacketHandler = require('./PacketHandler'); +const { Error } = require('../../../errors'); /** * Receives audio packets from a voice connection. diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 4f5915650..ebcb1378e 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -1,5 +1,6 @@ const { Readable } = require('stream'); const prism = require('prism-media'); +const { Error } = require('../../../errors'); /** * Options that can be passed to stream-playing methods: diff --git a/src/stores/GuildEmojiStore.js b/src/stores/GuildEmojiStore.js index bc5c57280..75bf16e08 100644 --- a/src/stores/GuildEmojiStore.js +++ b/src/stores/GuildEmojiStore.js @@ -3,6 +3,7 @@ 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. diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 8cdd7611e..6c4b5947f 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -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. diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js index d246d10b9..a07a9a093 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/stores/ReactionUserStore.js @@ -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} diff --git a/src/structures/Role.js b/src/structures/Role.js index 1d82f63ba..a0209b20f 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -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. diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js index d0fa35726..55d779cf5 100644 --- a/src/structures/shared/CreateMessage.js +++ b/src/structures/shared/CreateMessage.js @@ -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) { diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index ff2dc75b7..91a698893 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -2,7 +2,7 @@ const path = require('path'); const fs = require('fs'); const snekfetch = require('snekfetch'); const Util = require('../util/Util'); -const { Error, TypeError } = require('../errors'); +const { Error: DiscordError, TypeError } = require('../errors'); const { browser } = require('../util/Constants'); /** @@ -99,7 +99,7 @@ class DataResolver { const file = browser ? resource : path.resolve(resource); fs.stat(file, (err, stats) => { if (err) return reject(err); - if (!stats || !stats.isFile()) return reject(new Error('FILE_NOT_FOUND', file)); + if (!stats || !stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); fs.readFile(file, (err2, data) => { if (err2) reject(err2); else resolve(data); }); From 5352e28700e51f1148463f350fe5694124a3c270 Mon Sep 17 00:00:00 2001 From: Pascal Date: Tue, 23 Jan 2018 07:27:39 +0100 Subject: [PATCH 124/154] chore: update typings submodule --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 16f6834cd..161be64c6 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 16f6834cdce57324797d95d777872603262629a5 +Subproject commit 161be64c61171b4a07a8f5aee246b82da7f9abc0 From 00172e6c7dd293b7d04a531b946fde57f0c58d1e Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Wed, 24 Jan 2018 09:12:58 +0200 Subject: [PATCH 125/154] refactor: Move member role-related functions to a special store (#2242) * Ignore this I need a patch branch for Git * Move all member role related stuff to a new DataStore GuildMemberRoleStore is a new store that holds all member role related stuffs Because its consistent! * Minorest doc fix ever To whoever did this object, they forgot a `{` * Fix the spacing in the docs * Resue the stores resolve rather than copy paste Cause I'm dum and it overwrite resolve to the guild role stores resolve soo * Fix some requests - Removed the bs private functions - Set the roles in the constructor But, I need feedback. There is no way, that I saw, to make a member have roles whenever GuildMmber#_patch is called, due to the roles being null on the guild. So the only way might be the for loop and getter. * Fix an issue that I caused in #add I was testing some other things, and changed that to test. Forgot to change it back * Actually make the store generate just once when the member is created by first initializing the roles in the guild Also replaces GuildMember#_roles with GuildMemberRoleStore#_roles Tested all functions to make sure the expected result happens. * I missed this from moving remove from GuildMember to GuildMemberRoleStore * Fix RoleStore#create docs For real this time * Do all the requested changes - Rename all `somethingRole` to `something` (hoistRole => hoist) - Refactor add and remove to be cleaner and re-use set * Fix a bug where the store would loose some roles due to null roles that can throw an error in the for loop after they've been deleted * Remove the `role.id || role` part of the add and remove functions as Appel suggested * Replace roles.resolve with roles.resolveID for GuildMemberRoleStore#remove * Don't use Array.isArray in checks Use instanceof instead * Woops, I forgot to change this Renamed colorRole to color * The docs have dots, so we place the dots back --- src/index.js | 1 + src/stores/GuildMemberRoleStore.js | 150 ++++++++++++++++++++++++++ src/stores/RoleStore.js | 30 +++--- src/structures/Guild.js | 24 ++--- src/structures/GuildMember.js | 165 ++--------------------------- 5 files changed, 189 insertions(+), 181 deletions(-) create mode 100644 src/stores/GuildMemberRoleStore.js diff --git a/src/index.js b/src/index.js index 1cd14b654..bbbb32ab3 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ module.exports = { GuildChannelStore: require('./stores/GuildChannelStore'), GuildEmojiStore: require('./stores/GuildEmojiStore'), GuildMemberStore: require('./stores/GuildMemberStore'), + GuildMemberRoleStore: require('./stores/GuildMemberRoleStore'), GuildStore: require('./stores/GuildStore'), ReactionUserStore: require('./stores/ReactionUserStore'), MessageStore: require('./stores/MessageStore'), diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js new file mode 100644 index 000000000..9de5ab14d --- /dev/null +++ b/src/stores/GuildMemberRoleStore.js @@ -0,0 +1,150 @@ +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} roleOrRoles The role or roles to add + * @param {string} [reason] Reason for adding the role(s) + * @returns {Promise} + */ + 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|RoleResolvable[]} roles The roles or role IDs to apply + * @param {string} [reason] Reason for applying the roles + * @returns {Promise} + * @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} roleOrRoles The role or roles to remove + * @param {string} [reason] Reason for removing the role(s) + * @returns {Promise} + */ + 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); + } + } + } + + /** + * 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; diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 55eeaa944..020388946 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -34,7 +34,7 @@ class RoleStore extends DataStore { * name: 'Super Cool People', * color: 'BLUE' * }, - * reason: 'we needed a role for Super Cool People', + * 'we needed a role for Super Cool People', * }) * .then(console.log) * .catch(console.error); @@ -61,22 +61,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; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 51ea81d32..b24bfec47 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -179,6 +179,18 @@ 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.add(guildUser); @@ -192,18 +204,6 @@ class Guild extends Base { this.ownerID = data.owner_id; } - 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.presences) { for (const presence of data.presences) { this.presences.add(presence); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index bee96c814..530970f4c 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -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. @@ -27,7 +27,12 @@ class GuildMember extends Base { */ this.user = {}; - this._roles = []; + /** + * A list of roles that are applied to this GuildMember, mapped by the role ID + * @type {GuildMemberRoleStore} + */ + + this.roles = new GuildMemberRoleStore(this); if (data) this._patch(data); @@ -67,7 +72,7 @@ class GuildMember extends Base { if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); this.user = this.guild.client.users.add(data.user); - if (data.roles) this._roles = data.roles; + if (data.roles) this.roles._patch(data.roles); } get voiceState() { @@ -134,52 +139,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} - * @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 +155,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} @@ -395,104 +350,6 @@ class GuildMember extends Base { return this.edit({ channel }); } - /** - * Sets the roles applied to the member. - * @param {Collection|RoleResolvable[]} roles The roles or role IDs to apply - * @param {string} [reason] Reason for applying the roles - * @returns {Promise} - * @example - * // Set the member's roles to a single role - * guildMember.setRoles(['391156570408615936']) - * .then(console.log) - * .catch(console.error); - * @example - * // Remove all the roles from a member - * guildMember.setRoles([]) - * .then(member => console.log(`Member roles is now of ${member.roles.size} size`)) - * .catch(console.error); - */ - 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} - */ - 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|RoleResolvable[]} roles The roles or role IDs to add - * @param {string} [reason] Reason for adding the roles - * @returns {Promise} - */ - 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} - */ - 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|RoleResolvable[]} roles The roles or role IDs to remove - * @param {string} [reason] Reason for removing the roles - * @returns {Promise} - */ - 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 From 92c9f8864c98f212540291a0f979c7f3f691e8fe Mon Sep 17 00:00:00 2001 From: FireController1847 Date: Wed, 24 Jan 2018 00:24:24 -0700 Subject: [PATCH 126/154] Specify that Client#uptime is "in milliseconds" (#2288) --- src/client/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index db0fb151a..475d51d62 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -179,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 */ From 048e1474975363b39a586a2d902767b3f3b99318 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 24 Jan 2018 09:02:37 +0100 Subject: [PATCH 127/154] fix(RoleStore): create method is supposed to take an options object --- src/stores/RoleStore.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 020388946..5619360e2 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -20,8 +20,9 @@ class RoleStore extends DataStore { /** * Creates a new role in the guild with given information. * The position will silently reset to 1 if an invalid one is provided, or none. - * @param {RoleData} [data] The data to update the role with - * @param {string} [reason] Reason for creating this role + * @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} * @example * // Create a new role @@ -31,15 +32,16 @@ class RoleStore extends DataStore { * @example * // Create a new role with data and a reason * guild.roles.create({ + * data: { * name: 'Super Cool People', - * color: 'BLUE' + * color: 'BLUE', * }, - * 'we needed a role for Super Cool People', + * reason: 'we needed a role for Super Cool People', * }) * .then(console.log) * .catch(console.error); */ - create(data = {}, reason) { + create({ data = {}, reason } = {}) { if (data.color) data.color = resolveColor(data.color); if (data.permissions) data.permissions = Permissions.resolve(data.permissions); From 16b5de5d522b882b2d9485cd8fd6e5b36fa98c7d Mon Sep 17 00:00:00 2001 From: Dragon Fire Date: Wed, 24 Jan 2018 07:34:11 -0500 Subject: [PATCH 128/154] fix(Role) Update usages of highestRole#comparePositionTo to use GuildMemberRoleStore (#2289) --- src/structures/GuildMember.js | 2 +- src/structures/Role.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 530970f4c..19373adac 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -222,7 +222,7 @@ class GuildMember extends Base { 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.highestRole.comparePositionTo(this.highestRole) > 0; + return this.guild.me.roles.highest.comparePositionTo(this.roles.highest) > 0; } /** diff --git a/src/structures/Role.js b/src/structures/Role.js index a0209b20f..5712b27ee 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -118,7 +118,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; } /** From a832b564690b1eb2b0d7dce00f129b8687d8604b Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Wed, 24 Jan 2018 21:47:20 +0200 Subject: [PATCH 129/154] GuildEmoji: Move all role related functions to a separate store (#2271) * Prepare to work on moving all role functions to a Store And yes, this is *another* patch branch cause I messed up my master branch to hell * Move all emoji role related functions to its own store Tested everything and it works! (With a reload of the client) Also had to change a value in DataStore#holds holds.name for GuildEmojis would return the emoji name instead of the class name * New Line * Thanks JS for circular dependency! Because we can't have nice things... * Do space's request * Fix equals * Fix space's point. Raw API data has the role property as an array of IDs --- src/index.js | 1 + src/stores/GuildEmojiRoleStore.js | 110 ++++++++++++++++++++++++++++++ src/structures/GuildEmoji.js | 82 +++------------------- 3 files changed, 121 insertions(+), 72 deletions(-) create mode 100644 src/stores/GuildEmojiRoleStore.js diff --git a/src/index.js b/src/index.js index bbbb32ab3..b89d9ce56 100644 --- a/src/index.js +++ b/src/index.js @@ -28,6 +28,7 @@ module.exports = { ClientPresenceStore: require('./stores/ClientPresenceStore'), GuildChannelStore: require('./stores/GuildChannelStore'), GuildEmojiStore: require('./stores/GuildEmojiStore'), + GuildEmojiRoleStore: require('./stores/GuildEmojiRoleStore'), GuildMemberStore: require('./stores/GuildMemberStore'), GuildMemberRoleStore: require('./stores/GuildMemberRoleStore'), GuildStore: require('./stores/GuildStore'), diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js new file mode 100644 index 000000000..2471fb5fe --- /dev/null +++ b/src/stores/GuildEmojiRoleStore.js @@ -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} roleOrRoles The role or roles to add + * @returns {Promise} + */ + 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} roleOrRoles The role or roles to remove + * @returns {Promise} + */ + 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|RoleResolvable[]} roles The roles or role IDs to apply + * @returns {Promise} + * @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; diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 6bf63152d..df0c1169b 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,7 +1,6 @@ -const Collection = require('../util/Collection'); +const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore'); const Snowflake = require('../util/Snowflake'); const Emoji = require('./Emoji'); -const { TypeError } = require('../errors'); /** * Represents a custom emoji. @@ -17,6 +16,12 @@ class GuildEmoji extends Emoji { */ this.guild = guild; + /** + * A collection of roles this emoji is active for (empty if all), mapped by role ID + * @type {GuildEmojiRoleStore} + */ + this.roles = new GuildEmojiRoleStore(this); + this._patch(data); } @@ -35,7 +40,7 @@ class GuildEmoji extends Emoji { */ this.managed = data.managed; - this._roles = data.roles; + if (data.roles) this.roles._patch(data.roles); } /** @@ -56,19 +61,6 @@ class GuildEmoji extends Emoji { return new Date(this.createdTimestamp); } - /** - * A collection of roles this emoji is active for (empty if all), mapped by role ID - * @type {Collection} - * @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; - } - /** * Data for editing an emoji. * @typedef {Object} GuildEmojiEditData @@ -106,60 +98,6 @@ class GuildEmoji extends Emoji { 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} - */ - addRestrictedRole(role) { - return this.addRestrictedRoles([role]); - } - - /** - * Adds multiple roles to the list of roles that can use this emoji. - * @param {Collection|RoleResolvable[]} roles Roles to add - * @returns {Promise} - */ - 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} - */ - removeRestrictedRole(role) { - return this.removeRestrictedRoles([role]); - } - - /** - * Removes multiple roles from the list of roles that can use this emoji. - * @param {Collection|RoleResolvable[]} roles Roles to remove - * @returns {Promise} - */ - 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 }); - } - /** * Deletes the emoji. * @param {string} [reason] Reason for deleting the emoji @@ -182,13 +120,13 @@ class GuildEmoji extends Emoji { other.name === this.name && other.managed === this.managed && other.requiresColons === this.requiresColons && - other._roles === this._roles + other.roles.every(role => this.roles.has(role.id)) ); } else { return ( other.id === this.id && other.name === this.name && - other._roles === this._roles + other.roles.every(role => this.roles.has(role)) ); } } From 7e0e457334e42436e019c8cbf9e602aedab5e850 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Wed, 24 Jan 2018 15:09:40 -0600 Subject: [PATCH 130/154] fix(ClientUser#setStatus): resetting activity The method would pass in null if no activity was passed, so this takes the current client presence instead of deleting it. --- src/stores/ClientPresenceStore.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/stores/ClientPresenceStore.js b/src/stores/ClientPresenceStore.js index a6eedb7ce..5d6914d12 100644 --- a/src/stores/ClientPresenceStore.js +++ b/src/stores/ClientPresenceStore.js @@ -38,7 +38,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, @@ -54,9 +54,14 @@ class ClientPresenceStore extends PresenceStore { application_id: applicationID || undefined, secrets: activity.secrets || undefined, instance: activity.instance || undefined, - } : null, + } : this.clientPresence.activity, }; + if (packet.game) { + packet.game.type = typeof packet.game.type === 'number' ? + packet.game.type : ActivityTypes.indexOf(packet.game.type) + } + this.clientPresence.patch(packet); this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet }); return this.clientPresence; From 58d85282b43f83974a2131563fafafaf99eed372 Mon Sep 17 00:00:00 2001 From: Faith Date: Thu, 25 Jan 2018 00:37:41 +0100 Subject: [PATCH 131/154] Fix Destructuring Errors in Util (#2171) * Fix Destructuring * Fix another one --- src/util/Util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index c1020ec10..5eb6e6dfe 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -125,7 +125,7 @@ class Util { if (!has(given, key) || given[key] === undefined) { given[key] = def[key]; } else if (given[key] === Object(given[key])) { - given[key] = this.mergeDefault(def[key], given[key]); + given[key] = Util.mergeDefault(def[key], given[key]); } } @@ -139,7 +139,7 @@ class Util { * @private */ static convertToBuffer(ab) { - if (typeof ab === 'string') ab = this.str2ab(ab); + if (typeof ab === 'string') ab = Util.str2ab(ab); return Buffer.from(ab); } From e58ff642f5be86960ffdabb8e9dec82a6d500c8f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 25 Jan 2018 07:41:50 +0200 Subject: [PATCH 132/154] Make Util#splitMessage handle edge cases properly (#2212) * Make Util#splitMessage handle edge cases properly * Restart Travis * Set maxLength to 2000 + small tweak --- src/util/Util.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 5eb6e6dfe..5cd7ed065 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -18,21 +18,20 @@ class Util { * @param {SplitOptions} [options] Options controlling the behaviour of the split * @returns {string|string[]} */ - static splitMessage(text, { maxLength = 1950, char = '\n', prepend = '', append = '' } = {}) { + static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { if (text.length <= maxLength) return text; const splitText = text.split(char); if (splitText.length === 1) throw new RangeError('SPLIT_MAX_LEN'); - const messages = ['']; - let msg = 0; - for (let i = 0; i < splitText.length; i++) { - if (messages[msg].length + splitText[i].length + 1 > maxLength) { - messages[msg] += append; - messages.push(prepend); - msg++; + const messages = []; + let msg = ''; + for (const chunk of splitText) { + if (msg && (msg + char + chunk + append).length > maxLength) { + messages.push(msg + append); + msg = prepend; } - messages[msg] += (messages[msg].length > 0 && messages[msg] !== prepend ? char : '') + splitText[i]; + msg += (msg && msg !== prepend ? char : '') + chunk; } - return messages.filter(m => m); + return messages.concat(msg).filter(m => m); } /** From bd154bdd9ec03e21fd439e86081b065fa2f6dcbf Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Thu, 25 Jan 2018 02:06:28 -0600 Subject: [PATCH 133/154] fix: nullable activity --- src/stores/ClientPresenceStore.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/stores/ClientPresenceStore.js b/src/stores/ClientPresenceStore.js index 5d6914d12..eced8afa2 100644 --- a/src/stores/ClientPresenceStore.js +++ b/src/stores/ClientPresenceStore.js @@ -54,12 +54,16 @@ class ClientPresenceStore extends PresenceStore { application_id: applicationID || undefined, secrets: activity.secrets || undefined, instance: activity.instance || undefined, - } : this.clientPresence.activity, + } : null, }; + 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) + packet.game.type : ActivityTypes.indexOf(packet.game.type); } this.clientPresence.patch(packet); From 8c288b56a2f64a48d0553eda362bf6d3e71edfe3 Mon Sep 17 00:00:00 2001 From: Pascal Date: Fri, 26 Jan 2018 09:25:30 +0100 Subject: [PATCH 134/154] chore: update typings submodule --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 161be64c6..a1ee1317c 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 161be64c61171b4a07a8f5aee246b82da7f9abc0 +Subproject commit a1ee1317c9e71341cafd2d48ef270120e6fb9b57 From 10f98d8e57033fa120dff68f6afb55042df3da9b Mon Sep 17 00:00:00 2001 From: Dim Date: Sat, 27 Jan 2018 01:42:49 -0500 Subject: [PATCH 135/154] use String#padStart for Role#hexColor (#2294) --- src/structures/Role.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index 5712b27ee..68a3c3f03 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -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')}`; } /** From d93d628f197117e8cda731a8d0a00d61d276a641 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sat, 27 Jan 2018 17:04:03 -0600 Subject: [PATCH 136/154] make Message#member a getter --- src/structures/Message.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 8fb9688fa..69c38a0ad 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -54,13 +54,6 @@ class Message extends Base { */ this.author = this.client.users.add(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; - /** * Whether or not this message is pinned * @type {boolean} @@ -201,6 +194,15 @@ 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} + */ + get member() { + return this.guild ? this.guild.member(this.author) || null : null; + } + /** * The time the message was sent at * @type {Date} From 2e0048add1036c4648a99cd6a780280b6dd4ce9b Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sat, 27 Jan 2018 23:41:25 -0600 Subject: [PATCH 137/154] docs: doc changes --- src/structures/CategoryChannel.js | 2 +- src/structures/GuildMember.js | 6 +++--- src/structures/Message.js | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index b9d3ceff2..5766a4a75 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -6,7 +6,7 @@ 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} * @readonly */ diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 19373adac..3f609ddde 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -22,7 +22,7 @@ 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 = {}; @@ -322,7 +322,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} @@ -332,7 +332,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} diff --git a/src/structures/Message.js b/src/structures/Message.js index 69c38a0ad..ddcf02c91 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -198,6 +198,7 @@ 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; From d46eec4da40c01d134d863b9421fbc12d2ec6d20 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 28 Jan 2018 22:34:50 -0600 Subject: [PATCH 138/154] fix(resolveColor): not interpreting DEFAULT correctly --- src/util/Util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Util.js b/src/util/Util.js index 5cd7ed065..d7988599d 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -262,6 +262,7 @@ class Util { static resolveColor(color) { if (typeof color === 'string') { if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1)); + if (color === 'DEFAULT') return 0; color = Colors[color] || parseInt(color.replace('#', ''), 16); } else if (color instanceof Array) { color = (color[0] << 16) + (color[1] << 8) + color[2]; From 9810bdbc5ffcddfabd390c5ff72254465a78aa59 Mon Sep 17 00:00:00 2001 From: Isabella Date: Mon, 29 Jan 2018 10:56:55 -0600 Subject: [PATCH 139/154] fix(MessageEmbed): remove length checks (#2304) * fix(MessageEmbed): remove length checks * update error messages --- src/errors/Messages.js | 7 ++----- src/structures/MessageEmbed.js | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index a57ef6eed..0485296a6 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -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}`, diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index de3ceddf7..297b19fff 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -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; } @@ -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; } From 32e2dd212e3137d16457216e1330242c21487774 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 31 Jan 2018 21:04:15 +0100 Subject: [PATCH 140/154] fix: add clone methods to GuildMember and GuildMemberRoleStore Fixes #2312 This adds clone method to both classes to achieve the expected behaviour when cloning a GuildMember, also cloning their roles store --- src/stores/GuildMemberRoleStore.js | 6 ++++++ src/structures/GuildMember.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 9de5ab14d..af9d08ef1 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -128,6 +128,12 @@ class GuildMemberRoleStore extends DataStore { } } + clone() { + const clone = new this.constructor(this.member); + clone._patch(this.keyArray()); + return clone; + } + /** * Resolves a RoleResolvable to a Role object. * @method resolve diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 3f609ddde..1b988d32f 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -75,6 +75,12 @@ class GuildMember extends Base { if (data.roles) this.roles._patch(data.roles); } + _clone() { + const clone = super._clone(); + clone.roles = this.roles.clone(); + return clone; + } + get voiceState() { return this._frozenVoiceState || this.guild.voiceStates.get(this.id) || {}; } From 31873eb3a5133a1ed161c110ccb0b99ca5cd6390 Mon Sep 17 00:00:00 2001 From: Lasse Niermann Date: Thu, 1 Feb 2018 20:06:59 +0100 Subject: [PATCH 141/154] docs(ClientUser): mark email field as user account only property (#2306) * Store Mail - User Account Only Added that info * docs(ClientUser): mark email field as nullable --- src/structures/ClientUser.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 92fe176f5..dd227c31c 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -22,7 +22,8 @@ class ClientUser extends Structures.get('User') { /** * The email of this account - * @type {string} + * This is only filled when using a user account. + * @type {?string} */ this.email = data.email; this._typing = new Map(); From 016526486cff91669b4459bd5e4aec40ccc9e734 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 2 Feb 2018 01:41:05 +0100 Subject: [PATCH 142/154] enhancement(Activity): add toString method (#2313) --- src/structures/Presence.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 535c7d11d..054e92195 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -134,6 +134,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); } From 234648bd2a438038bc52cc2f58ff70bdb1b02288 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Fri, 2 Feb 2018 11:41:08 -0600 Subject: [PATCH 143/154] fix(ClientApplication): createAsset incorrectly resolving image and posting --- src/structures/ClientApplication.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 749f635ed..8c968b0da 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -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), + } }); } /** From bd1bf11ed0e27435cd00146a5d2cc1e2ee4cefce Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 2 Feb 2018 12:45:18 -0600 Subject: [PATCH 144/154] spotify stuff (#2314) --- src/structures/Presence.js | 16 +++++++++++++++- src/util/Constants.js | 9 +++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 054e92195..cedf3b6cd 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,4 +1,4 @@ -const { ActivityTypes } = require('../util/Constants'); +const { ActivityTypes, ActivityFlags } = require('../util/Constants'); /** * Represents a user's presence. @@ -118,6 +118,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; } /** @@ -201,6 +212,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 }); } diff --git a/src/util/Constants.js b/src/util/Constants.js index bf6106cc1..e66e04041 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -362,6 +362,15 @@ exports.ActivityTypes = [ 'WATCHING', ]; +exports.ActivityFlags = { + INSTANCE: 1 << 0, + JOIN: 1 << 1, + SPECTATE: 1 << 2, + JOIN_REQUEST: 1 << 3, + SYNC: 1 << 4, + PLAY: 1 << 5, +}; + exports.ExplicitContentFilterTypes = [ 'DISABLED', 'NON_FRIENDS', From e0cbf0bb607381633fe125c18b86722985ba31ea Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Fri, 2 Feb 2018 21:34:21 +0200 Subject: [PATCH 145/154] feature: add GuildEmoji#fetchAuthor (#2315) * Make the base code * Fxi lint --- src/structures/GuildEmoji.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index df0c1169b..7568738ae 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -61,6 +61,15 @@ class GuildEmoji extends Emoji { return new Date(this.createdTimestamp); } + /** + * Fetches the author for this emoji + * @returns {Promise} + */ + 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 From 43363172c2b62c4df5098f09ed6316d4e11aaf04 Mon Sep 17 00:00:00 2001 From: Snazzah <7025343+Snazzah@users.noreply.github.com> Date: Sun, 4 Feb 2018 13:44:13 -0600 Subject: [PATCH 146/154] docs: Add TypeDef for MessageActivity (#2321) --- src/structures/Message.js | 2 +- src/structures/Presence.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index ddcf02c91..5b9c74aa5 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -138,7 +138,7 @@ class Message extends Base { /** * Group activity - * @type {?Object} + * @type {?MessageActivity} */ this.activity = data.activity ? { partyID: data.activity.party_id, diff --git a/src/structures/Presence.js b/src/structures/Presence.js index cedf3b6cd..e4359539d 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,5 +1,12 @@ 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. */ From 87e5a4565937bf08c392401f1b75552aec49428d Mon Sep 17 00:00:00 2001 From: Sanctuary Date: Sun, 4 Feb 2018 18:32:45 -0200 Subject: [PATCH 147/154] feat(ClientOptions): add support for setting an initial presence (#2320) * docs/feat(WebsocketOptions): Parse ws options presence Allow the `presence` property in `WebsocketOptions` to be used the same way as `ClientUser#setPresence`. * Move presence options to top level --- src/client/ClientManager.js | 5 ++++- src/stores/ClientPresenceStore.js | 13 +++++++++---- src/util/Constants.js | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index ad006b8fe..f1891e0ce 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -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); diff --git a/src/stores/ClientPresenceStore.js b/src/stores/ClientPresenceStore.js index eced8afa2..94d48d4a4 100644 --- a/src/stores/ClientPresenceStore.js +++ b/src/stores/ClientPresenceStore.js @@ -19,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) { @@ -66,9 +73,7 @@ class ClientPresenceStore extends PresenceStore { packet.game.type : ActivityTypes.indexOf(packet.game.type); } - this.clientPresence.patch(packet); - this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet }); - return this.clientPresence; + return packet; } } diff --git a/src/util/Constants.js b/src/util/Constants.js index e66e04041..b29a2dea8 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -26,6 +26,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST * requests (higher values will reduce rate-limiting errors on bad connections) + * @property {PresenceData} [presence] Presence data to use upon login * @property {WSEventType[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be * processed, potentially resulting in performance improvements for larger bots. Only disable events you are * 100% certain you don't need, as many are important, but not obviously so. The safest one to disable with the @@ -47,6 +48,7 @@ exports.DefaultOptions = { restWsBridgeTimeout: 5000, disabledEvents: [], restTimeOffset: 500, + presence: {}, /** * WebSocket options (these are left as snake_case to match the API) From 66c0512de2a3bda5e3ae5f9443425a242987abd5 Mon Sep 17 00:00:00 2001 From: Isabella Date: Wed, 7 Feb 2018 10:07:59 -0600 Subject: [PATCH 148/154] feat(ShardClientUtil#broadcastEval): allow promise resolve (#2328) --- src/sharding/ShardClientUtil.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 8b8262150..f4066717a 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -119,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('.'); @@ -128,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) }); } From 47bc0fc51e937f036a71d477f56fa884d44ee92d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 13 Feb 2018 17:29:42 +0100 Subject: [PATCH 149/154] feat(GuildChannel): add fetchInvites method (#2339) * feat(GuildChannel): add fetchInvites method * fix: actually use the 'channels' endpoint --- src/structures/GuildChannel.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index a79f11bae..324b6aeda 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -430,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>} + */ + 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 From 2fd4c6def72850710dd3de2dd32b93c1a21a2a77 Mon Sep 17 00:00:00 2001 From: Pascal Date: Tue, 13 Feb 2018 17:40:43 +0100 Subject: [PATCH 150/154] chore(deps): update uws to 9.14.0 Closes #2327 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb1dc3ced..58e7d088c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "erlpack": "discordapp/erlpack", "sodium": "^2.0.0", "libsodium-wrappers": "^0.7.0", - "uws": "^8.14.0", + "uws": "^9.14.0", "zlib-sync": "^0.1.0" }, "devDependencies": { From 5cf8a634f8c8856b00be3f22fcdd12823fd812bf Mon Sep 17 00:00:00 2001 From: Pascal Date: Tue, 13 Feb 2018 17:54:52 +0100 Subject: [PATCH 151/154] chore: update typings submodule --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index a1ee1317c..604441490 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit a1ee1317c9e71341cafd2d48ef270120e6fb9b57 +Subproject commit 60444149022d84dc2626cff1a91004c31d73f491 From 672f93f5bd5e4237053030b35f903d75a148bd35 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Fri, 16 Feb 2018 08:43:46 +0100 Subject: [PATCH 152/154] chore: bump deps and remove unneeded ones --- package.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 58e7d088c..a4161bb55 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ "runkitExampleFilename": "./docs/examples/ping.js", "dependencies": { "pako": "^1.0.0", - "prism-media": "github:hydrabolt/prism-media", - "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", @@ -47,14 +47,13 @@ "zlib-sync": "^0.1.0" }, "devDependencies": { - "@types/node": "^8.0.0", + "@types/node": "^9.4.6", "discord.js-docgen": "discordjs/docgen", - "eslint": "^4.11.0", - "jsdoc-strip-async-await": "^0.1.0", + "eslint": "^4.17.0", "json-filter-loader": "^1.0.0", "parallel-webpack": "^2.2.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" From 332558a3d824a17217aaee7c2762cbb74e5cc9d6 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Fri, 16 Feb 2018 08:46:31 +0100 Subject: [PATCH 153/154] fix: npm scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a4161bb55..c57662825 100644 --- a/package.json +++ b/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", "webpack": "parallel-webpack" From 8e60743c0b60ccddc0cab735b8f8648930bcd370 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Fri, 16 Feb 2018 08:54:49 +0100 Subject: [PATCH 154/154] fix: dont require snekfetch supplemental because they are not a thing anymore --- webpack.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index d92cb7da2..f5d0050f1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -46,7 +46,6 @@ const createConfig = options => { }, }, }, - ...require('snekfetch/webpack.supplemental').rules, ], }, node: {