diff --git a/src/index.js b/src/index.js index fd15c23..7d40f49 100644 --- a/src/index.js +++ b/src/index.js @@ -4,15 +4,15 @@ Sentry.init({ dsn: process.env.SENTRY_DSN }) const MusicManager = require('./structures/MusicManager') const manager = new MusicManager() manager.connect().then(async () => { - const [ song ] = await manager.getSongs('ytsearch:30 sec video') - // const player = await manager.lavalink.join({ - // guild: '445203868624748555', - // channel: '701928171519344801', - // node: '1' - // }, { selfdeaf: true }) + const [song] = await manager.getSongs('ytsearch:30 sec video') + // const player = await manager.lavalink.join({ + // guild: '445203868624748555', + // channel: '701928171519344801', + // node: '1' + // }, { selfdeaf: true }) - console.log(song) + console.log(song) - // await player.play(song) - // player.once("error", error => console.error(error)) + // await player.play(song) + // player.once("error", error => console.error(error)) }) diff --git a/src/structures/MusicManager.js b/src/structures/MusicManager.js index 4f16e52..4410d64 100644 --- a/src/structures/MusicManager.js +++ b/src/structures/MusicManager.js @@ -1,7 +1,6 @@ const Client = require('./discord/Client') const LavacordManager = require('./lavacord/Manager') const MusicPlayer = require('./lavacord/MusicPlayer') -const Song = require('./lavacord/Song') const APIController = require('./http/APIController') const fetch = require('node-fetch') @@ -13,89 +12,89 @@ const dnsLookup = promisify(lookup) // TODO: Create SongProvider class class MusicManager { - constructor (clientOptions, lavacordOptions = {}) { - this.clientOptions = clientOptions || {} - this.lavacordOptions = Object.assign({ - Player: MusicPlayer - }, lavacordOptions) - - this.handleClientError = this.handleClientError.bind(this) - } - - connect () { - return this.connectClient() - .then(() => this.connectLavalink()) - .then(() => this.startHTTPServer()) + constructor (clientOptions, lavacordOptions = {}) { + this.clientOptions = clientOptions || {} + this.lavacordOptions = Object.assign({ + Player: MusicPlayer + }, lavacordOptions) + + this.handleClientError = this.handleClientError.bind(this) + } + + connect () { + return this.connectClient() + .then(() => this.connectLavalink()) + .then(() => this.startHTTPServer()) + } + + // Discord + connectClient () { + const CLUSTER_ID = process.env.INDEX_CLUSTER_ID_FROM_ONE ? parseInt(process.env.CLUSTER_ID) - 1 : parseInt(process.env.CLUSTER_ID) + const maxShards = parseInt(process.env.MAX_SHARDS) + const firstShardID = maxShards ? 0 : CLUSTER_ID * parseInt(process.env.SHARDS_PER_CLUSTER) + const lastShardID = maxShards ? maxShards - 1 : ((CLUSTER_ID + 1) * parseInt(process.env.SHARDS_PER_CLUSTER)) - 1 + + this.client = new Client(process.env.DISCORD_TOKEN, Object.assign({ + compress: true, + userId: process.env.USER_ID, + firstShardID, + lastShardID, + maxShards: maxShards || parseInt(process.env.SHARDS_PER_CLUSTER) * parseInt(process.env.MAX_CLUSTERS) + }, this.clientOptions)) + + this.client.on('error', this.handleClientError) + + return this.client.connect() + } + + handleClientError (...args) { + console.error('CLIENT', ...args) + } + + // Lavalink + async connectLavalink () { + const nodes = await this.lavalinkNodes() + this.lavalink = new LavacordManager(this.client, nodes, this.lavacordOptions) + return this.lavalink.connect() + } + + async lavalinkNodes () { + let lavalinkNodes + try { + lavalinkNodes = require.main.require('../lavalink_nodes.json') + } catch (err) {} + + if (!lavalinkNodes) { + const addresses = await dnsLookup('tasks.lavalink', { all: true }) + lavalinkNodes = addresses.map((host, i) => ({ + id: i++, + host, + port: process.env.LAVALINK_PORT, + password: process.env.LAVALINK_PASSWORD + })) } - // Discord - connectClient () { - const CLUSTER_ID = process.env.INDEX_CLUSTER_ID_FROM_ONE ? parseInt(process.env.CLUSTER_ID) - 1 : parseInt(process.env.CLUSTER_ID) - const maxShards = parseInt(process.env.MAX_SHARDS) - const firstShardID = maxShards ? 0 : CLUSTER_ID * parseInt(process.env.SHARDS_PER_CLUSTER) - const lastShardID = maxShards ? maxShards - 1 : ((CLUSTER_ID + 1) * parseInt(process.env.SHARDS_PER_CLUSTER)) - 1 - - this.client = new Client(process.env.DISCORD_TOKEN, Object.assign({ - compress: true, - userId: process.env.USER_ID, - firstShardID, - lastShardID, - maxShards: maxShards || parseInt(process.env.SHARDS_PER_CLUSTER) * parseInt(process.env.MAX_CLUSTERS) - }, this.clientOptions)) - - this.client.on('error', this.handleClientError) - - return this.client.connect() - } - - handleClientError (...args) { - console.error('CLIENT', ...args) - } - - // Lavalink - async connectLavalink () { - const nodes = await this.lavalinkNodes() - this.lavalink = new LavacordManager(this.client, nodes, this.lavacordOptions) - return this.lavalink.connect() - } - - async lavalinkNodes () { - let lavalinkNodes - try { - lavalinkNodes = require.main.require('../lavalink_nodes.json') - } catch (err) {} - - if (!lavalinkNodes) { - const addresses = await dnsLookup('tasks.lavalink', { all: true }) - lavalinkNodes = addresses.map((host, i) => ({ - id: i++, - host, - port: process.env.LAVALINK_PORT, - password: process.env.LAVALINK_PASSWORD - })) - } - - return lavalinkNodes - } - - getSongs (search) { - const [ node ] = this.lavalink.idealNodes - const params = new URLSearchParams() - params.append('identifier', search) - return fetch(`http://${node.host}:${node.port}/loadtracks?${params}`, { headers: { Authorization: node.password } }) - .then(res => res.json()) - .then(data => data.tracks) - .catch(err => { - console.error(err) - return [] - }) - } - - // HTTP - startHTTPServer () { - this.api = new APIController(this) - return this.api.start(process.env.PORT) - } + return lavalinkNodes + } + + getSongs (search) { + const [node] = this.lavalink.idealNodes + const params = new URLSearchParams() + params.append('identifier', search) + return fetch(`http://${node.host}:${node.port}/loadtracks?${params}`, { headers: { Authorization: node.password } }) + .then(res => res.json()) + .then(data => data.tracks) + .catch(err => { + console.error(err) + return [] + }) + } + + // HTTP + startHTTPServer () { + this.api = new APIController(this) + return this.api.start(process.env.PORT) + } } -module.exports = MusicManager \ No newline at end of file +module.exports = MusicManager diff --git a/src/structures/discord/Bucket.js b/src/structures/discord/Bucket.js index e304fe9..6b1eb90 100644 --- a/src/structures/discord/Bucket.js +++ b/src/structures/discord/Bucket.js @@ -1,56 +1,56 @@ class Bucket { - constructor(tokenLimit, interval, options = {}) { - this.tokenLimit = tokenLimit - this.interval = interval - this.latencyRef = options.latencyRef || { latency: 0 } - this.lastReset = this.tokens = this.lastSend = 0 - this.reservedTokens = options.reservedTokens || 0 - this._queue = [] - } + constructor (tokenLimit, interval, options = {}) { + this.tokenLimit = tokenLimit + this.interval = interval + this.latencyRef = options.latencyRef || { latency: 0 } + this.lastReset = this.tokens = this.lastSend = 0 + this.reservedTokens = options.reservedTokens || 0 + this._queue = [] + } - queue(func, priority=false) { - if(priority) { - this._queue.unshift({func, priority}) - } else { - this._queue.push({func, priority}) - } - this.check() + queue (func, priority = false) { + if (priority) { + this._queue.unshift({ func, priority }) + } else { + this._queue.push({ func, priority }) } + this.check() + } - check() { - if(this.timeout || this._queue.length === 0) { - return - } - if(this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency < Date.now()) { - this.lastReset = Date.now() - this.tokens = Math.max(0, this.tokens - this.tokenLimit) - } + check () { + if (this.timeout || this._queue.length === 0) { + return + } + if (this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency < Date.now()) { + this.lastReset = Date.now() + this.tokens = Math.max(0, this.tokens - this.tokenLimit) + } - let val; - const tokensAvailable = this.tokens < this.tokenLimit - const unreservedTokensAvailable = this.tokens < (this.tokenLimit - this.reservedTokens) - while(this._queue.length > 0 && (unreservedTokensAvailable || (tokensAvailable && this._queue[0].priority))) { - this.tokens++ - const item = this._queue.shift() - val = this.latencyRef.latency - Date.now() + this.lastSend - if(this.latencyRef.latency === 0 || val <= 0) { - item.func() - this.lastSend = Date.now() - } else { - setTimeout(() => { - item.func() - }, val) - this.lastSend = Date.now() + val - } - } + let val + const tokensAvailable = this.tokens < this.tokenLimit + const unreservedTokensAvailable = this.tokens < (this.tokenLimit - this.reservedTokens) + while (this._queue.length > 0 && (unreservedTokensAvailable || (tokensAvailable && this._queue[0].priority))) { + this.tokens++ + const item = this._queue.shift() + val = this.latencyRef.latency - Date.now() + this.lastSend + if (this.latencyRef.latency === 0 || val <= 0) { + item.func() + this.lastSend = Date.now() + } else { + setTimeout(() => { + item.func() + }, val) + this.lastSend = Date.now() + val + } + } - if(this._queue.length > 0 && !this.timeout) { - this.timeout = setTimeout(() => { - this.timeout = null - this.check() - }, this.tokens < this.tokenLimit ? this.latencyRef.latency : Math.max(0, this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency - Date.now())) - } + if (this._queue.length > 0 && !this.timeout) { + this.timeout = setTimeout(() => { + this.timeout = null + this.check() + }, this.tokens < this.tokenLimit ? this.latencyRef.latency : Math.max(0, this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency - Date.now())) } + } } -module.exports = Bucket \ No newline at end of file +module.exports = Bucket diff --git a/src/structures/discord/Client.js b/src/structures/discord/Client.js index f0255bd..882acbe 100644 --- a/src/structures/discord/Client.js +++ b/src/structures/discord/Client.js @@ -4,154 +4,154 @@ const ShardManager = require('./ShardManager') const fetch = require('node-fetch') let EventEmitter try { - EventEmitter = require("eventemitter3") -} catch(err) { - EventEmitter = require("events") + EventEmitter = require('eventemitter3') +} catch (err) { + EventEmitter = require('events') } -let Erlpack; +let Erlpack try { - Erlpack = require("erlpack") -} catch(err) { + Erlpack = require('erlpack') +} catch (err) { } -let ZlibSync; +let ZlibSync try { - ZlibSync = require("zlib-sync") -} catch(err) { - try { - ZlibSync = require("pako") - } catch(err) { - } + ZlibSync = require('zlib-sync') +} catch (err) { + try { + ZlibSync = require('pako') + } catch (err) { + } } const sleep = (ms) => new Promise((res) => setTimeout(res, ms)) class Client extends EventEmitter { - constructor(token, options) { - super(); - this.options = Object.assign({ - autoreconnect: true, - compress: false, - connectionTimeout: 30000, - disableEvents: {}, - firstShardID: 0, - largeThreshold: 250, - latencyThreshold: 30000, - maxShards: 1, - messageLimit: 100, - opusOnly: false, - ratelimiterOffset: 0, - requestTimeout: 15000, - restMode: false, - seedVoiceConnections: false, - ws: {}, - agent: null, - maxReconnectAttempts: Infinity, - reconnectDelay: (lastDelay, attempts) => Math.pow(attempts + 1, 0.7) * 20000 - }, options) - if(this.options.lastShardID === undefined && this.options.maxShards !== "auto") { - this.options.lastShardID = this.options.maxShards - 1 - } - if(this.options.agent && !(this.options.ws && this.options.ws.agent)) { - this.options.ws = this.options.ws || {} - this.options.ws.agent = this.options.agent - } - if(this.options.hasOwnProperty("intents")) { - // Resolve intents option to the proper integer - if(Array.isArray(this.options.intents)) { - let bitmask = 0; - for(const intent of this.options.intents) { - if(Constants.Intents[intent]) { - bitmask |= Constants.Intents[intent] - } - } - this.options.intents = bitmask - } - - // Ensure requesting all guild members isn't destined to fail - if(this.options.getAllUsers && !(this.options.intents & Constants.Intents.guildMembers)) { - throw new Error("Cannot request all members without guildMembers intent") - } + constructor (token, options) { + super() + this.options = Object.assign({ + autoreconnect: true, + compress: false, + connectionTimeout: 30000, + disableEvents: {}, + firstShardID: 0, + largeThreshold: 250, + latencyThreshold: 30000, + maxShards: 1, + messageLimit: 100, + opusOnly: false, + ratelimiterOffset: 0, + requestTimeout: 15000, + restMode: false, + seedVoiceConnections: false, + ws: {}, + agent: null, + maxReconnectAttempts: Infinity, + reconnectDelay: (lastDelay, attempts) => Math.pow(attempts + 1, 0.7) * 20000 + }, options) + if (this.options.lastShardID === undefined && this.options.maxShards !== 'auto') { + this.options.lastShardID = this.options.maxShards - 1 + } + if (this.options.agent && !(this.options.ws && this.options.ws.agent)) { + this.options.ws = this.options.ws || {} + this.options.ws.agent = this.options.agent + } + if (this.options.hasOwnProperty('intents')) { + // Resolve intents option to the proper integer + if (Array.isArray(this.options.intents)) { + let bitmask = 0 + for (const intent of this.options.intents) { + if (Constants.Intents[intent]) { + bitmask |= Constants.Intents[intent] + } } + this.options.intents = bitmask + } - this.token = token - this.userId = this.options.userId - this.startTime = 0 - this.lastConnect = 0 - this.shards = new ShardManager(this) - this.guildShardMap = {} - - this.connect = this.connect.bind(this) - this.lastReconnectDelay = 0 - this.reconnectAttempts = 0 + // Ensure requesting all guild members isn't destined to fail + if (this.options.getAllUsers && !(this.options.intents & Constants.Intents.guildMembers)) { + throw new Error('Cannot request all members without guildMembers intent') + } } - get uptime() { - return this.startTime ? Date.now() - this.startTime : 0 - } + this.token = token + this.userId = this.options.userId + this.startTime = 0 + this.lastConnect = 0 + this.shards = new ShardManager(this) + this.guildShardMap = {} + + this.connect = this.connect.bind(this) + this.lastReconnectDelay = 0 + this.reconnectAttempts = 0 + } - async connect() { - try { - const data = await (this.options.maxShards === "auto" ? this.getBotGateway() : this.getGateway()) - if(!data.url || (this.options.maxShards === "auto" && !data.shards)) { - throw new Error("Invalid response from gateway REST call") - } - if(data.url.includes("?")) { - data.url = data.url.substring(0, data.url.indexOf("?")) - } - if(!data.url.endsWith("/")) { - data.url += "/" - } - this.gatewayURL = `${data.url}?v=${Constants.GATEWAY_VERSION}&encoding=${Erlpack ? "etf" : "json"}` + get uptime () { + return this.startTime ? Date.now() - this.startTime : 0 + } - if(this.options.compress) { - this.gatewayURL += "&compress=zlib-stream" - } + async connect () { + try { + const data = await (this.options.maxShards === 'auto' ? this.getBotGateway() : this.getGateway()) + if (!data.url || (this.options.maxShards === 'auto' && !data.shards)) { + throw new Error('Invalid response from gateway REST call') + } + if (data.url.includes('?')) { + data.url = data.url.substring(0, data.url.indexOf('?')) + } + if (!data.url.endsWith('/')) { + data.url += '/' + } + this.gatewayURL = `${data.url}?v=${Constants.GATEWAY_VERSION}&encoding=${Erlpack ? 'etf' : 'json'}` - if(this.options.maxShards === "auto") { - if(!data.shards) { - throw new Error("Failed to autoshard due to lack of data from Discord."); - } - this.options.maxShards = data.shards - if(this.options.lastShardID === undefined) { - this.options.lastShardID = data.shards - 1 - } - } + if (this.options.compress) { + this.gatewayURL += '&compress=zlib-stream' + } - for(let i = this.options.firstShardID; i <= this.options.lastShardID; ++i) { - this.shards.spawn(i) - } - } catch(err) { - console.error(err) - if(!this.options.autoreconnect) { - throw err - } - const reconnectDelay = this.options.reconnectDelay(this.lastReconnectDelay, this.reconnectAttempts) - await sleep(reconnectDelay) - this.lastReconnectDelay = reconnectDelay - this.reconnectAttempts = this.reconnectAttempts + 1 - return this.connect() + if (this.options.maxShards === 'auto') { + if (!data.shards) { + throw new Error('Failed to autoshard due to lack of data from Discord.') } - } + this.options.maxShards = data.shards + if (this.options.lastShardID === undefined) { + this.options.lastShardID = data.shards - 1 + } + } - getGateway() { - return this.restRequest('GET', Constants.Endpoints.GATEWAY) + for (let i = this.options.firstShardID; i <= this.options.lastShardID; ++i) { + this.shards.spawn(i) + } + } catch (err) { + console.error(err) + if (!this.options.autoreconnect) { + throw err + } + const reconnectDelay = this.options.reconnectDelay(this.lastReconnectDelay, this.reconnectAttempts) + await sleep(reconnectDelay) + this.lastReconnectDelay = reconnectDelay + this.reconnectAttempts = this.reconnectAttempts + 1 + return this.connect() } + } - getBotGateway() { - if(!this.token.startsWith('Bot ')) { - this.token = 'Bot ' + this.token - } - return this.restRequest('GET', Constants.Endpoints.GATEWAY_BOT, true) + getGateway () { + return this.restRequest('GET', Constants.Endpoints.GATEWAY) + } + + getBotGateway () { + if (!this.token.startsWith('Bot ')) { + this.token = 'Bot ' + this.token } + return this.restRequest('GET', Constants.Endpoints.GATEWAY_BOT, true) + } - restRequest (method, endpoint, auth) { - const options = { method } - if (auth) { - options.headers = { - Authorization: this.token - } - } - return fetch('https://discordapp.com' + Constants.REST_BASE_URL + endpoint, options).then(res => res.json()) + restRequest (method, endpoint, auth) { + const options = { method } + if (auth) { + options.headers = { + Authorization: this.token + } } + return fetch('https://discordapp.com' + Constants.REST_BASE_URL + endpoint, options).then(res => res.json()) + } } module.exports = Client diff --git a/src/structures/discord/Shard.js b/src/structures/discord/Shard.js index 5947fee..016d257 100644 --- a/src/structures/discord/Shard.js +++ b/src/structures/discord/Shard.js @@ -1,433 +1,433 @@ -const Bucket = require('./Bucket'); +const Bucket = require('./Bucket') const { GATEWAY_VERSION, GatewayOPCodes } = require('../../utils/Constants') -let WebSocket = typeof window !== "undefined" ? window.WebSocket : require('ws') +let WebSocket = typeof window !== 'undefined' ? window.WebSocket : require('ws') -let EventEmitter; +let EventEmitter try { - EventEmitter = require('eventemitter3') -} catch(err) { - EventEmitter = require('events').EventEmitter + EventEmitter = require('eventemitter3') +} catch (err) { + EventEmitter = require('events').EventEmitter } -let Erlpack; +let Erlpack try { - Erlpack = require('erlpack') -} catch(err) { + Erlpack = require('erlpack') +} catch (err) { } -let ZlibSync; +let ZlibSync try { - ZlibSync = require('zlib-sync') -} catch(err) { - try { - ZlibSync = require('pako') - } catch(err) { - } + ZlibSync = require('zlib-sync') +} catch (err) { + try { + ZlibSync = require('pako') + } catch (err) { + } } try { - WebSocket = require('uws') -} catch(err) { + WebSocket = require('uws') +} catch (err) { } class Shard extends EventEmitter { - constructor(id, client) { - super() + constructor (id, client) { + super() - this.id = id - this.client = client + this.id = id + this.client = client - this.onWSMessage = this.onWSMessage.bind(this) + this.onWSMessage = this.onWSMessage.bind(this) - this.hardReset() - } + this.hardReset() + } - get latency() { - return this.lastHeartbeatSent && this.lastHeartbeatReceived ? this.lastHeartbeatReceived - this.lastHeartbeatSent : Infinity - } + get latency () { + return this.lastHeartbeatSent && this.lastHeartbeatReceived ? this.lastHeartbeatReceived - this.lastHeartbeatSent : Infinity + } - connect() { - if(this.ws && this.ws.readyState != WebSocket.CLOSED) { - this.emit("error", new Error("Existing connection detected"), this.id) - return - } - ++this.connectAttempts - this.connecting = true - return this.initializeWS() + connect () { + if (this.ws && this.ws.readyState != WebSocket.CLOSED) { + this.emit('error', new Error('Existing connection detected'), this.id) + return } - - disconnect(options = {}, error) { - if(!this.ws) { - return - } - if(this.heartbeatInterval) { - clearInterval(this.heartbeatInterval) - this.heartbeatInterval = null - } - - this.ws.onclose = undefined - try { - if(options.reconnect && this.sessionID) { - this.ws.terminate() - } else { - this.ws.close(1000) - } - } catch(err) { - this.emit("error", err, this.id) - } - - this.ws = null - this.reset() - - super.emit("disconnect", error || null) - - if(options.reconnect === "auto" && this.client.options.autoreconnect) { - if(this.sessionID) { - this.emit("debug", `Immediately reconnecting for potential resume | Attempt ${this.connectAttempts}`, this.id) - this.client.shards.connect(this) - } else { - this.emit("debug", `Queueing reconnect in ${this.reconnectInterval}ms | Attempt ${this.connectAttempts}`, this.id) - setTimeout(() => { - this.client.shards.connect(this) - }, this.reconnectInterval) - this.reconnectInterval = Math.min(Math.round(this.reconnectInterval * (Math.random() * 2 + 1)), 30000) - } - } else if(!options.reconnect) { - this.hardReset() - } + ++this.connectAttempts + this.connecting = true + return this.initializeWS() + } + + disconnect (options = {}, error) { + if (!this.ws) { + return } - - reset() { - this.connecting = false - this.ready = false - this.preReady = false - this.lastHeartbeatAck = true - this.lastHeartbeatReceived = null - this.lastHeartbeatSent = null - this.status = "disconnected" + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + this.heartbeatInterval = null } - hardReset() { - this.reset() - this.seq = 0 - this.sessionID = null - this.reconnectInterval = 1000 - this.connectAttempts = 0 - this.ws = null - this.heartbeatInterval = null - this.globalBucket = new Bucket(120, 60000, { reservedTokens: 5 }) + this.ws.onclose = undefined + try { + if (options.reconnect && this.sessionID) { + this.ws.terminate() + } else { + this.ws.close(1000) + } + } catch (err) { + this.emit('error', err, this.id) } - resume() { - this.status = "resuming"; - this.sendWS(GatewayOPCodes.RESUME, { - token: this.client.token, - session_id: this.sessionID, - seq: this.seq - }); - } + this.ws = null + this.reset() - identify() { - if(this.client.options.compress && !ZlibSync) { - this.emit("error", new Error("pako/zlib-sync not found, cannot decompress data")) - return - } - const identify = { - token: this.client.token, - v: GATEWAY_VERSION, - compress: !!this.client.options.compress, - large_threshold: this.client.options.largeThreshold, - guild_subscriptions: !!this.client.options.guildSubscriptions, - intents: this.client.options.intents, - properties: { - "os": process.platform, - "browser": "SwitchbladeMusic", - "device": "SwitchbladeMusic" - } - } - this.sendWS(GatewayOPCodes.IDENTIFY, identify) - } + super.emit('disconnect', error || null) - wsEvent(packet) { - switch(packet.t) { - case 'RESUMED': - case 'READY': { - this.connectAttempts = 0 - this.reconnectInterval = 1000 + if (options.reconnect === 'auto' && this.client.options.autoreconnect) { + if (this.sessionID) { + this.emit('debug', `Immediately reconnecting for potential resume | Attempt ${this.connectAttempts}`, this.id) + this.client.shards.connect(this) + } else { + this.emit('debug', `Queueing reconnect in ${this.reconnectInterval}ms | Attempt ${this.connectAttempts}`, this.id) + setTimeout(() => { + this.client.shards.connect(this) + }, this.reconnectInterval) + this.reconnectInterval = Math.min(Math.round(this.reconnectInterval * (Math.random() * 2 + 1)), 30000) + } + } else if (!options.reconnect) { + this.hardReset() + } + } + + reset () { + this.connecting = false + this.ready = false + this.preReady = false + this.lastHeartbeatAck = true + this.lastHeartbeatReceived = null + this.lastHeartbeatSent = null + this.status = 'disconnected' + } + + hardReset () { + this.reset() + this.seq = 0 + this.sessionID = null + this.reconnectInterval = 1000 + this.connectAttempts = 0 + this.ws = null + this.heartbeatInterval = null + this.globalBucket = new Bucket(120, 60000, { reservedTokens: 5 }) + } + + resume () { + this.status = 'resuming' + this.sendWS(GatewayOPCodes.RESUME, { + token: this.client.token, + session_id: this.sessionID, + seq: this.seq + }) + } + + identify () { + if (this.client.options.compress && !ZlibSync) { + this.emit('error', new Error('pako/zlib-sync not found, cannot decompress data')) + return + } + const identify = { + token: this.client.token, + v: GATEWAY_VERSION, + compress: !!this.client.options.compress, + large_threshold: this.client.options.largeThreshold, + guild_subscriptions: !!this.client.options.guildSubscriptions, + intents: this.client.options.intents, + properties: { + os: process.platform, + browser: 'SwitchbladeMusic', + device: 'SwitchbladeMusic' + } + } + this.sendWS(GatewayOPCodes.IDENTIFY, identify) + } - this.connecting = false - this.status = 'ready' - this.client.shards._readyPacketCB() + wsEvent (packet) { + switch (packet.t) { + case 'RESUMED': + case 'READY': { + this.connectAttempts = 0 + this.reconnectInterval = 1000 - if(packet.t === 'RESUMED') { - this.preReady = true - this.ready = true + this.connecting = false + this.status = 'ready' + this.client.shards._readyPacketCB() - super.emit('resume') - break - } + if (packet.t === 'RESUMED') { + this.preReady = true + this.ready = true - if(!this.client.token.startsWith('Bot ')) { - this.client.token = 'Bot ' + this.client.token - } + super.emit('resume') + break + } - if(packet.d._trace) { - this.discordServerTrace = packet.d._trace - } + if (!this.client.token.startsWith('Bot ')) { + this.client.token = 'Bot ' + this.client.token + } - this.sessionID = packet.d.session_id + if (packet.d._trace) { + this.discordServerTrace = packet.d._trace + } - this.preReady = true - this.emit('shardPreReady', this.id) + this.sessionID = packet.d.session_id - this.checkReady() + this.preReady = true + this.emit('shardPreReady', this.id) - break - } - case 'VOICE_SERVER_UPDATE': { - packet.d.session_id = this.sessionID - packet.d.shard = this - break - } - case 'GUILD_CREATE': { - if(!packet.d.unavailable) { - this.client.guildShardMap[packet.d.id] = this.id - } - } - default: { - this.emit("unknown", packet, this.id) - break - } - } /* eslint-enable no-redeclare */ - } + this.checkReady() - checkReady() { - if(!this.ready) { - this.ready = true - super.emit("ready") + break + } + case 'VOICE_SERVER_UPDATE': { + packet.d.session_id = this.sessionID + packet.d.shard = this + break + } + case 'GUILD_CREATE': { + if (!packet.d.unavailable) { + this.client.guildShardMap[packet.d.id] = this.id } + } + default: { + this.emit('unknown', packet, this.id) + break + } + } /* eslint-enable no-redeclare */ + } + + checkReady () { + if (!this.ready) { + this.ready = true + super.emit('ready') } + } - initializeWS() { - if(!this.client.token) { - return this.disconnect(null, new Error("Token not specified")) - } + initializeWS () { + if (!this.client.token) { + return this.disconnect(null, new Error('Token not specified')) + } - this.status = "connecting" - if(this.client.options.compress) { - this.emit("debug", "Initializing zlib-sync-based compression") - this._zlibSync = new ZlibSync.Inflate({ - chunkSize: 128 * 1024 - }); + this.status = 'connecting' + if (this.client.options.compress) { + this.emit('debug', 'Initializing zlib-sync-based compression') + this._zlibSync = new ZlibSync.Inflate({ + chunkSize: 128 * 1024 + }) + } + this.ws = new WebSocket(this.client.gatewayURL, this.client.options.ws) + this.ws.onopen = () => { + this.status = 'handshaking' + this.emit('connect', this.id) + this.lastHeartbeatAck = true + } + this.ws.onmessage = (m) => { + try { + let { data } = m + if (data instanceof ArrayBuffer) { + if (this.client.options.compress || Erlpack) { + data = Buffer.from(data) + } + } else if (Array.isArray(data)) { // Fragmented messages + data = Buffer.concat(data) // Copyfull concat is slow, but no alternative } - this.ws = new WebSocket(this.client.gatewayURL, this.client.options.ws) - this.ws.onopen = () => { - this.status = "handshaking" - this.emit("connect", this.id) - this.lastHeartbeatAck = true - }; - this.ws.onmessage = (m) => { - try { - let { data } = m - if(data instanceof ArrayBuffer) { - if(this.client.options.compress || Erlpack) { - data = Buffer.from(data) - } - } else if(Array.isArray(data)) { // Fragmented messages - data = Buffer.concat(data) // Copyfull concat is slow, but no alternative - } - if(this.client.options.compress) { - if(data.length >= 4 && data.readUInt32BE(data.length - 4) === 0xFFFF) { - this._zlibSync.push(data, ZlibSync.Z_SYNC_FLUSH) - if(this._zlibSync.err) { - this.emit("error", new Error(`zlib error ${this._zlibSync.err}: ${this._zlibSync.msg}`)) - return - } - - data = Buffer.from(this._zlibSync.result) - if(Erlpack) { - this.onWSMessage(Erlpack.unpack(data)) - } else { - return this.onWSMessage(JSON.parse(data.toString())) - } - } else { - this._zlibSync.push(data, false) - } - } else if(Erlpack) { - return this.onWSMessage(Erlpack.unpack(data)) - } else { - return this.onWSMessage(JSON.parse(data.toString())) - } - } catch(err) { - this.emit("error", err, this.id) + if (this.client.options.compress) { + if (data.length >= 4 && data.readUInt32BE(data.length - 4) === 0xFFFF) { + this._zlibSync.push(data, ZlibSync.Z_SYNC_FLUSH) + if (this._zlibSync.err) { + this.emit('error', new Error(`zlib error ${this._zlibSync.err}: ${this._zlibSync.msg}`)) + return } - }; - this.ws.onerror = (event) => { - this.emit("error", event, this.id) - }; - this.ws.onclose = (event) => { - let err = !event.code || event.code === 1000 ? null : new Error(event.code + ": " + event.reason) - let reconnect = "auto" - if(event.code) { - this.emit("debug", `${event.code === 1000 ? "Clean" : "Unclean"} WS close: ${event.code}: ${event.reason}`, this.id) - if(event.code === 4001) { - err = new Error("Gateway received invalid OP code") - } else if(event.code === 4002) { - err = new Error("Gateway received invalid message") - } else if(event.code === 4003) { - err = new Error("Not authenticated") - this.sessionID = null; - } else if(event.code === 4004) { - err = new Error("Authentication failed") - this.sessionID = null; - reconnect = false; - this.emit("error", new Error(`Invalid token: ${this.client.token}`)) - } else if(event.code === 4005) { - err = new Error("Already authenticated") - } else if(event.code === 4006 || event.code === 4009) { - err = new Error("Invalid session") - this.sessionID = null; - } else if(event.code === 4007) { - err = new Error("Invalid sequence number: " + this.seq) - this.seq = 0; - } else if(event.code === 4008) { - err = new Error("Gateway connection was ratelimited") - } else if(event.code === 4010) { - err = new Error("Invalid shard key") - this.sessionID = null; - reconnect = false; - } else if(event.code === 4011) { - err = new Error("Shard has too many guilds (>2500)") - this.sessionID = null; - reconnect = false; - } else if(event.code === 4013) { - err = new Error("Invalid intents specified") - this.sessionID = null; - reconnect = false; - } else if(event.code === 4014) { - err = new Error("Disallowed intents specified") - this.sessionID = null; - reconnect = false; - } else if(event.code === 1006) { - err = new Error("Connection reset by peer") - } else if(!event.wasClean && event.reason) { - err = new Error(event.code + ": " + event.reason) - } + + data = Buffer.from(this._zlibSync.result) + if (Erlpack) { + this.onWSMessage(Erlpack.unpack(data)) } else { - this.emit("debug", "WS close: unknown code: " + event.reason, this.id) + return this.onWSMessage(JSON.parse(data.toString())) } - this.disconnect({ - reconnect - }, err) + } else { + this._zlibSync.push(data, false) + } + } else if (Erlpack) { + return this.onWSMessage(Erlpack.unpack(data)) + } else { + return this.onWSMessage(JSON.parse(data.toString())) + } + } catch (err) { + this.emit('error', err, this.id) + } + } + this.ws.onerror = (event) => { + this.emit('error', event, this.id) + } + this.ws.onclose = (event) => { + let err = !event.code || event.code === 1000 ? null : new Error(event.code + ': ' + event.reason) + let reconnect = 'auto' + if (event.code) { + this.emit('debug', `${event.code === 1000 ? 'Clean' : 'Unclean'} WS close: ${event.code}: ${event.reason}`, this.id) + if (event.code === 4001) { + err = new Error('Gateway received invalid OP code') + } else if (event.code === 4002) { + err = new Error('Gateway received invalid message') + } else if (event.code === 4003) { + err = new Error('Not authenticated') + this.sessionID = null + } else if (event.code === 4004) { + err = new Error('Authentication failed') + this.sessionID = null + reconnect = false + this.emit('error', new Error(`Invalid token: ${this.client.token}`)) + } else if (event.code === 4005) { + err = new Error('Already authenticated') + } else if (event.code === 4006 || event.code === 4009) { + err = new Error('Invalid session') + this.sessionID = null + } else if (event.code === 4007) { + err = new Error('Invalid sequence number: ' + this.seq) + this.seq = 0 + } else if (event.code === 4008) { + err = new Error('Gateway connection was ratelimited') + } else if (event.code === 4010) { + err = new Error('Invalid shard key') + this.sessionID = null + reconnect = false + } else if (event.code === 4011) { + err = new Error('Shard has too many guilds (>2500)') + this.sessionID = null + reconnect = false + } else if (event.code === 4013) { + err = new Error('Invalid intents specified') + this.sessionID = null + reconnect = false + } else if (event.code === 4014) { + err = new Error('Disallowed intents specified') + this.sessionID = null + reconnect = false + } else if (event.code === 1006) { + err = new Error('Connection reset by peer') + } else if (!event.wasClean && event.reason) { + err = new Error(event.code + ': ' + event.reason) } + } else { + this.emit('debug', 'WS close: unknown code: ' + event.reason, this.id) + } + this.disconnect({ + reconnect + }, err) + } - setTimeout(() => { - if(this.connecting) { - this.disconnect({ - reconnect: "auto" - }, new Error("Connection timeout")) - } - }, this.client.options.connectionTimeout) + setTimeout(() => { + if (this.connecting) { + this.disconnect({ + reconnect: 'auto' + }, new Error('Connection timeout')) + } + }, this.client.options.connectionTimeout) + } + + onWSMessage (packet) { + if (this.listeners('rawWS').length > 0 || this.client.listeners('rawWS').length) { + this.client.emit('rawWS', packet, this.id) } - onWSMessage(packet) { - if(this.listeners("rawWS").length > 0 || this.client.listeners("rawWS").length) { - this.client.emit("rawWS", packet, this.id) - } + if (packet.s) { + if (packet.s > this.seq + 1 && this.ws && this.status !== 'resuming') { + this.emit('warn', `Non-consecutive sequence (${this.seq} -> ${packet.s})`, this.id) + } + this.seq = packet.s + } - if(packet.s) { - if(packet.s > this.seq + 1 && this.ws && this.status !== "resuming") { - this.emit("warn", `Non-consecutive sequence (${this.seq} -> ${packet.s})`, this.id) - } - this.seq = packet.s + switch (packet.op) { + case GatewayOPCodes.EVENT: { + if (!this.client.options.disableEvents[packet.t]) { + this.wsEvent(packet) } - - switch(packet.op) { - case GatewayOPCodes.EVENT: { - if(!this.client.options.disableEvents[packet.t]) { - this.wsEvent(packet) - } - break - } - case GatewayOPCodes.HEARTBEAT: { - this.heartbeat() - break - } - case GatewayOPCodes.INVALID_SESSION: { - this.seq = 0 - this.sessionID = null - this.emit("warn", "Invalid session, reidentifying!", this.id) - this.identify() - break - } - case GatewayOPCodes.RECONNECT: { - this.disconnect({ - reconnect: "auto" - }) - break - } - case GatewayOPCodes.HELLO: { - if(packet.d.heartbeat_interval > 0) { - if(this.heartbeatInterval) { - clearInterval(this.heartbeatInterval) - } - this.heartbeatInterval = setInterval(() => this.heartbeat(true), packet.d.heartbeat_interval) - } - - this.discordServerTrace = packet.d._trace - this.connecting = false - - if(this.sessionID) { - this.resume() - } else { - this.identify() - } - this.heartbeat() - this.emit("hello", packet.d._trace, this.id) - break - } - case GatewayOPCodes.HEARTBEAT_ACK: { - this.lastHeartbeatAck = true - this.lastHeartbeatReceived = new Date().getTime() - break - } - default: { - this.emit("unknown", packet, this.id) - break - } + break + } + case GatewayOPCodes.HEARTBEAT: { + this.heartbeat() + break + } + case GatewayOPCodes.INVALID_SESSION: { + this.seq = 0 + this.sessionID = null + this.emit('warn', 'Invalid session, reidentifying!', this.id) + this.identify() + break + } + case GatewayOPCodes.RECONNECT: { + this.disconnect({ + reconnect: 'auto' + }) + break + } + case GatewayOPCodes.HELLO: { + if (packet.d.heartbeat_interval > 0) { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + } + this.heartbeatInterval = setInterval(() => this.heartbeat(true), packet.d.heartbeat_interval) } - } - heartbeat(normal) { - if(normal && !this.lastHeartbeatAck) { - return this.disconnect({ - reconnect: "auto" - }, new Error("Server didn't acknowledge previous heartbeat, possible lost connection")) + this.discordServerTrace = packet.d._trace + this.connecting = false + + if (this.sessionID) { + this.resume() + } else { + this.identify() } - this.lastHeartbeatAck = false - this.lastHeartbeatSent = new Date().getTime() - this.sendWS(GatewayOPCodes.HEARTBEAT, this.seq, true) + this.heartbeat() + this.emit('hello', packet.d._trace, this.id) + break + } + case GatewayOPCodes.HEARTBEAT_ACK: { + this.lastHeartbeatAck = true + this.lastHeartbeatReceived = new Date().getTime() + break + } + default: { + this.emit('unknown', packet, this.id) + break + } } + } - sendWS(op, _data, priority = false) { - if(this.ws && this.ws.readyState === WebSocket.OPEN) { - let i = 0 - let waitFor = 1 - const func = () => { - if(++i >= waitFor && this.ws && this.ws.readyState === WebSocket.OPEN) { - const data = Erlpack ? Erlpack.pack({op: op, d: _data}) : JSON.stringify({ op: op, d: _data }) - this.ws.send(data) - this.emit("debug", JSON.stringify({op: op, d: _data}), this.id) - } - }; - if(op === GatewayOPCodes.STATUS_UPDATE) { - ++waitFor - this.presenceUpdateBucket.queue(func, priority) - } - this.globalBucket.queue(func, priority) + heartbeat (normal) { + if (normal && !this.lastHeartbeatAck) { + return this.disconnect({ + reconnect: 'auto' + }, new Error("Server didn't acknowledge previous heartbeat, possible lost connection")) + } + this.lastHeartbeatAck = false + this.lastHeartbeatSent = new Date().getTime() + this.sendWS(GatewayOPCodes.HEARTBEAT, this.seq, true) + } + + sendWS (op, _data, priority = false) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + let i = 0 + let waitFor = 1 + const func = () => { + if (++i >= waitFor && this.ws && this.ws.readyState === WebSocket.OPEN) { + const data = Erlpack ? Erlpack.pack({ op: op, d: _data }) : JSON.stringify({ op: op, d: _data }) + this.ws.send(data) + this.emit('debug', JSON.stringify({ op: op, d: _data }), this.id) } + } + if (op === GatewayOPCodes.STATUS_UPDATE) { + ++waitFor + this.presenceUpdateBucket.queue(func, priority) + } + this.globalBucket.queue(func, priority) } + } - emit(event, ...args) { - super.emit.call(this, event, ...args) - } + emit (event, ...args) { + super.emit.call(this, event, ...args) + } } module.exports = Shard diff --git a/src/structures/discord/ShardManager.js b/src/structures/discord/ShardManager.js index 7926209..578c2e8 100644 --- a/src/structures/discord/ShardManager.js +++ b/src/structures/discord/ShardManager.js @@ -1,124 +1,124 @@ -const Shard = require("./Shard") +const Shard = require('./Shard') class ShardManager { - constructor(client) { - this._client = client; + constructor (client) { + this._client = client - this.connectQueue = []; - this.lastConnect = 0; - this.connectTimeout = null; - this.shards = new Map() - } + this.connectQueue = [] + this.lastConnect = 0 + this.connectTimeout = null + this.shards = new Map() + } - // Collection - get (id) { - return this.shards.get(id) - } + // Collection + get (id) { + return this.shards.get(id) + } - find(func) { - for(const item of this.shards.values()) { - if(func(item)) { - return item - } - } - return undefined + find (func) { + for (const item of this.shards.values()) { + if (func(item)) { + return item + } } + return undefined + } - get size () { - return this.shards.size - } + get size () { + return this.shards.size + } - // ShardManager - _readyPacketCB() { - this.lastConnect = Date.now() - this.tryConnect() - } + // ShardManager + _readyPacketCB () { + this.lastConnect = Date.now() + this.tryConnect() + } - connect(shard) { - if(shard.sessionID || (this.lastConnect <= Date.now() - 5000 && !this.find((shard) => shard.connecting))) { - shard.connect() - this.lastConnect = Date.now() + 7500 - } else { - this.connectQueue.push(shard) - this.tryConnect() - } + connect (shard) { + if (shard.sessionID || (this.lastConnect <= Date.now() - 5000 && !this.find((shard) => shard.connecting))) { + shard.connect() + this.lastConnect = Date.now() + 7500 + } else { + this.connectQueue.push(shard) + this.tryConnect() } + } - tryConnect() { - if(this.connectQueue.length > 0) { - if(this.lastConnect <= Date.now() - 5000) { - const shard = this.connectQueue.shift() - shard.connect() - this.lastConnect = Date.now() + 7500 - } else if(!this.connectTimeout) { - this.connectTimeout = setTimeout(() => { - this.connectTimeout = null - this.tryConnect() - }, 1000) - } - } + tryConnect () { + if (this.connectQueue.length > 0) { + if (this.lastConnect <= Date.now() - 5000) { + const shard = this.connectQueue.shift() + shard.connect() + this.lastConnect = Date.now() + 7500 + } else if (!this.connectTimeout) { + this.connectTimeout = setTimeout(() => { + this.connectTimeout = null + this.tryConnect() + }, 1000) + } } + } - spawn(id) { - let shard = this.shards.get(id) - if(!shard) { - shard = new Shard(id, this._client) - this.shards.set(id, shard) - shard.on("ready", () => { - this._client.emit("shardReady", shard.id) - if(this._client.ready) { - return - } - for(const other of this.shards.values()) { - if(!other.ready) { - return - } - } - this._client.ready = true - this._client.startTime = Date.now() - this._client.emit("ready") - }).on("resume", () => { - this._client.emit("shardResume", shard.id) - if(this._client.ready) { - return; - } - for(const other of this.shards.values()) { - if(!other.ready) { - return - } - } - this._client.ready = true - this._client.startTime = Date.now() - this._client.emit("ready") - }).on("disconnect", (error) => { - this._client.emit("shardDisconnect", error, shard.id) - for(const other of this.shards.values()) { - if(other.ready) { - return - } - } - this._client.ready = false - this._client.startTime = 0 - this._client.emit("disconnect") - }); + spawn (id) { + let shard = this.shards.get(id) + if (!shard) { + shard = new Shard(id, this._client) + this.shards.set(id, shard) + shard.on('ready', () => { + this._client.emit('shardReady', shard.id) + if (this._client.ready) { + return } - if(shard.status === "disconnected") { - this.connect(shard) + for (const other of this.shards.values()) { + if (!other.ready) { + return + } } + this._client.ready = true + this._client.startTime = Date.now() + this._client.emit('ready') + }).on('resume', () => { + this._client.emit('shardResume', shard.id) + if (this._client.ready) { + return + } + for (const other of this.shards.values()) { + if (!other.ready) { + return + } + } + this._client.ready = true + this._client.startTime = Date.now() + this._client.emit('ready') + }).on('disconnect', (error) => { + this._client.emit('shardDisconnect', error, shard.id) + for (const other of this.shards.values()) { + if (other.ready) { + return + } + } + this._client.ready = false + this._client.startTime = 0 + this._client.emit('disconnect') + }) } - - toString() { - return `[ShardManager ${this.shards.size}]` + if (shard.status === 'disconnected') { + this.connect(shard) } + } - toJSON(props = []) { - return super.toJSON([ - "connectQueue", - "lastConnect", - "connectionTimeout", - ...props - ]) - } + toString () { + return `[ShardManager ${this.shards.size}]` + } + + toJSON (props = []) { + return super.toJSON([ + 'connectQueue', + 'lastConnect', + 'connectionTimeout', + ...props + ]) + } } -module.exports = ShardManager \ No newline at end of file +module.exports = ShardManager diff --git a/src/structures/http/APIController.js b/src/structures/http/APIController.js index 4ffb84e..18eefe0 100644 --- a/src/structures/http/APIController.js +++ b/src/structures/http/APIController.js @@ -2,126 +2,126 @@ const express = require('express') const cors = require('cors') class APIController { - constructor (manager) { - this.manager = manager - this.app = express() - this.app.use(cors()) - } - - start (port) { - return this.createRoutes(this.app).then(() => this.listen(port)) - } - - async createRoutes (app) { - // TODO: Auth - app.get('/search', async (req, res) => { - const { identifier, playFirstResult, guildId, channelId } = req.query - if (!identifier) { - return res.status(400).send({ error: 'Missing "identifier" query parameter.' }) - } - - const songs = await this.manager.getSongs(identifier) - // TODO: Parse songs - if (playFirstResult) { - if (!guildId) { - return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) - } - let player = this.manager.lavalink.players.get(guildId) - if (!player) { - if (!channelId) { - return res.status(400).send({ error: 'Missing "channelId" query parameter.' }) - } - player = await this.manager.lavalink.join({ - guild: guildId, - channel: channelId, - node: '1' // TODO: Choose best node - }, { selfdeaf: true }) // TODO: Default options? - } - - const [ song ] = songs - if (song) { - player.play(song) - return res.send(song) - } - - return res.status(404).send({ error: 'nao achei nada meu patrão' }) - } - - res.send(songs) - }) - - app.get('/playing', async (req, res) => { - const { guildId } = req.query - if (!guildId) { - return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) - } - - const player = this.manager.lavalink.players.get(guildId) - const song = player && player.song - res.send(song || {}) - }) - - app.get('/play', async (req, res) => { - const { guildId, channelId, track } = req.query - if (!guildId) { - return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) - } - if (!track) { - return res.status(400).send({ error: 'Missing "track" query parameter.' }) - } - - let player = this.manager.lavalink.players.get(guildId) - if (!player) { - if (!channelId) { - return res.status(400).send({ error: 'Missing "channelId" query parameter.' }) - } - player = await this.manager.lavalink.join({ - guild: guildId, - channel: channelId, - node: '1' // TODO: Choose best node - }, { selfdeaf: true }) // TODO: Default options? - } - - // const song = - await player.play(song) - - res.send() - }) - - app.get('/skip', async (req, res) => { - const { guildId } = req.query - if (!guildId) { - return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) - } - - let player = this.manager.lavalink.players.get(guildId) - if (!player || !player.playing) { - return res.status(400).send({ error: 'n to tocano porra' }) - } - - const song = player.next() - - res.send({ ok: true, song }) - }) - - app.get('/queue', async (req, res) => { - const { guildId } = req.query - if (!guildId) { - return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) - } - - let player = this.manager.lavalink.players.get(guildId) - if (!player || !player.playing) { - return res.status(400).send({ error: 'n to tocano porra' }) - } - - res.send({ playing: player.song, queue: player.queue }) - }) - } - - listen (port) { - return new Promise(r => this.app.listen(port, r)) - } + constructor (manager) { + this.manager = manager + this.app = express() + this.app.use(cors()) + } + + start (port) { + return this.createRoutes(this.app).then(() => this.listen(port)) + } + + async createRoutes (app) { + // TODO: Auth + app.get('/search', async (req, res) => { + const { identifier, playFirstResult, guildId, channelId } = req.query + if (!identifier) { + return res.status(400).send({ error: 'Missing "identifier" query parameter.' }) + } + + const songs = await this.manager.getSongs(identifier) + // TODO: Parse songs + if (playFirstResult) { + if (!guildId) { + return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) + } + let player = this.manager.lavalink.players.get(guildId) + if (!player) { + if (!channelId) { + return res.status(400).send({ error: 'Missing "channelId" query parameter.' }) + } + player = await this.manager.lavalink.join({ + guild: guildId, + channel: channelId, + node: '1' // TODO: Choose best node + }, { selfdeaf: true }) // TODO: Default options? + } + + const [song] = songs + if (song) { + player.play(song) + return res.send(song) + } + + return res.status(404).send({ error: 'nao achei nada meu patrão' }) + } + + res.send(songs) + }) + + app.get('/playing', async (req, res) => { + const { guildId } = req.query + if (!guildId) { + return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) + } + + const player = this.manager.lavalink.players.get(guildId) + const song = player && player.song + res.send(song || {}) + }) + + app.get('/play', async (req, res) => { + const { guildId, channelId, track } = req.query + if (!guildId) { + return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) + } + if (!track) { + return res.status(400).send({ error: 'Missing "track" query parameter.' }) + } + + let player = this.manager.lavalink.players.get(guildId) + if (!player) { + if (!channelId) { + return res.status(400).send({ error: 'Missing "channelId" query parameter.' }) + } + player = await this.manager.lavalink.join({ + guild: guildId, + channel: channelId, + node: '1' // TODO: Choose best node + }, { selfdeaf: true }) // TODO: Default options? + } + + // const song = + await player.play(song) + + res.send() + }) + + app.get('/skip', async (req, res) => { + const { guildId } = req.query + if (!guildId) { + return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) + } + + const player = this.manager.lavalink.players.get(guildId) + if (!player || !player.playing) { + return res.status(400).send({ error: 'n to tocano porra' }) + } + + const song = player.next() + + res.send({ ok: true, song }) + }) + + app.get('/queue', async (req, res) => { + const { guildId } = req.query + if (!guildId) { + return res.status(400).send({ error: 'Missing "guildId" query parameter.' }) + } + + const player = this.manager.lavalink.players.get(guildId) + if (!player || !player.playing) { + return res.status(400).send({ error: 'n to tocano porra' }) + } + + res.send({ playing: player.song, queue: player.queue }) + }) + } + + listen (port) { + return new Promise(r => this.app.listen(port, r)) + } } module.exports = APIController diff --git a/src/structures/lavacord/Manager.js b/src/structures/lavacord/Manager.js index e0bfcd9..b55d7d9 100644 --- a/src/structures/lavacord/Manager.js +++ b/src/structures/lavacord/Manager.js @@ -1,33 +1,33 @@ const { Manager: LavacordManager } = require('lavacord') module.exports = class Manager extends LavacordManager { - constructor (client, nodes, options) { - super(nodes, options || {}) + constructor (client, nodes, options) { + super(nodes, options || {}) - this.client = client - this.user = client.userId - this.send = packet => { - const id = this.client.guildShardMap[packet.d.guild_id] - const shard = this.client.shards.get(id) - if (shard) return shard.sendWS(packet.op, packet.d) - } - - client - .once('ready', () => { - this.shards = client.shards.size || 1 - }) - .on('rawWS', async (packet) => { - switch (packet.t) { - case 'VOICE_SERVER_UPDATE': - await this.voiceServerUpdate(packet.d) - break - case 'VOICE_STATE_UPDATE': - await this.voiceStateUpdate(packet.d) - break - case 'GUILD_CREATE': - for (const state of packet.d.voice_states) await this.voiceStateUpdate({ ...state, guild_id: packet.d.id }) - break - } - }) + this.client = client + this.user = client.userId + this.send = packet => { + const id = this.client.guildShardMap[packet.d.guild_id] + const shard = this.client.shards.get(id) + if (shard) return shard.sendWS(packet.op, packet.d) } -} \ No newline at end of file + + client + .once('ready', () => { + this.shards = client.shards.size || 1 + }) + .on('rawWS', async (packet) => { + switch (packet.t) { + case 'VOICE_SERVER_UPDATE': + await this.voiceServerUpdate(packet.d) + break + case 'VOICE_STATE_UPDATE': + await this.voiceStateUpdate(packet.d) + break + case 'GUILD_CREATE': + for (const state of packet.d.voice_states) await this.voiceStateUpdate({ ...state, guild_id: packet.d.id }) + break + } + }) + } +} diff --git a/src/structures/lavacord/MusicPlayer.js b/src/structures/lavacord/MusicPlayer.js index 0ebf16f..bca0017 100644 --- a/src/structures/lavacord/MusicPlayer.js +++ b/src/structures/lavacord/MusicPlayer.js @@ -1,62 +1,62 @@ const { Player } = require('lavacord') class MusicPlayer extends Player { - constructor (node, id) { - super (node, id) + constructor (node, id) { + super(node, id) - this.song = null + this.song = null - this.volume = 25 - this.paused = false - this.looping = false + this.volume = 25 + this.paused = false + this.looping = false - this.previousVolume = null - this.bassboost = false + this.previousVolume = null + this.bassboost = false - this.queue = [] + this.queue = [] - this.registerListeners() - } - - play (song, forcePlay = false) { - if (this.playing && !forcePlay) { - this.queueSong(song) - return false - } + this.registerListeners() + } - this.song = song - super.play(song.track, { volume: this.volume }) - return true + play (song, forcePlay = false) { + if (this.playing && !forcePlay) { + this.queueSong(song) + return false } - queueSong (song) { - this.queue.push(song) - } + this.song = song + super.play(song.track, { volume: this.volume }) + return true + } - next () { - if (this.looping) this.queueSong(this.playingSong) + queueSong (song) { + this.queue.push(song) + } - const next = this.queue.shift() - if (next) { - this.play(next, true) - return next - } - super.stop() - } + next () { + if (this.looping) this.queueSong(this.playingSong) - registerListeners () { - this.on('end', ({ reason }) => { - if (reason !== 'STOPPED') { - if (reason === 'REPLACED') return - this.next() - } - }) - - this.on('stop', () => { - this.song = null - this.destroy() - }) + const next = this.queue.shift() + if (next) { + this.play(next, true) + return next } + super.stop() + } + + registerListeners () { + this.on('end', ({ reason }) => { + if (reason !== 'STOPPED') { + if (reason === 'REPLACED') return + this.next() + } + }) + + this.on('stop', () => { + this.song = null + this.destroy() + }) + } } module.exports = MusicPlayer diff --git a/src/structures/lavacord/Song.js b/src/structures/lavacord/Song.js index 0820326..4f79c1a 100644 --- a/src/structures/lavacord/Song.js +++ b/src/structures/lavacord/Song.js @@ -1,50 +1,50 @@ -const b = "QAAAgwIAI01laWFVbSAtIG9mIGNvdXJzZSBpIHN0aWxsIGxvdmUgeW91AAZNZWlhVW0AAAAAAAbd0AALZGpHdTJHNV9NS00AAQAraHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kakd1Mkc1X01LTQAHeW91dHViZQAAAAAAAAAA" +const b = 'QAAAgwIAI01laWFVbSAtIG9mIGNvdXJzZSBpIHN0aWxsIGxvdmUgeW91AAZNZWlhVW0AAAAAAAbd0AALZGpHdTJHNV9NS00AAQAraHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kakd1Mkc1X01LTQAHeW91dHViZQAAAAAAAAAA' class Song { - constructor (track, info) { - this.track = track - this.info = info - } - - get title () { - return this.info.title - } - - get author () { - return this.info.author - } - - get length () { - return this.info.length - } - - get identifier () { - return this.info.identifier - } - - get stream () { - return this.info.isStream - } - - get uri () { - return this.info.uri - } - - // Static - static from (track) { - return new this(track, this.decodeTrack(track)) - } - - static decodeTrack (track) { - const info = {} - - const buf = Buffer.from(b, 'base64') - console.log(buf[0] & 0xFF) - console.log(buf.indexOf('MeiaUm')) - // title = 7 - - // TODO: Decode track - return info - } + constructor (track, info) { + this.track = track + this.info = info + } + + get title () { + return this.info.title + } + + get author () { + return this.info.author + } + + get length () { + return this.info.length + } + + get identifier () { + return this.info.identifier + } + + get stream () { + return this.info.isStream + } + + get uri () { + return this.info.uri + } + + // Static + static from (track) { + return new this(track, this.decodeTrack(track)) + } + + static decodeTrack (track) { + const info = {} + + const buf = Buffer.from(b, 'base64') + console.log(buf[0] & 0xFF) + console.log(buf.indexOf('MeiaUm')) + // title = 7 + + // TODO: Decode track + return info + } } Song.decodeTrack(b) diff --git a/src/utils/Constants.js b/src/utils/Constants.js index c4966b1..b56e298 100644 --- a/src/utils/Constants.js +++ b/src/utils/Constants.js @@ -1,38 +1,38 @@ module.exports.GatewayOPCodes = { - EVENT: 0, - HEARTBEAT: 1, - IDENTIFY: 2, - STATUS_UPDATE: 3, - VOICE_STATE_UPDATE: 4, - VOICE_SERVER_PING: 5, - RESUME: 6, - RECONNECT: 7, - GET_GUILD_MEMBERS: 8, - INVALID_SESSION: 9, - HELLO: 10, - HEARTBEAT_ACK: 11, - SYNC_GUILD: 12, - SYNC_CALL: 13 + EVENT: 0, + HEARTBEAT: 1, + IDENTIFY: 2, + STATUS_UPDATE: 3, + VOICE_STATE_UPDATE: 4, + VOICE_SERVER_PING: 5, + RESUME: 6, + RECONNECT: 7, + GET_GUILD_MEMBERS: 8, + INVALID_SESSION: 9, + HELLO: 10, + HEARTBEAT_ACK: 11, + SYNC_GUILD: 12, + SYNC_CALL: 13 } module.exports.GATEWAY_VERSION = 6 module.exports.Intents = { - guilds: 1 << 0, - guildMembers: 1 << 1, - guildBans: 1 << 2, - guildEmojis: 1 << 3, - guildIntegrations: 1 << 4, - guildWebhooks: 1 << 5, - guildInvites: 1 << 6, - guildVoiceStates: 1 << 7, - guildPresences: 1 << 8, - guildMessages: 1 << 9, - guildMessageReactions: 1 << 10, - guildMessageTyping: 1 << 11, - directMessages: 1 << 12, - directMessageReactions: 1 << 13, - directMessageTyping: 1 << 14 + guilds: 1 << 0, + guildMembers: 1 << 1, + guildBans: 1 << 2, + guildEmojis: 1 << 3, + guildIntegrations: 1 << 4, + guildWebhooks: 1 << 5, + guildInvites: 1 << 6, + guildVoiceStates: 1 << 7, + guildPresences: 1 << 8, + guildMessages: 1 << 9, + guildMessageReactions: 1 << 10, + guildMessageTyping: 1 << 11, + directMessages: 1 << 12, + directMessageReactions: 1 << 13, + directMessageTyping: 1 << 14 } const REST_VERSION = 7 @@ -40,6 +40,6 @@ module.exports.REST_VERSION = REST_VERSION module.exports.REST_BASE_URL = '/api/v' + REST_VERSION module.exports.Endpoints = { - GATEWAY: '/gateway', - GATEWAY_BOT: '/gateway/bot' + GATEWAY: '/gateway', + GATEWAY_BOT: '/gateway/bot' }