const { EventEmitter } = require('events');
const { WebSocket } = require('ws');
/**
* Represents a Player that connects to a node and interacts with a guild's session.
* @extends EventEmitter
* @class
*/
class Player extends EventEmitter {
/**
* Creates a new Player instance.
* @param {Object} options - The options for the Player.
* @param {string} options.guildId - The ID of the guild.
* @param {Object} options.node - The node to connect to.
*/
constructor(options) {
super();
/**
* The ID of the guild.
* @type {string}
*/
this.guildId = options.guildId;
/**
* The node to connect to.
* @type {string}
*/
this.node = options.node;
/**
* The connection information.
* @type {Object}
*/
this.connectionInfo = {
token: null,
endpoint: null,
sessionId: null,
};
/**
* Listening web socket, works with NodeLink only
* @type {Object}
*/
this.listeningWebSocket = null;
/**
* The position of the track
* @type {number}
* @default 0
*/
this.position = 0;
/**
* The track that is currently playing
* @type {Object}
*/
this.track = null;
/**
* Whether the player is paused
* @type {boolean}
* @default false
*/
this.paused = false;
/**
* The volume of the player
* @type {number}
* @default 100
*/
this.volume = 100;
/**
* The filters of the player
* @type {Object}
* @default {}
*/
this.filters = {};
/**
* The status of the player
* @type {string}
* @default 'stopped'
*/
this.status = 'stopped';
}
/**
* Function for handling events
* @function
* @param {Object} data - The data to handle
*/
handleEvents = (data) => {
switch (data.type) {
case 'TrackStartEvent':
this.emit('trackStart', data.track);
this.emit('start', data.track);
this.track = data.track;
this.status = 'playing';
break;
case 'TrackEndEvent':
this.emit('trackEnd', data.track);
this.emit('end', data.track);
this.track = null;
this.status = 'stopped';
break;
case 'TrackExceptionEvent':
this.emit('trackException', data.track);
this.track = null;
this.status = 'stopped';
break;
case 'TrackStuckEvent':
this.emit('trackStuck', data.track);
this.track = null;
this.status = 'stopped';
break;
case 'WebSocketClosedEvent':
this.emit('webSocketClosed', data);
this.track = null;
this.status = 'stopped';
break;
}
};
/**
* Function for handling player updates
* @function
* @param {Object} data - The data to handle
* @return {Object} - This player instance
*/
handlePlayerUpdate = (data) => {
this.position = data.state.position;
this.lastUpdate = data.state.time;
this.connected = data.state.connected;
this.ping = data.state.ping;
};
/**
* Start connection between LavaLink/NodeLink and Discord voice server
* @function
* @async
*/
connect = async () => {
this.update({
voice: {
token: this.connectionInfo.token,
endpoint: this.connectionInfo.endpoint,
sessionId: this.connectionInfo.sessionId,
},
});
};
/**
* Update player data
* @function
* @async
* @param {Object} data - The data to update
* @param {boolean} noReplace - Whether to replace the data
* @return {Object} - Request result
*/
update = async (data, noReplace) => {
if (!this.node.sessionId) throw new Error('Node is not ready');
const res = await globalThis.fetch(
`${this.node.fetchUrl}/v4/sessions/${this.node.sessionId}/players/${
this.guildId
}?noReplace=${!noReplace ? true : noReplace}`,
{
method: 'PATCH',
headers: {
Authorization: this.node.pass,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}
);
return await res.json();
};
/**
* Destroy this player on the server
* @function
* @async
* @return {Object} - Request result
*/
destroy = async () => {
const res = await globalThis.fetch(
`${this.node.fetchUrl}/v4/sessions/${this.node.sessionId}/players/${this.guildId}`,
{
method: 'DELETE',
headers: {
Authorization: this.node.pass,
},
}
);
return res;
};
/**
* Get this player's information
* @function
* @async
* @return {Object} - This player's information
*/
get = async () => {
if (!this.node.sessionId) throw new Error('Node is not ready');
const res = await globalThis.fetch(
`${this.node.fetchUrl}/v4/sessions/${this.node.sessionId}/players/${this.guildId}`,
{
headers: {
Authorization: this.node.pass,
},
}
);
const result = await res.json();
this.position = result.state.position;
this.ping = result.state.ping;
this.connected = result.state.connected;
return result;
};
/**
* Plays a track
* @function
* @async
* @param {Object} data - The data to play
* @param {string} data.track - The base64 encoded track to play
* @return {Object} - Request result
*/
play = async (data) => {
return await this.update(data);
};
/**
* Pause playing
* @function
* @async
* @return {Object} - Request result
*/
pause = async () => {
this.paused = true;
return await this.update({
paused: true,
});
};
/**
* Resume playing
* @function
* @async
* @return {Object} - Request result
*/
resume = async () => {
this.paused = false;
return await this.update({
paused: false,
});
};
/**
* Stop playing
* @function
* @async
* @returns {Object} - Request result
*/
stop = async () => {
this.track = null;
return await this.update({
track: null,
});
};
/**
* Set volume
* @function
* @async
* @param {number} data - The volume to set
* @return {Object} - Request result
*/
setVolume = async (data) => {
if (data >= 1000 || data < 0) {
throw new Error('Volume must be between 0 and 1000');
} else {
this.volume = data;
return await this.update({
volume: data,
});
}
};
/**
* Set filter
* @function
* @async
* @param {Object} data - The filter to set
* @return {Object} - This node instance
*/
setFilter = async (data) => {
this.filters = { ...this.filters, ...data };
return await this.update({
filters: data,
});
};
/**
* Clear filter
* @function
* @async
* @return {Object} - Request result
*/
clearFilter = async () => {
this.filters = {};
return await this.update({
filters: {},
});
};
/**
* Get volume
* @function
* @async
* @return {number} - Volume
*/
getVolume = async () => {
const { volume } = await this.get();
return volume;
};
/**
* Get filters
* @function
* @async
* @return {Object} - Filters
*/
getFilters = async () => {
const { filters } = await this.get();
return filters;
};
/**
* Seek track
* @function
* @async
* @param {number} int - The position to seek to
* @return {number} - Request result
*/
seek = async (int) => {
this.position = int;
return await this.update({
position: int,
});
};
/**
* Start listening on VC
* @function
* @async
* @return {Object} - An event emitter for listening
*/
startListen = async () => {
if (this.listeningWebSocket) return this.listeningWebSocket;
const listener = new EventEmitter();
const listeningWebSocket = new WebSocket(`${this.node.url}/connection/data`, {
headers: {
Authorization: this.node.pass,
'user-id': this.node.botId,
'guild-id': this.guildId,
'Client-Name': this.node.userAgent,
},
});
this.listeningWebSocket = listeningWebSocket;
listeningWebSocket.on('open', function () {
listener.emit('open');
});
listeningWebSocket.on('message', function (data) {
const message = JSON.parse(data);
if (message.type === 'startSpeakingEvent') {
listener.emit('startSpeaking', message.data);
/*
{
op: 'speak',
type: 'startSpeakingEvent',
data: { userId: '897295756124360744', guildId: '919809544648020008' }
}
*/
}
if (message.type == 'endSpeakingEvent') {
listener.emit('endSpeaking', message.data);
/*
{
op: 'speak',
type: 'endSpeakingEvent',
data: {
userId: '897295756124360744',
guildId: '919809544648020008',
data: 'Raw PCM data'
type: 'pcm'
}
}
*/
}
});
listeningWebSocket.on('close', function () {
listener.emit('close');
});
listeningWebSocket.on('error', function () {
listener.emit('error');
});
return listener;
};
/**
* Stop listening on VC
* @function
* @async
* @return {Boolean} Return true if stopped listening
*/
stopListen = async () => {
if (!this.listeningWebSocket) return false;
this.listeningWebSocket.close();
this.listeningWebSocket = null;
return true;
};
}
module.exports = { Player };