import http from 'node:http' import { URL } from 'node:url' import connectionHandler from './handler.js' import inputHandler from './inputHandler.js' import config from '../../config.js' import { debugLog, parseClientName } from '../utils.js' import { WebSocketServer } from '../ws.js' if (typeof config.server.port !== 'number') throw new Error('Port must be a number.') if (typeof config.server.password !== 'string') throw new Error('Password must be a string.') if (typeof config.options.threshold !== 'boolean' && typeof config.options.threshold !== 'number') throw new Error('Threshold must be a boolean or a number.') if (typeof config.options.playerUpdateInterval !== 'boolean' && typeof config.options.playerUpdateInterval !== 'number') throw new Error('Player update interval must be a boolean or a number.') if (typeof config.options.statsInterval !== 'boolean' && typeof config.options.statsInterval !== 'number') throw new Error('Stats interval must be a boolean or a number.') if (typeof config.options.maxResultsLength !== 'number') throw new Error('Max results length must be a number.') if (typeof config.options.maxAlbumPlaylistLength !== 'number') throw new Error('Max album playlist length must be a number.') if (typeof config.options.maxCaptionsLength !== 'number') throw new Error('Max captions length must be a number.') if (typeof config.options.bypassAgeRestriction !== 'boolean') throw new Error('Bypass age restriction must be a boolean.') if (!['bandcamp', 'deezer', 'soundcloud', 'youtube', 'ytmusic'].includes(config.search.defaultSearchSource)) throw new Error('Default search source must be either "bandcamp", "deezer", "soundcloud", "youtube" or "ytmusic".') if (config.search.fallbackSearchSource === 'soundcloud') throw new Error('SoundCloud is not supported as a fallback source.') if (config.search.sources.spotify.enabled && !config.search.sources.spotify.market) throw new Error('Spotify is enabled but no market was provided.') if (config.search.sources.deezer.enabled && config.search.sources.deezer.decryptionKey === 'DISABLED') throw new Error('Deezer is enabled but no decryption key or API key was provided.') if (config.search.sources.soundcloud.enabled && config.search.sources.soundcloud.clientId === 'DISABLED') throw new Error('SoundCloud is enabled but no client ID was provided.') if (![ 'high', 'medium', 'low', 'lowest' ].includes(config.audio.quality)) throw new Error('Audio quality must be either "high", "medium", "low" or "lowest".') if (![ 'xsalsa20_poly1305', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305_lite' ].includes(config.audio.encryption)) throw new Error('Encryption must be either "xsalsa20_poly1305", "xsalsa20_poly1305_suffix" or "xsalsa20_poly1305_lite".') if (typeof config.voiceReceive.timeout !== 'number') throw new Error('Voice receive timeout must be a number.') if (![ 'opus', 'pcm' ].includes(config.voiceReceive.type)) throw new Error('Voice receive type must be either "opus" or "pcm".') const server = http.createServer(connectionHandler.requestHandler) const v4 = new WebSocketServer() v4.on('/v4/websocket', connectionHandler.configureConnection) v4.on('/connection/data', inputHandler.setupConnection) server.on('upgrade', (req, socket, head) => { const { pathname } = new URL(req.url, `http://${req.headers.host}`) if (req.headers.authorization !== config.server.password) { debugLog('disconnect', 3, { name: 'Unknown', version: '0.0.0', code: 401, reason: 'Invalid password' }) req.socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n') return req.socket.destroy() } const parsedClientName = parseClientName(req.headers['client-name']) if (!parsedClientName) { debugLog('connect', 1, { name: req.headers['client-name'], error: 'Client-name doesn\'t conform to NAME/VERSION format.' }) req.socket.write('HTTP/1.1 400 Bad Request\r\n\r\n') return req.socket.destroy() } req.clientInfo = parsedClientName if (pathname === '/v4/websocket') { debugLog('connect', 3, parsedClientName) v4.handleUpgrade(req, socket, head, { 'isNodeLink': true }, (ws) => v4.emit('/v4/websocket', ws, req, parsedClientName)) } if (pathname === '/connection/data') { if (!req.headers['guild-id'] || !req.headers['user-id']) { debugLog('connectCD', 1, { ...parsedClientName, error: `"${!req.headers['guild-id'] ? 'guild-id' : 'user-id'}" header not provided.` }) req.socket.write('HTTP/1.1 400 Bad Request\r\n\r\n') return req.socket.destroy() } debugLog('connectCD', 3, { ...parsedClientName, guildId: req.headers['guild-id'] }) v4.handleUpgrade(req, socket, head, {}, (ws) => v4.emit('/connection/data', ws, req, parsedClientName)) } }) v4.on('error', (err) => { debugLog('error', 3, { error: err.message }) }) server.on('error', (err) => { debugLog('http', 1, { error: err.message }) }) server.listen(config.server.port || 2333, () => { console.log(`[\u001b[32mwebsocket\u001b[37m]: Listening on port \u001b[94m${config.server.port || 2333}\u001b[37m.`) })