UNPKG

djs-selfbot-v11

Version:

Selfbot module Discord

631 lines (563 loc) 17.9 kB
const VoiceWebSocket = require('./VoiceWebSocket'); const VoiceUDP = require('./VoiceUDPClient'); const Util = require('../../util/Util'); const Constants = require('../../util/Constants'); const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/VoiceReceiver'); const SingleSilence = require('./util/SingleSilence'); const EventEmitter = require('events').EventEmitter; const Prism = require('prism-media'); // The delay between packets when a user is considered to have stopped speaking // https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200 const DISCORD_SPEAKING_DELAY = 250; /** * Represents a connection to a guild's voice server. * ```js * // Obtained using: * voiceChannel.join() * .then(connection => { * * }); * ``` * @extends {EventEmitter} */ class VoiceConnection extends EventEmitter { constructor(voiceManager, channel) { super(); /** * The voice manager that instantiated this connection * @type {ClientVoiceManager} */ this.voiceManager = voiceManager; /** * The client that instantiated this connection * @type {Client} */ 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} */ this.channel = channel; /** * The current status of the voice connection * @type {VoiceStatus} */ this.status = Constants.VoiceStatus.AUTHENTICATING; /** * Whether we're currently transmitting audio * @type {boolean} */ this.speaking = false; /** * An array of Voice Receivers that have been created for this connection * @type {VoiceReceiver[]} */ this.receivers = []; /** * The authentication data needed to connect to the voice server * @type {Object} * @private */ this.authentication = {}; /** * The audio player for this voice connection * @type {AudioPlayer} */ this.player = new AudioPlayer(this); this.player.on('debug', m => { /** * Debug info from the connection. * @event VoiceConnection#debug * @param {string} message The debug message */ this.emit('debug', `audio player - ${m}`); }); this.player.on('error', e => { /** * Warning info from the connection. * @event VoiceConnection#warn * @param {string|Error} warning The warning */ this.emit('warn', e); }); /** * Map SSRC to user id * @type {Map<number, Snowflake>} * @private */ this.ssrcMap = new Map(); /** * Map user id to speaking timeout * @type {Map<Snowflake, Timeout>} * @private */ this.speakingTimeouts = new Map(); /** * Object that wraps contains the `ws` and `udp` sockets of this voice connection * @type {Object} * @private */ this.sockets = {}; this.authenticate(); } /** * The current stream dispatcher (if any) * @type {?StreamDispatcher} * @readonly */ get dispatcher() { return this.player.dispatcher; } /** * Sets whether the voice connection should display as "speaking" or not. * @param {boolean} value Whether or not to speak * @private */ setSpeaking(value) { if (this.speaking === value) return; if (this.status !== Constants.VoiceStatus.CONNECTED) return; this.speaking = value; this.sockets.ws.sendPacket({ op: Constants.VoiceOPCodes.SPEAKING, d: { speaking: true, delay: 0, }, }).catch(e => { this.emit('debug', e); }); } /** * Sends a request to the main gateway to join a voice channel. * @param {Object} [options] The options to provide */ sendVoiceStateUpdate(options = {}) { options = Util.mergeDefault({ guild_id: this.channel.guild.id, flags: 2, channel_id: this.channel.id, self_mute: false, self_deaf: false, self_video: false, }, options); this.client.ws.send({ op: Constants.OPCodes.VOICE_STATE_UPDATE, d: options, }); } /** * Set the token and endpoint required to connect to the voice servers. * @param {string} token The voice token * @param {string} endpoint The voice endpoint * @returns {void} */ setTokenAndEndpoint(token, endpoint) { if (!endpoint) { // Signifies awaiting endpoint stage return; } if (!token) { this.authenticateFailed('Token not provided from voice server packet.'); return; } endpoint = endpoint.match(/([^:]*)/)[0]; if (!endpoint) { this.authenticateFailed('Invalid endpoint received.'); return; } if (this.status === Constants.VoiceStatus.AUTHENTICATING) { this.authentication.token = token; this.authentication.endpoint = endpoint; this.checkAuthenticated(); } else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) { this.reconnect(token, endpoint); } } /** * Sets the Session ID for the connection. * @param {string} sessionID The voice session ID */ setSessionID(sessionID) { if (!sessionID) { this.authenticateFailed('Session ID not supplied.'); return; } if (this.status === Constants.VoiceStatus.AUTHENTICATING) { this.authentication.sessionID = sessionID; this.checkAuthenticated(); } else if (sessionID !== this.authentication.sessionID) { this.authentication.sessionID = sessionID; /** * Emitted when a new session ID is received. * @event VoiceConnection#newSession * @private */ this.emit('newSession', sessionID); } } /** * Checks whether the voice connection is authenticated. * @private */ checkAuthenticated() { const { token, endpoint, sessionID } = this.authentication; this.emit('debug', `Authenticated with sessionID ${sessionID}`); if (token && endpoint && sessionID) { this.client.clearTimeout(this.connectTimeout); this.status = Constants.VoiceStatus.CONNECTING; /** * Emitted when we successfully initiate a voice connection. * @event VoiceConnection#authenticated */ this.emit('authenticated'); this.connect(); } } /** * Invoked when we fail to initiate a voice connection. * @param {string} reason The reason for failure * @private */ authenticateFailed(reason) { this.client.clearTimeout(this.connectTimeout); if (this.status === Constants.VoiceStatus.AUTHENTICATING) { /** * Emitted when we fail to initiate a voice connection. * @event VoiceConnection#failed * @param {Error} error The encountered error */ this.emit('failed', new Error(reason)); } else { /** * Emitted whenever the connection encounters an error. * @event VoiceConnection#error * @param {Error} error The encountered error */ this.emit('error', new Error(reason)); } this.status = Constants.VoiceStatus.DISCONNECTED; } /** * Move to a different voice channel in the same guild. * @param {VoiceChannel} channel The channel to move to * @private */ updateChannel(channel) { this.channel = channel; this.sendVoiceStateUpdate(); } /** * Attempts to authenticate to the voice server. * @private */ authenticate() { this.sendVoiceStateUpdate(); this.connectTimeout = this.client.setTimeout( () => this.authenticateFailed(new Error('Connection not established within 15 seconds.')), 15000); } /** * Attempts to reconnect to the voice server (typically after a region change). * @param {string} token The voice token * @param {string} endpoint The voice endpoint * @private */ reconnect(token, endpoint) { this.authentication.token = token; this.authentication.endpoint = endpoint; this.status = Constants.VoiceStatus.RECONNECTING; /** * Emitted when the voice connection is reconnecting (typically after a region change). * @event VoiceConnection#reconnecting */ this.emit('reconnecting'); this.connect(); } /** * Disconnect the voice connection, causing a disconnect and closing event to be emitted. */ disconnect() { this.emit('closing'); this.sendVoiceStateUpdate({ channel_id: null, }); this._disconnect(); } startStreaming() { this.client.ws.send({ op: 18, d: { type: "guild", guild_id: this.channel.guild.id, channel_id: this.channel.id, preferred_region: "null" } }); this.client.ws.send({ op: 22, d: { paused: false, stream_key: `guild:${this.channel.guild.id}:${this.channel.id}:${this.client.user.id}` } }); } startCamera() { this.sendVoiceStateUpdate({ self_video: true }); } /** * Internally disconnects (doesn't send disconnect packet). * @private */ _disconnect() { this.player.destroy(); this.cleanup(); this.status = Constants.VoiceStatus.DISCONNECTED; /** * Emitted when the voice connection disconnects. * @event VoiceConnection#disconnect */ this.emit('disconnect'); } /** * Cleans up after disconnect. * @private */ cleanup() { const { ws, udp } = this.sockets; if (ws) { ws.removeAllListeners('error'); ws.removeAllListeners('ready'); ws.removeAllListeners('sessionDescription'); ws.removeAllListeners('startSpeaking'); ws.shutdown(); } if (udp) udp.removeAllListeners('error'); this.sockets.ws = null; this.sockets.udp = null; } /** * Connect the voice connection. * @private */ connect() { this.emit('debug', `Connect triggered`); if (this.status !== Constants.VoiceStatus.RECONNECTING) { if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.'); if (this.sockets.udp) throw new Error('There is already an existing UDP connection.'); } if (this.sockets.ws) this.sockets.ws.shutdown(); if (this.sockets.udp) this.sockets.udp.shutdown(); this.sockets.ws = new VoiceWebSocket(this); this.sockets.udp = new VoiceUDP(this); const { ws, udp } = this.sockets; ws.on('error', err => this.emit('error', err)); udp.on('error', err => this.emit('error', err)); ws.on('ready', this.onReady.bind(this)); ws.on('sessionDescription', this.onSessionDescription.bind(this)); ws.on('startSpeaking', this.onStartSpeaking.bind(this)); } /** * Invoked when the voice websocket is ready. * @param {Object} data The received data * @private */ onReady({ port, ssrc, ip }) { this.authentication.port = port; this.authentication.ssrc = ssrc; this.sockets.udp.createUDPSocket(ip); this.sockets.udp.socket.on('message', this.onUDPMessage.bind(this)); this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); this.emit('ready'); } /** * Invoked when a session description is received. * @param {string} mode The encryption mode * @param {string} secret The secret key * @private */ onSessionDescription(mode, secret) { this.authentication.encryptionMode = mode; this.authentication.secretKey = secret; this.status = Constants.VoiceStatus.CONNECTED; const ready = () => { this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, * the connection will already be ready. * @event VoiceConnection#ready */ this.emit('ready'); }; if (this.dispatcher) { //ready(); } else { // This serves to provide support for voice receive, sending audio is required to receive it. this.playOpusStream(new SingleSilence()).once('end', ready); } } /** * Invoked whenever a user initially starts speaking. * @param {Object} data The speaking data * @private */ onStartSpeaking({ user_id, ssrc }) { this.ssrcMap.set(+ssrc, user_id); } /** * Invoked when a speaking event is received. * @param {Object} data The received data * @private */ onSpeaking({ user_id, speaking }) { const guild = this.channel.guild; const user = this.client.users.get(user_id); if (!speaking) { for (const receiver of this.receivers) { receiver.stoppedSpeaking(user); } } /** * Emitted whenever a user starts/stops speaking. * @event VoiceConnection#speaking * @param {User} user The user that has started/stopped speaking * @param {boolean} speaking Whether or not the user is speaking */ if (this.status === Constants.VoiceStatus.CONNECTED) this.emit('speaking', user, speaking); guild._memberSpeakUpdate(user_id, speaking); } /** * Handles synthesizing of the speaking event. * @param {Buffer} buffer Received packet from the UDP socket * @private */ onUDPMessage(buffer) { const ssrc = +buffer.readUInt32BE(8).toString(10); const user = this.client.users.get(this.ssrcMap.get(ssrc)); if (!user) return; let speakingTimeout = this.speakingTimeouts.get(ssrc); if (typeof speakingTimeout === 'undefined') { this.onSpeaking({ user_id: user.id, ssrc, speaking: true }); } else { this.client.clearTimeout(speakingTimeout); } speakingTimeout = this.client.setTimeout(() => { try { this.onSpeaking({ user_id: user.id, ssrc, speaking: false }); this.client.clearTimeout(speakingTimeout); this.speakingTimeouts.delete(ssrc); } catch (ex) { // Connection already closed, ignore } }, DISCORD_SPEAKING_DELAY); this.speakingTimeouts.set(ssrc, speakingTimeout); } /** * Options that can be passed to stream-playing methods: * @typedef {Object} StreamOptions * @property {number} [seek=0] The time to seek to * @property {number} [volume=1] The volume to play at * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss * @property {number|string} [bitrate=48000] The bitrate (quality) of the audio. * If set to 'auto', the voice channel's bitrate will be used */ /** * Play the given file in the voice connection. * @param {string} file The absolute path to the file * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} * @example * // Play files natively * voiceChannel.join() * .then(connection => { * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); * }) * .catch(console.error); */ playFile(file, options) { return this.player.playUnknownStream(`file:${file}`, options); } /** * Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) * @param {string} input the arbitrary input * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} */ playArbitraryInput(input, options) { return this.player.playUnknownStream(input, options); } /** * Plays and converts an audio stream in the voice connection. * @param {ReadableStream} stream The audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} * @example * // Play streams using ytdl-core * const ytdl = require('ytdl-core'); * const streamOptions = { seek: 0, volume: 1 }; * voiceChannel.join() * .then(connection => { * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); * const dispatcher = connection.playStream(stream, streamOptions); * }) * .catch(console.error); */ playStream(stream, options) { return this.player.playUnknownStream(stream, options); } /** * Plays a stream of 16-bit signed stereo PCM. * @param {ReadableStream} stream The audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} */ playConvertedStream(stream, options) { return this.player.playPCMStream(stream, options); } /** * Plays an Opus encoded stream. * <warn>Note that inline volume is not compatible with this method.</warn> * @param {ReadableStream} stream The Opus audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} */ playOpusStream(stream, options) { return this.player.playOpusStream(stream, options); } /** * Plays a voice broadcast. * @param {VoiceBroadcast} broadcast The broadcast to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} * @example * // Play a broadcast * const broadcast = client * .createVoiceBroadcast() * .playFile('./test.mp3'); * const dispatcher = voiceConnection.playBroadcast(broadcast); */ playBroadcast(broadcast, options) { return this.player.playBroadcast(broadcast, options); } /** * Creates a VoiceReceiver so you can start listening to voice data. * It's recommended to only create one of these. * @returns {VoiceReceiver} */ createReceiver() { const receiver = new VoiceReceiver(this); this.receivers.push(receiver); return receiver; } } module.exports = VoiceConnection;