Skip to content

Commit

Permalink
Added VoiceReciever from NodeLink to Poru.
Browse files Browse the repository at this point in the history
  • Loading branch information
Joniii11 committed May 8, 2024
1 parent 9360463 commit 5315f0a
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 2 deletions.
4 changes: 4 additions & 0 deletions src/Node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export class Node {
public readonly name: string;
public readonly restURL: string;
public readonly socketURL: string;
public readonly host: string;
public readonly port: number;
public password: string;
public readonly secure: boolean;
public readonly regions: Array<string> | null;
Expand Down Expand Up @@ -151,6 +153,8 @@ export class Node {
this.poru = poru;
this.name = node.name;
this.options = node;
this.host = node.host;
this.port = node.port;
this.secure = node.secure || false;
this.restURL = `http${node.secure ? "s" : ""}://${node.host}:${node.port}`;
this.socketURL = `${this.secure ? "wss" : "ws"}://${node.host}:${node.port}/v4/websocket`;
Expand Down
212 changes: 212 additions & 0 deletions src/Player/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Queue from "../guild/Queue"
import { EventEmitter } from "events"
import { Filters } from "./Filters"
import { Response, LoadTrackResponse } from "../guild/Response"
import WebSocket from "ws"

type Loop = "NONE" | "TRACK" | "QUEUE"

Expand All @@ -15,6 +16,53 @@ const escapeRegExp = (str: string) => {
} catch { }
}

interface BaseVoiceRecieverEvent {
op: "speak"
};

export interface StartSpeakingEventVoiceRecieverData {
/**
* The user ID of the user who started speaking.
*/
userId: string;

/**
* The guild ID of the guild where the user started speaking.
*/
guildId: string;
};

export interface EndSpeakingEventVoiceRecieverData {
/**
* The user ID of the user who stopped speaking.
*/
userId: string;
/**
* The guild ID of the guild where the user stopped speaking.
*/
guildId: string;
/**
* The audio data received from the user in base64.
*/
data: string;
/**
* The type of the audio data. Can be either opus or pcm. Older versions may include ogg/opus.
*/
type: "opus" | "pcm" | "ogg/opus"
}

export interface StartSpeakingEventVoiceReciever extends BaseVoiceRecieverEvent {
type: "startSpeakingEvent",
data: StartSpeakingEventVoiceRecieverData
};

export interface EndSpeakingEventVoiceReciever extends BaseVoiceRecieverEvent {
type: "endSpeakingEvent",
data: EndSpeakingEventVoiceRecieverData
}

export type VoiceRecieverEvent = StartSpeakingEventVoiceReciever | EndSpeakingEventVoiceReciever;

/**
* Represents a player capable of playing audio tracks.
* @extends EventEmitter
Expand Down Expand Up @@ -66,6 +114,13 @@ export class Player extends EventEmitter {
/** The volume of the player (0-1000) */
public volume: number

/** Should only be used when the node is a NodeLink */
protected voiceRecieverWsClient: WebSocket | null
protected isConnectToVoiceReciever: boolean;
protected voiceRecieverReconnectTimeout: NodeJS.Timeout | null;
protected voiceRecieverAttempt: number;
protected voiceRecieverReconnectTries: number;

constructor(poru: Poru, node: Node, options: ConnectionOptions) {
super()
this.poru = poru
Expand All @@ -92,6 +147,13 @@ export class Player extends EventEmitter {
this.loop = "NONE"
this.data = {}

this.voiceRecieverWsClient = null;
this.isConnectToVoiceReciever = false;
this.voiceRecieverReconnectTimeout = null;
this.voiceRecieverAttempt = 0;
this.voiceRecieverReconnectTries = 3;


this.poru.emit("playerCreate", this)
this.on("playerUpdate", (packet) => {
(this.isConnected = packet.state.connected),
Expand Down Expand Up @@ -586,4 +648,154 @@ export class Player extends EventEmitter {

this.poru.send({ op: 4, d: data })
};

public async setupVoiceRecieverConnection() {
return new Promise<boolean>(async (resolve, reject) => {
if (!this.node.isNodeLink) return reject(new Error("[Poru Exception] This function is only available for NodeLink nodes."));
if (!this.poru.userId) return reject(new Error("[Poru Error] No user id found in the Poru instance. Consider using a supported library."))

if (this.voiceRecieverWsClient) await this.removeVoiceRecieverConnection();

const headers: { [key: string]: string } = {
Authorization: this.node.password,
"User-Id": this.poru.userId,
"Guild-Id": this.guildId,
"Client-Name": this.node.clientName,
};

this.voiceRecieverWsClient = new WebSocket(`${this.node.secure ? "wss" : "ws"}://${this.node.host}:${this.node.port}/connection/data`, { headers });
this.voiceRecieverWsClient.on("open", this.voiceRecieverOpen.bind(this));
this.voiceRecieverWsClient.on("error", this.voiceRecieverError.bind(this));
this.voiceRecieverWsClient.on("message", this.voiceRecieverMessage.bind(this));
this.voiceRecieverWsClient.on("close", this.voiceRecieverClose.bind(this));

return resolve(true);
})
};

public async removeVoiceRecieverConnection() {
return new Promise<boolean>((resolve, reject) => {
if (!this.node.isNodeLink) return reject(new Error("[Poru Exception] This function is only available for NodeLink nodes."));

this.voiceRecieverWsClient?.close(1000, "destroy");
this.voiceRecieverWsClient?.removeAllListeners();
this.voiceRecieverWsClient = null;
this.isConnectToVoiceReciever = false;

return resolve(true);
})
};

// Private stuff
/**
* This will close the connection to the node
* @param {any} event any
* @returns {void} void
*/
private async voiceRecieverClose(event: any): Promise<void> {
try {
await this.voiceRecieverDisconnect();
this.poru.emit("debug", this.node.name, `[Voice Reciever Web Socket] Connection was closed with the following Error code: ${event || "Unknown code"}`);

if (event !== 1000) await this.voiceRecieverReconnect();
} catch (error) {
this.poru.emit("debug", "[Voice Reciever Web Socket] Error while closing the connection with the node.", error);
};
};

/**
* Handles the message event
* @param payload any
* @returns {void}
*/
private async voiceRecieverReconnect(): Promise<void> {
this.voiceRecieverReconnectTimeout = setTimeout(async () => {
if (this.voiceRecieverAttempt > this.voiceRecieverReconnectTries) {
throw new Error(
`[Poru Voice Reciever Websocket] Unable to connect with ${this.node.name} node to the voice Reciever Websocket after ${this.voiceRecieverReconnectTries} tries`
);
}
// Delete the ws instance
this.isConnected = false;
this.voiceRecieverWsClient?.removeAllListeners();
this.voiceRecieverWsClient = null;

this.poru.emit("debug", this.node.name, `[Voice Reciever Web Socket] Reconnecting to the voice Reciever Websocket...`)

await this.setupVoiceRecieverConnection();
this.voiceRecieverAttempt++;
}, this.node.reconnectTimeout);
};

/**
* This function will make the node disconnect
* @returns {Promise<void>} void
*/
private async voiceRecieverDisconnect(): Promise<void> {
if (!this.isConnectToVoiceReciever) return;

this.voiceRecieverWsClient?.close(1000, "destroy");
this.voiceRecieverWsClient?.removeAllListeners();
this.voiceRecieverWsClient = null;
this.poru.emit("voiceRecieverDisconnected", this, `[Voice Reciever Web Socket] Connection was closed.`);
};

/**
* This function will open up again the node
* @returns {Promise<void>} The Promise<void>
*/
private async voiceRecieverOpen(): Promise<void> {
try {
if (this.voiceRecieverReconnectTimeout) {
clearTimeout(this.voiceRecieverReconnectTimeout);
this.voiceRecieverReconnectTimeout = null;
};

this.isConnectToVoiceReciever = true;
this.poru.emit("voiceRecieverConnected", this, `[Voice Reciever Web Socket] Connection ready ${this.node.socketURL}/connection/data`)
} catch (error) {
this.poru.emit("debug", `[Voice Reciever Web Socket] Error while opening the connection with the node ${this.node.name}. to the voice Reciever Websocket.`, error)
};
};

/**
* This will send a message to the node
* @param {string} payload The sent payload we recieved in stringified form
* @returns {Promise<void>} Return void
*/
private async voiceRecieverMessage(payload: string): Promise<void> {
try {
const packet = JSON.parse(payload) as VoiceRecieverEvent;
if (!packet?.op) return;

this.poru.emit("debug", this.node.name, `[Voice Reciever Web Socket] Recieved a payload: ${payload}`)

switch (packet.type) {
case "startSpeakingEvent": {
this.poru.emit("startSpeaking", this, packet.data);
break;
}
case "endSpeakingEvent": {
this.poru.emit("endSpeaking", this, packet.data);
break;
}
default: {
this.poru.emit("debug", this.node.name, `[Voice Reciever Web Socket] Recieved an unknown payload: ${payload}`)
break;
}
}
} catch (err) {
this.poru.emit("voiceRecieverError", this, "[Voice Reciever Web Socket] Error while parsing the payload. " + err);
};
};
/**
* This function will emit the error so that the user's listeners can get them and listen to them
* @param {any} event any
* @returns {void} void
*/
private voiceRecieverError(event: any): void {
if (!event) return
this.poru.emit("voiceRecieverError", this, event);
this.poru.emit("debug", `[Voice Reciever Web Socket] Connection for NodeLink Voice Reciever (${this.node.name}) has the following error code: ${event.code || event}`);
};
}
41 changes: 39 additions & 2 deletions src/Poru.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Node, NodelinkGetLyricsInterface, NodeStats } from "./Node/Node";
import { Player } from "./Player/Player";
import { EndSpeakingEventVoiceRecieverData, Player, StartSpeakingEventVoiceRecieverData } from "./Player/Player";
import { EventEmitter } from "events";
import { Config as config } from "./config";
import { Response, LoadTrackResponse } from "./guild/Response";
Expand Down Expand Up @@ -245,14 +245,51 @@ export interface PoruEvents {
*/
playerDestroy: (player: Player) => void;


/**
* Emitted when a socket connection is closed.
* @param {Player} player - The player associated with the socket.
* @param {Track} track - The track associated with the socket.
* @param {WebSocketClosedEvent} data - Additional data related to the socket closure.
*/
socketClose: (player: Player, track: Track, data: WebSocketClosedEvent) => void;

/**
* Emitted when a voice Reciever was setup and the user started speaking.
* @param {Player} player - The player associated with the voice reciever.
* @param {StartSpeakingEventVoiceRecieverData} data - Additional data related to the start of speaking.
*/
startSpeaking: (player: Player, data: StartSpeakingEventVoiceRecieverData) => void;

/**
* Emitted when a voice Reciever was setup and the user stopped speaking.
* @param {Player} player - The player associated with the voice reciever.
* @param {EndSpeakingEventVoiceRecieverData} data - Additional data related to the end of speaking including the voice data.
*/
endSpeaking: (player: Player, data: EndSpeakingEventVoiceRecieverData) => void;

/**
* Emitted when a voice Reciever encounters an error.
* @param player The player associated with the voice reciever.
* @param error The error that occurred.
* @returns
*/
voiceRecieverError: (player: Player, error: any) => void;

/**
* Emitted when a voice Reciever connected itself.
* @param player The player associated with the voice reciever.
* @param reason The reason for the connection.
* @returns
*/
voiceRecieverConnected: (player: Player, status: string) => void;

/**
* Emitted when a voice Reciever disconnected itself.
* @param player The player associated with the voice reciever.
* @param reason The reason for the disconnection.
* @returns
*/
voiceRecieverDisconnected: (player: Player, reason: string) => void;
}

export declare interface Poru {
Expand Down

0 comments on commit 5315f0a

Please sign in to comment.