aqualink 2.15.1 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/structures/Aqua.js +0 -9
- package/build/structures/Player.js +27 -9
- package/build/structures/Rest.js +223 -114
- package/package.json +1 -1
package/build/structures/Aqua.js
CHANGED
|
@@ -29,7 +29,6 @@ const RECONNECT_DELAY = 400
|
|
|
29
29
|
const CACHE_VALID_TIME = 12000
|
|
30
30
|
const NODE_TIMEOUT = 30000
|
|
31
31
|
const MAX_CACHE_SIZE = 20
|
|
32
|
-
const MAX_BROKEN_PLAYERS = 50
|
|
33
32
|
const MAX_FAILOVER_QUEUE = 50
|
|
34
33
|
const MAX_REBUILD_LOCKS = 100
|
|
35
34
|
const WRITE_BUFFER_SIZE = 100
|
|
@@ -279,7 +278,6 @@ class Aqua extends EventEmitter {
|
|
|
279
278
|
this._brokenPlayers.set(player.guildId, state)
|
|
280
279
|
}
|
|
281
280
|
}
|
|
282
|
-
this._trimBrokenPlayers()
|
|
283
281
|
}
|
|
284
282
|
|
|
285
283
|
async _rebuildBrokenPlayers(node) {
|
|
@@ -786,7 +784,6 @@ class Aqua extends EventEmitter {
|
|
|
786
784
|
}
|
|
787
785
|
}
|
|
788
786
|
|
|
789
|
-
this._trimBrokenPlayers()
|
|
790
787
|
if (this._failoverQueue.size > MAX_FAILOVER_QUEUE) this._failoverQueue.clear()
|
|
791
788
|
if (this._rebuildLocks.size > MAX_REBUILD_LOCKS) this._rebuildLocks.clear()
|
|
792
789
|
|
|
@@ -795,12 +792,6 @@ class Aqua extends EventEmitter {
|
|
|
795
792
|
if (!this.nodeMap.has(id)) this._nodeStates.delete(id)
|
|
796
793
|
}
|
|
797
794
|
}
|
|
798
|
-
|
|
799
|
-
_trimBrokenPlayers() {
|
|
800
|
-
if (this._brokenPlayers.size <= MAX_BROKEN_PLAYERS) return
|
|
801
|
-
const sorted = [...this._brokenPlayers.entries()].sort((a, b) => a[1].brokenAt - b[1].brokenAt)
|
|
802
|
-
sorted.slice(0, sorted.length - MAX_BROKEN_PLAYERS).forEach(([id]) => this._brokenPlayers.delete(id))
|
|
803
|
-
}
|
|
804
795
|
}
|
|
805
796
|
|
|
806
797
|
module.exports = Aqua
|
|
@@ -18,6 +18,9 @@ const EVENT_HANDLERS = Object.freeze({
|
|
|
18
18
|
WebSocketClosedEvent: 'socketClosed',
|
|
19
19
|
LyricsLineEvent: 'lyricsLine',
|
|
20
20
|
LyricsFoundEvent: 'lyricsFound',
|
|
21
|
+
VolumeChangedEvent: 'volumeChanged',
|
|
22
|
+
FiltersChangedEvent: 'filtersChanged',
|
|
23
|
+
SeekEvent: 'seekEvent',
|
|
21
24
|
LyricsNotFoundEvent: 'lyricsNotFound'
|
|
22
25
|
})
|
|
23
26
|
|
|
@@ -301,7 +304,7 @@ class Player extends EventEmitter {
|
|
|
301
304
|
this.playing = true
|
|
302
305
|
this.paused = false
|
|
303
306
|
this.position = 0
|
|
304
|
-
await this.batchUpdatePlayer({guildId: this.guildId,
|
|
307
|
+
await this.batchUpdatePlayer({guildId: this.guildId, track: { encoded: this.current.track} }, true)
|
|
305
308
|
return this
|
|
306
309
|
} catch (error) {
|
|
307
310
|
this.aqua.emit(AqualinkEvents.Error, error)
|
|
@@ -430,8 +433,8 @@ class Player extends EventEmitter {
|
|
|
430
433
|
this._dataStore.clear()
|
|
431
434
|
this._dataStore = null
|
|
432
435
|
}
|
|
433
|
-
|
|
434
|
-
if (this.current?.dispose) this.current.dispose()
|
|
436
|
+
// if autoResume is enabled, ig it will need to be keeped in case the connection dies somehow, so don't dispose it
|
|
437
|
+
if (this.current?.dispose && !this.aqua.options.autoResume) this.current.dispose()
|
|
435
438
|
this.connection = this.filters = this.current = this.autoplaySeed = null
|
|
436
439
|
|
|
437
440
|
if (!skipRemote) {
|
|
@@ -449,7 +452,7 @@ class Player extends EventEmitter {
|
|
|
449
452
|
pause(paused) {
|
|
450
453
|
if (this.destroyed || this.paused === !!paused) return this
|
|
451
454
|
this.paused = !!paused
|
|
452
|
-
this.batchUpdatePlayer({guildId: this.guildId, paused: this.paused}, true)
|
|
455
|
+
this.batchUpdatePlayer({guildId: this.guildId, paused: this.paused}, true).catch(() => {})
|
|
453
456
|
return this
|
|
454
457
|
}
|
|
455
458
|
|
|
@@ -459,7 +462,7 @@ class Player extends EventEmitter {
|
|
|
459
462
|
const pos = position === 0 ? 0 : this.position + position
|
|
460
463
|
const clamped = len ? Math.min(Math.max(pos, 0), len) : Math.max(pos, 0)
|
|
461
464
|
this.position = clamped
|
|
462
|
-
this.batchUpdatePlayer({guildId: this.guildId, position: clamped}, true)
|
|
465
|
+
this.batchUpdatePlayer({guildId: this.guildId, position: clamped}, true).catch(() => {})
|
|
463
466
|
return this
|
|
464
467
|
}
|
|
465
468
|
|
|
@@ -467,7 +470,7 @@ class Player extends EventEmitter {
|
|
|
467
470
|
if (this.destroyed || !this.playing) return this
|
|
468
471
|
this.playing = this.paused = false
|
|
469
472
|
this.position = 0
|
|
470
|
-
this.batchUpdatePlayer({guildId: this.guildId,
|
|
473
|
+
this.batchUpdatePlayer({guildId: this.guildId, track: {encoded: null}}, true).catch(() => {})
|
|
471
474
|
return this
|
|
472
475
|
}
|
|
473
476
|
|
|
@@ -475,7 +478,7 @@ class Player extends EventEmitter {
|
|
|
475
478
|
const vol = _functions.clamp(volume)
|
|
476
479
|
if (this.destroyed || this.volume === vol) return this
|
|
477
480
|
this.volume = vol
|
|
478
|
-
this.batchUpdatePlayer({guildId: this.guildId, volume: vol})
|
|
481
|
+
this.batchUpdatePlayer({guildId: this.guildId, volume: vol}).catch(() => {})
|
|
479
482
|
return this
|
|
480
483
|
}
|
|
481
484
|
|
|
@@ -492,7 +495,7 @@ class Player extends EventEmitter {
|
|
|
492
495
|
const id = _functions.toId(channel)
|
|
493
496
|
if (!id) throw new TypeError('Invalid text channel')
|
|
494
497
|
this.textChannel = id
|
|
495
|
-
this.batchUpdatePlayer({guildId: this.guildId, text_channel: id})
|
|
498
|
+
this.batchUpdatePlayer({guildId: this.guildId, text_channel: id}).catch(() => {})
|
|
496
499
|
return this
|
|
497
500
|
}
|
|
498
501
|
|
|
@@ -821,6 +824,21 @@ class Player extends EventEmitter {
|
|
|
821
824
|
this.aqua.emit(AqualinkEvents.LyricsLine, this, track, payload)
|
|
822
825
|
}
|
|
823
826
|
|
|
827
|
+
async volumeChanged(player, track, payload) {
|
|
828
|
+
if (this.destroyed) return
|
|
829
|
+
this.aqua.emit(AqualinkEvents.VolumeChanged, this, track, payload)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async filtersChanged(player, track, payload) {
|
|
833
|
+
if (this.destroyed) return
|
|
834
|
+
this.aqua.emit(AqualinkEvents.FiltersChanged, this, track, payload)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async seekEvent(player, track, payload) {
|
|
838
|
+
if (this.destroyed) return
|
|
839
|
+
this.aqua.emit(AqualinkEvents.Seek, this, track, payload)
|
|
840
|
+
}
|
|
841
|
+
|
|
824
842
|
async lyricsFound(player, track, payload) {
|
|
825
843
|
if (this.destroyed) return
|
|
826
844
|
this.aqua.emit(AqualinkEvents.LyricsFound, this, track, payload)
|
|
@@ -876,4 +894,4 @@ class Player extends EventEmitter {
|
|
|
876
894
|
}
|
|
877
895
|
}
|
|
878
896
|
|
|
879
|
-
module.exports = Player
|
|
897
|
+
module.exports = Player
|
package/build/structures/Rest.js
CHANGED
|
@@ -1,33 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
if (typeof str !== 'string' || !str) return false
|
|
18
|
-
const len = str.length
|
|
19
|
-
if (len % 4 === 1) return false
|
|
20
|
-
for (let i = 0; i < len; i++) {
|
|
21
|
-
if (!isValidBase64Char(str.charCodeAt(i))) return false
|
|
22
|
-
}
|
|
23
|
-
return true
|
|
24
|
-
}
|
|
1
|
+
const { Buffer } = require('buffer')
|
|
2
|
+
const { Agent: HttpsAgent, request: httpsRequest } = require('https')
|
|
3
|
+
const { Agent: HttpAgent, request: httpRequest } = require('http')
|
|
4
|
+
const http2 = require('http2')
|
|
5
|
+
const { createBrotliDecompress, createUnzip, brotliDecompressSync, unzipSync } = require('zlib')
|
|
6
|
+
|
|
7
|
+
const BASE64_LOOKUP = new Uint8Array(256)
|
|
8
|
+
for (let i = 65; i <= 90; i++) BASE64_LOOKUP[i] = 1
|
|
9
|
+
for (let i = 97; i <= 122; i++) BASE64_LOOKUP[i] = 1
|
|
10
|
+
for (let i = 48; i <= 57; i++) BASE64_LOOKUP[i] = 1
|
|
11
|
+
BASE64_LOOKUP[43] = BASE64_LOOKUP[47] = BASE64_LOOKUP[61] = BASE64_LOOKUP[95] = BASE64_LOOKUP[45] = 1
|
|
12
|
+
|
|
13
|
+
const ENCODING_BR = 1
|
|
14
|
+
const ENCODING_GZIP = 2
|
|
15
|
+
const ENCODING_DEFLATE = 3
|
|
16
|
+
const ENCODING_NONE = 0
|
|
25
17
|
|
|
26
18
|
const MAX_RESPONSE_SIZE = 10485760
|
|
19
|
+
const SMALL_RESPONSE_THRESHOLD = 512
|
|
20
|
+
const COMPRESSION_MIN_SIZE = 1024
|
|
27
21
|
const API_VERSION = 'v4'
|
|
28
22
|
const UTF8_ENCODING = 'utf8'
|
|
29
23
|
const JSON_CONTENT_TYPE = 'application/json'
|
|
30
24
|
const HTTP2_THRESHOLD = 1024
|
|
25
|
+
const MAX_HEADER_POOL = 10
|
|
31
26
|
|
|
32
27
|
const ERRORS = Object.freeze({
|
|
33
28
|
NO_SESSION: new Error('Session ID required'),
|
|
@@ -37,6 +32,25 @@ const ERRORS = Object.freeze({
|
|
|
37
32
|
RESPONSE_ABORTED: new Error('Response aborted')
|
|
38
33
|
})
|
|
39
34
|
|
|
35
|
+
const _isValidBase64 = (str) => {
|
|
36
|
+
if (typeof str !== 'string' || !str) return false
|
|
37
|
+
const len = str.length
|
|
38
|
+
if (len % 4 === 1) return false
|
|
39
|
+
for (let i = 0; i < len; i++) {
|
|
40
|
+
if (!BASE64_LOOKUP[str.charCodeAt(i)]) return false
|
|
41
|
+
}
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const _getEncodingType = (encodingHeader) => {
|
|
46
|
+
if (!encodingHeader) return ENCODING_NONE
|
|
47
|
+
const firstChar = encodingHeader.charCodeAt(0)
|
|
48
|
+
if (firstChar === 98 && encodingHeader.startsWith('br')) return ENCODING_BR
|
|
49
|
+
if (firstChar === 103 && encodingHeader.startsWith('gzip')) return ENCODING_GZIP
|
|
50
|
+
if (firstChar === 100 && encodingHeader.startsWith('deflate')) return ENCODING_DEFLATE
|
|
51
|
+
return ENCODING_NONE
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
class Rest {
|
|
41
55
|
constructor(aqua, node) {
|
|
42
56
|
this.aqua = aqua
|
|
@@ -48,7 +62,6 @@ class Rest {
|
|
|
48
62
|
const host = node.host.includes(':') && !node.host.startsWith('[') ? `[${node.host}]` : node.host
|
|
49
63
|
this.baseUrl = `${protocol}//${host}:${node.port}`
|
|
50
64
|
this._apiBase = `/${API_VERSION}`
|
|
51
|
-
this._sessionPath = this.sessionId ? `${this._apiBase}/sessions/${this.sessionId}` : null
|
|
52
65
|
|
|
53
66
|
this._endpoints = Object.freeze({
|
|
54
67
|
loadtracks: `${this._apiBase}/loadtracks?identifier=`,
|
|
@@ -68,7 +81,7 @@ class Rest {
|
|
|
68
81
|
this.defaultHeaders = Object.freeze({
|
|
69
82
|
Authorization: String(node.auth || node.password || ''),
|
|
70
83
|
Accept: 'application/json, */*;q=0.5',
|
|
71
|
-
'Accept-Encoding': 'gzip, deflate
|
|
84
|
+
'Accept-Encoding': 'br, gzip, deflate',
|
|
72
85
|
'User-Agent': `Aqualink/${aqua?.version || '1.0'} (Node.js ${process.version})`
|
|
73
86
|
})
|
|
74
87
|
|
|
@@ -77,10 +90,6 @@ class Rest {
|
|
|
77
90
|
this.useHttp2 = !!(aqua?.options?.useHttp2)
|
|
78
91
|
this._h2 = null
|
|
79
92
|
this._h2CleanupTimer = null
|
|
80
|
-
|
|
81
|
-
privateData.set(this, {
|
|
82
|
-
cleanupCallbacks: new Set()
|
|
83
|
-
})
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
_setupAgent(node) {
|
|
@@ -105,26 +114,31 @@ class Rest {
|
|
|
105
114
|
|
|
106
115
|
this.agent = new (node.ssl ? HttpsAgent : HttpAgent)(agentOpts)
|
|
107
116
|
this.request = node.ssl ? httpsRequest : httpRequest
|
|
117
|
+
|
|
118
|
+
const origCreateConnection = this.agent.createConnection
|
|
119
|
+
this.agent.createConnection = (options, callback) => {
|
|
120
|
+
const socket = origCreateConnection.call(this.agent, options, callback)
|
|
121
|
+
socket.setNoDelay(true)
|
|
122
|
+
socket.setKeepAlive(true, 500)
|
|
123
|
+
socket.unref?.()
|
|
124
|
+
return socket
|
|
125
|
+
}
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
setSessionId(sessionId) {
|
|
111
129
|
this.sessionId = sessionId
|
|
112
|
-
this._sessionPath = sessionId ? `${this._apiBase}/sessions/${sessionId}` : null
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
_getSessionPath() {
|
|
116
|
-
if (!this.
|
|
117
|
-
|
|
118
|
-
this._sessionPath = `${this._apiBase}/sessions/${this.sessionId}`
|
|
119
|
-
}
|
|
120
|
-
return this._sessionPath
|
|
133
|
+
if (!this.sessionId) throw ERRORS.NO_SESSION
|
|
134
|
+
return `${this._apiBase}/sessions/${this.sessionId}`
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
_buildHeaders(hasPayload, payloadLength) {
|
|
124
138
|
if (!hasPayload) return this.defaultHeaders
|
|
125
139
|
|
|
126
140
|
let headers = this._headerPool.pop()
|
|
127
|
-
if (!headers) headers =
|
|
141
|
+
if (!headers) headers = Object.create(null)
|
|
128
142
|
|
|
129
143
|
headers.Authorization = this.defaultHeaders.Authorization
|
|
130
144
|
headers.Accept = this.defaultHeaders.Accept
|
|
@@ -137,11 +151,13 @@ class Rest {
|
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
_returnHeadersToPool(headers) {
|
|
140
|
-
if (headers !== this.defaultHeaders && this._headerPool.length <
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
if (headers !== this.defaultHeaders && this._headerPool.length < MAX_HEADER_POOL) {
|
|
155
|
+
headers.Authorization = null
|
|
156
|
+
headers.Accept = null
|
|
157
|
+
headers['Accept-Encoding'] = null
|
|
158
|
+
headers['User-Agent'] = null
|
|
159
|
+
headers['Content-Type'] = null
|
|
160
|
+
headers['Content-Length'] = null
|
|
145
161
|
this._headerPool.push(headers)
|
|
146
162
|
}
|
|
147
163
|
}
|
|
@@ -151,7 +167,6 @@ class Rest {
|
|
|
151
167
|
const payload = body === undefined ? undefined : (typeof body === 'string' ? body : JSON.stringify(body))
|
|
152
168
|
const payloadLength = payload ? Buffer.byteLength(payload, UTF8_ENCODING) : 0
|
|
153
169
|
const headers = this._buildHeaders(!!payload, payloadLength)
|
|
154
|
-
|
|
155
170
|
const useHttp2 = this.useHttp2 && payloadLength >= HTTP2_THRESHOLD
|
|
156
171
|
|
|
157
172
|
try {
|
|
@@ -165,29 +180,31 @@ class Rest {
|
|
|
165
180
|
|
|
166
181
|
_makeHttp1Request(method, url, headers, payload) {
|
|
167
182
|
return new Promise((resolve, reject) => {
|
|
168
|
-
let req
|
|
169
|
-
let timeoutId
|
|
183
|
+
let req = null
|
|
184
|
+
let timeoutId = null
|
|
170
185
|
let resolved = false
|
|
171
|
-
|
|
186
|
+
let chunks = []
|
|
172
187
|
let totalSize = 0
|
|
188
|
+
let preallocatedBuffer = null
|
|
173
189
|
|
|
174
190
|
const cleanup = () => {
|
|
175
191
|
if (timeoutId) {
|
|
176
192
|
clearTimeout(timeoutId)
|
|
177
193
|
timeoutId = null
|
|
178
194
|
}
|
|
179
|
-
chunks
|
|
195
|
+
chunks = null
|
|
196
|
+
preallocatedBuffer = null
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
const complete = (isSuccess, value) => {
|
|
183
|
-
if (resolved) return
|
|
200
|
+
if (resolved) return;
|
|
184
201
|
resolved = true
|
|
185
202
|
cleanup()
|
|
186
203
|
if (req && !isSuccess) req.destroy()
|
|
187
204
|
isSuccess ? resolve(value) : reject(value)
|
|
188
205
|
}
|
|
189
206
|
|
|
190
|
-
req = this.request(url, { method, headers, agent: this.agent, timeout: this.timeout }, res => {
|
|
207
|
+
req = this.request(url, { method, headers, agent: this.agent, timeout: this.timeout }, (res) => {
|
|
191
208
|
cleanup()
|
|
192
209
|
|
|
193
210
|
const status = res.statusCode
|
|
@@ -195,38 +212,123 @@ class Rest {
|
|
|
195
212
|
|
|
196
213
|
const contentLength = res.headers['content-length']
|
|
197
214
|
if (contentLength === '0') return res.resume(), complete(true, null)
|
|
198
|
-
|
|
215
|
+
|
|
216
|
+
const clInt = contentLength ? parseInt(contentLength, 10) : 0
|
|
217
|
+
if (clInt > MAX_RESPONSE_SIZE) {
|
|
218
|
+
res.resume()
|
|
199
219
|
return complete(false, ERRORS.RESPONSE_TOO_LARGE)
|
|
200
220
|
}
|
|
201
221
|
|
|
202
|
-
const
|
|
222
|
+
const encodingType = _getEncodingType(res.headers['content-encoding'])
|
|
223
|
+
|
|
224
|
+
if (clInt > 0 && clInt < SMALL_RESPONSE_THRESHOLD && encodingType === ENCODING_NONE) {
|
|
225
|
+
res.once('data', (chunk) => {
|
|
226
|
+
const text = chunk.toString(UTF8_ENCODING)
|
|
227
|
+
let result = text
|
|
228
|
+
const contentType = res.headers['content-type'] || ''
|
|
229
|
+
|
|
230
|
+
if (contentType.charCodeAt(0) === 97 && contentType.includes('application/json')) {
|
|
231
|
+
try {
|
|
232
|
+
result = JSON.parse(text)
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return complete(false, new Error(`JSON parse error: ${err.message}`))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (status >= 400) {
|
|
239
|
+
const error = new Error(`HTTP ${status} ${method} ${url}`)
|
|
240
|
+
error.statusCode = status
|
|
241
|
+
error.statusMessage = res.statusMessage
|
|
242
|
+
error.headers = res.headers
|
|
243
|
+
error.body = result
|
|
244
|
+
error.url = url
|
|
245
|
+
return complete(false, error)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
complete(true, result)
|
|
249
|
+
})
|
|
250
|
+
res.once('error', (err) => complete(false, err))
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
|
|
255
|
+
preallocatedBuffer = Buffer.allocUnsafe(clInt)
|
|
256
|
+
chunks = []
|
|
257
|
+
} else {
|
|
258
|
+
chunks = []
|
|
259
|
+
}
|
|
260
|
+
|
|
203
261
|
let stream = res
|
|
204
262
|
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
263
|
+
if (encodingType !== ENCODING_NONE) {
|
|
264
|
+
if (clInt > 0 && clInt < COMPRESSION_MIN_SIZE) {
|
|
265
|
+
const compressedChunks = []
|
|
266
|
+
res.on('data', (chunk) => compressedChunks.push(chunk))
|
|
267
|
+
res.once('end', () => {
|
|
268
|
+
try {
|
|
269
|
+
const compressed = Buffer.concat(compressedChunks)
|
|
270
|
+
const decompressed = encodingType === ENCODING_BR
|
|
271
|
+
? brotliDecompressSync(compressed)
|
|
272
|
+
: unzipSync(compressed)
|
|
273
|
+
const text = decompressed.toString(UTF8_ENCODING)
|
|
274
|
+
let result = text
|
|
275
|
+
const contentType = res.headers['content-type'] || ''
|
|
276
|
+
|
|
277
|
+
if (contentType.charCodeAt(0) === 97 && contentType.includes('application/json')) {
|
|
278
|
+
result = JSON.parse(text)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (status >= 400) {
|
|
282
|
+
const error = new Error(`HTTP ${status} ${method} ${url}`)
|
|
283
|
+
error.statusCode = status
|
|
284
|
+
error.statusMessage = res.statusMessage
|
|
285
|
+
error.headers = res.headers
|
|
286
|
+
error.body = result
|
|
287
|
+
error.url = url
|
|
288
|
+
return complete(false, error)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
complete(true, result)
|
|
292
|
+
} catch (err) {
|
|
293
|
+
complete(false, err)
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
res.once('error', (err) => complete(false, err))
|
|
297
|
+
return;
|
|
298
|
+
} else {
|
|
299
|
+
const decompressor = encodingType === ENCODING_BR ? createBrotliDecompress() : createUnzip()
|
|
300
|
+
decompressor.once('error', (err) => complete(false, err))
|
|
301
|
+
res.pipe(decompressor)
|
|
302
|
+
stream = decompressor
|
|
303
|
+
}
|
|
210
304
|
}
|
|
211
305
|
|
|
212
306
|
res.once('aborted', () => complete(false, ERRORS.RESPONSE_ABORTED))
|
|
213
|
-
res.once('error', err => complete(false, err))
|
|
214
|
-
|
|
215
|
-
stream.on('data', chunk => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
307
|
+
res.once('error', (err) => complete(false, err))
|
|
308
|
+
|
|
309
|
+
stream.on('data', (chunk) => {
|
|
310
|
+
if (preallocatedBuffer) {
|
|
311
|
+
chunk.copy(preallocatedBuffer, totalSize)
|
|
312
|
+
totalSize += chunk.length
|
|
313
|
+
} else {
|
|
314
|
+
totalSize += chunk.length
|
|
315
|
+
if (totalSize > MAX_RESPONSE_SIZE) return complete(false, ERRORS.RESPONSE_TOO_LARGE)
|
|
316
|
+
chunks.push(chunk)
|
|
317
|
+
}
|
|
219
318
|
})
|
|
220
319
|
|
|
221
320
|
stream.once('end', () => {
|
|
222
321
|
if (totalSize === 0) return complete(true, null)
|
|
223
322
|
|
|
224
|
-
const buffer =
|
|
323
|
+
const buffer = preallocatedBuffer
|
|
324
|
+
? preallocatedBuffer.slice(0, totalSize)
|
|
325
|
+
: (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalSize))
|
|
225
326
|
const text = buffer.toString(UTF8_ENCODING)
|
|
226
|
-
|
|
227
327
|
let result = text
|
|
228
328
|
const contentType = res.headers['content-type'] || ''
|
|
229
|
-
|
|
329
|
+
|
|
330
|
+
if (contentType.charCodeAt(0) === 97 && contentType.includes('application/json')) {
|
|
331
|
+
try {
|
|
230
332
|
result = JSON.parse(text)
|
|
231
333
|
} catch (err) {
|
|
232
334
|
return complete(false, new Error(`JSON parse error: ${err.message}`))
|
|
@@ -247,41 +349,43 @@ class Rest {
|
|
|
247
349
|
})
|
|
248
350
|
})
|
|
249
351
|
|
|
250
|
-
req.once('error', err => complete(false, err))
|
|
251
|
-
req.once('socket', socket => {
|
|
252
|
-
socket.setNoDelay(true)
|
|
253
|
-
socket.setKeepAlive(true, 500)
|
|
254
|
-
socket.unref?.()
|
|
255
|
-
})
|
|
256
|
-
|
|
352
|
+
req.once('error', (err) => complete(false, err))
|
|
257
353
|
timeoutId = setTimeout(() => complete(false, new Error(`Request timeout: ${this.timeout}ms`)), this.timeout)
|
|
258
354
|
payload ? req.end(payload) : req.end()
|
|
259
355
|
})
|
|
260
356
|
}
|
|
261
357
|
|
|
262
|
-
|
|
358
|
+
_getOrCreateHttp2Session() {
|
|
263
359
|
if (!this._h2 || this._h2.closed || this._h2.destroyed) {
|
|
264
|
-
this.
|
|
360
|
+
if (this._h2) this._h2 = null
|
|
265
361
|
|
|
266
362
|
this._h2 = http2.connect(this.baseUrl)
|
|
267
363
|
|
|
268
364
|
if (this._h2CleanupTimer) clearTimeout(this._h2CleanupTimer)
|
|
269
365
|
this._h2CleanupTimer = setTimeout(() => this._closeHttp2Session(), 60000)
|
|
366
|
+
this._h2CleanupTimer.unref()
|
|
270
367
|
|
|
271
|
-
this._h2.once('error', () => this.
|
|
272
|
-
this._h2.once('close', () => this.
|
|
368
|
+
this._h2.once('error', () => { this._h2 = null })
|
|
369
|
+
this._h2.once('close', () => { this._h2 = null })
|
|
273
370
|
this._h2.socket?.unref?.()
|
|
274
371
|
}
|
|
275
372
|
|
|
373
|
+
return this._h2
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async _makeHttp2Request(method, path, headers, payload) {
|
|
377
|
+
const session = this._getOrCreateHttp2Session()
|
|
378
|
+
|
|
276
379
|
return new Promise((resolve, reject) => {
|
|
277
|
-
let req
|
|
278
|
-
let timeoutId
|
|
380
|
+
let req = null
|
|
381
|
+
let timeoutId = null
|
|
279
382
|
let resolved = false
|
|
280
|
-
|
|
383
|
+
let chunks = []
|
|
281
384
|
let totalSize = 0
|
|
385
|
+
let preallocatedBuffer = null
|
|
282
386
|
|
|
283
387
|
const complete = (isSuccess, value) => {
|
|
284
|
-
if (resolved) return
|
|
388
|
+
if (resolved) return;
|
|
285
389
|
resolved = true
|
|
286
390
|
|
|
287
391
|
if (timeoutId) {
|
|
@@ -289,12 +393,13 @@ class Rest {
|
|
|
289
393
|
timeoutId = null
|
|
290
394
|
}
|
|
291
395
|
|
|
292
|
-
chunks
|
|
396
|
+
chunks = null
|
|
397
|
+
preallocatedBuffer = null
|
|
293
398
|
if (req && !isSuccess) req.close(http2.constants.NGHTTP2_CANCEL)
|
|
294
399
|
isSuccess ? resolve(value) : reject(value)
|
|
295
400
|
}
|
|
296
401
|
|
|
297
|
-
const
|
|
402
|
+
const h2Headers = {
|
|
298
403
|
':method': method,
|
|
299
404
|
':path': path,
|
|
300
405
|
Authorization: headers.Authorization,
|
|
@@ -303,12 +408,12 @@ class Rest {
|
|
|
303
408
|
'User-Agent': headers['User-Agent']
|
|
304
409
|
}
|
|
305
410
|
|
|
306
|
-
if (headers['Content-Type'])
|
|
307
|
-
if (headers['Content-Length'])
|
|
411
|
+
if (headers['Content-Type']) h2Headers['Content-Type'] = headers['Content-Type']
|
|
412
|
+
if (headers['Content-Length']) h2Headers['Content-Length'] = headers['Content-Length']
|
|
308
413
|
|
|
309
|
-
req =
|
|
414
|
+
req = session.request(h2Headers)
|
|
310
415
|
|
|
311
|
-
req.once('response', respHeaders => {
|
|
416
|
+
req.once('response', (respHeaders) => {
|
|
312
417
|
if (timeoutId) {
|
|
313
418
|
clearTimeout(timeoutId)
|
|
314
419
|
timeoutId = null
|
|
@@ -316,33 +421,48 @@ class Rest {
|
|
|
316
421
|
|
|
317
422
|
const status = respHeaders[':status'] || 0
|
|
318
423
|
const cl = respHeaders['content-length']
|
|
424
|
+
|
|
319
425
|
if (status === 204 || cl === '0') return req.resume(), complete(true, null)
|
|
320
|
-
|
|
321
|
-
|
|
426
|
+
|
|
427
|
+
const clInt = cl ? parseInt(cl, 10) : 0
|
|
428
|
+
if (clInt > MAX_RESPONSE_SIZE) {
|
|
429
|
+
req.resume()
|
|
430
|
+
return complete(false, ERRORS.RESPONSE_TOO_LARGE)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
|
|
434
|
+
preallocatedBuffer = Buffer.allocUnsafe(clInt)
|
|
322
435
|
}
|
|
323
436
|
|
|
324
|
-
const
|
|
325
|
-
const decompressor =
|
|
326
|
-
? (
|
|
437
|
+
const encodingType = _getEncodingType(respHeaders['content-encoding'])
|
|
438
|
+
const decompressor = encodingType !== ENCODING_NONE
|
|
439
|
+
? (encodingType === ENCODING_BR ? createBrotliDecompress() : createUnzip())
|
|
327
440
|
: null
|
|
328
441
|
const stream = decompressor ? req.pipe(decompressor) : req
|
|
329
442
|
|
|
330
|
-
if (decompressor) decompressor.once('error', err => complete(false, err))
|
|
331
|
-
req.once('error', err => complete(false, err))
|
|
332
|
-
|
|
333
|
-
stream.on('data', chunk => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
443
|
+
if (decompressor) decompressor.once('error', (err) => complete(false, err))
|
|
444
|
+
req.once('error', (err) => complete(false, err))
|
|
445
|
+
|
|
446
|
+
stream.on('data', (chunk) => {
|
|
447
|
+
if (preallocatedBuffer) {
|
|
448
|
+
chunk.copy(preallocatedBuffer, totalSize)
|
|
449
|
+
totalSize += chunk.length
|
|
450
|
+
} else {
|
|
451
|
+
totalSize += chunk.length
|
|
452
|
+
if (totalSize > MAX_RESPONSE_SIZE) return complete(false, ERRORS.RESPONSE_TOO_LARGE)
|
|
453
|
+
chunks.push(chunk)
|
|
454
|
+
}
|
|
337
455
|
})
|
|
338
456
|
|
|
339
457
|
stream.once('end', () => {
|
|
340
458
|
if (totalSize === 0) return complete(true, null)
|
|
341
459
|
|
|
342
|
-
const buffer =
|
|
460
|
+
const buffer = preallocatedBuffer
|
|
461
|
+
? preallocatedBuffer.slice(0, totalSize)
|
|
462
|
+
: (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalSize))
|
|
343
463
|
const text = buffer.toString(UTF8_ENCODING)
|
|
464
|
+
let result = null
|
|
344
465
|
|
|
345
|
-
let result
|
|
346
466
|
try {
|
|
347
467
|
result = JSON.parse(text)
|
|
348
468
|
} catch (err) {
|
|
@@ -403,14 +523,14 @@ class Rest {
|
|
|
403
523
|
}
|
|
404
524
|
|
|
405
525
|
async decodeTrack(encodedTrack) {
|
|
406
|
-
if (!
|
|
526
|
+
if (!_isValidBase64(encodedTrack)) throw ERRORS.INVALID_TRACK
|
|
407
527
|
return this.makeRequest('GET', `${this._endpoints.decodetrack}${encodeURIComponent(encodedTrack)}`)
|
|
408
528
|
}
|
|
409
529
|
|
|
410
530
|
async decodeTracks(encodedTracks) {
|
|
411
531
|
if (!Array.isArray(encodedTracks) || encodedTracks.length === 0) throw ERRORS.INVALID_TRACKS
|
|
412
532
|
for (let i = 0; i < encodedTracks.length; i++) {
|
|
413
|
-
if (!
|
|
533
|
+
if (!_isValidBase64(encodedTracks[i])) throw ERRORS.INVALID_TRACKS
|
|
414
534
|
}
|
|
415
535
|
return this.makeRequest('POST', this._endpoints.decodetracks, encodedTracks)
|
|
416
536
|
}
|
|
@@ -442,7 +562,7 @@ class Rest {
|
|
|
442
562
|
async getLyrics({ track, skipTrackSource = false }) {
|
|
443
563
|
const guildId = track?.guild_id ?? track?.guildId
|
|
444
564
|
const encoded = track?.encoded
|
|
445
|
-
const hasEncoded = typeof encoded === 'string' && encoded.length > 0 &&
|
|
565
|
+
const hasEncoded = typeof encoded === 'string' && encoded.length > 0 && _isValidBase64(encoded)
|
|
446
566
|
const title = track?.info?.title
|
|
447
567
|
|
|
448
568
|
if (!track || (!guildId && !hasEncoded && !title)) {
|
|
@@ -505,15 +625,6 @@ class Rest {
|
|
|
505
625
|
}
|
|
506
626
|
|
|
507
627
|
destroy() {
|
|
508
|
-
const privateInfo = privateData.get(this)
|
|
509
|
-
|
|
510
|
-
if (privateInfo?.cleanupCallbacks) {
|
|
511
|
-
for (const callback of privateInfo.cleanupCallbacks) {
|
|
512
|
-
try { callback() } catch {}
|
|
513
|
-
}
|
|
514
|
-
privateInfo.cleanupCallbacks.clear()
|
|
515
|
-
}
|
|
516
|
-
|
|
517
628
|
if (this.agent) {
|
|
518
629
|
this.agent.destroy()
|
|
519
630
|
this.agent = null
|
|
@@ -531,8 +642,6 @@ class Rest {
|
|
|
531
642
|
this.request = null
|
|
532
643
|
this.defaultHeaders = null
|
|
533
644
|
this._endpoints = null
|
|
534
|
-
|
|
535
|
-
privateData.delete(this)
|
|
536
645
|
}
|
|
537
646
|
}
|
|
538
647
|
|